Skip to content

feat: complete bulk actions across all publish tabs + composer stay-on-save#117

Open
cominop wants to merge 42 commits into
brightbeanxyz:mainfrom
cominop:feat/bulk-actions
Open

feat: complete bulk actions across all publish tabs + composer stay-on-save#117
cominop wants to merge 42 commits into
brightbeanxyz:mainfrom
cominop:feat/bulk-actions

Conversation

@cominop

@cominop cominop commented Jun 30, 2026

Copy link
Copy Markdown

Summary

Completes the bulk actions feature across all three publish tabs (Drafts, Queue, Approvals) and fixes the composer redirecting users away after saving.

Changes

Bulk Actions (Drafts, Queue, Approvals)

  • Approvals tab now has full bulk action support: approve, reject, delete via modal dialogs
  • Queue tab now has bulk delete with confirmation modal
  • All Apply buttons and modal confirm buttons refactored from fragile inline Alpine.js @click handlers into named x-data methods — eliminated 199 lines of inline JavaScript
  • Approvals tab auto-refreshes after bulk actions complete, updating filter counts and moving posts to the correct filter without a manual page reload

Composer Stay-on-Save

  • Removed the redirect to calendar after schedule/publish/queue actions
  • All save actions now stay in the composer with a save confirmation toast — users can continue editing without re-navigating

Production Fixes

  • Media files now serve correctly on Railway (Django static() doesn't register in production — replaced with explicit re_path)
  • Calendar timezone crash fixed (missing tzdata package in slim Python image)
  • Media volume sync on container startup (adds missing files without overwriting uploads)
  • Pinterest link_url and alt_text passed through platform_extra on PlatformPost

Screenshots

(to be added)

How to test

  1. Open Publish → Approvals, select multiple posts, use Bulk Actions → Reject
  2. Confirm the modal appears, add a reason, click Reject — tab should refresh automatically
  3. From Drafts, select a post, use Submit for Approval → Apply
  4. Open composer, edit a post, click Schedule — verify you stay in the composer
  5. Navigate to calendar, confirm no timezone crash

Sharehaus Bot added 30 commits June 27, 2026 12:43
Adds per-row selection checkboxes and WordPress-style bulk action header
to all 4 publish tabs (Drafts, Queue, Approvals, Sent).

- Drafts: Delete, Send to Queue
- Queue: Delete
- Approvals: Delete, Approve, Reject, Publish
- Sent: Delete
- Delete/reject modals with confirmation dialogs
- HX-Redirect back to correct tab after action completes
- CSRF meta tag for HTMX AJAX requests in base.html

No migrations required. Backwards-compatible — existing single-row
approve/reject/delete actions are unchanged.
… Queue

- publish_queue.html: wire hx-post + hx-trigger on hidden bulk form
- publish_approvals.html: hx-trigger submit, add approve branch, early return for delete/reject
- publish_drafts.html: add 3 new bulk actions to dropdown
- apps/calendar/views.py: add submit_for_approval, add_to_queue, next_available handlers

Next Available queries PostingSlot table for first open slot per workspace.
Railway v5 doesn't execute 'railway run' commands remotely, so migrations
were never applied to the deployed DB. Added entrypoint.sh that runs
'migrate' + 'collectstatic' before starting gunicorn.
base.html referenced js/unsplash-modal.js but the Unsplash app is not
installed in this repo, causing 500 on every page (Missing staticfiles
manifest entry). Commented out the references.
- CreatePostRequest: new link_url + alt_text fields
- composer/services.create_post: accepts link_url + alt_text, writes to
  PlatformPost.platform_extra as {link_url, alt_text}; alt_text falls
  back to MediaAsset.alt_text when caller leaves it blank
- api/routers/posts.py: pass payload.link_url + payload.alt_text through

Pinterest provider already reads content.link_url and content.extra['alt_text']
from PublishContent, which is now populated.
Restored apps/unsplash/ from origin/feature/unsplash-integration:
- views.py, urls.py, models, serializers, services
- templates/integrations/unsplash_modal.html
- static/js/unsplash-modal.js (also copied to repo static/)

Wired up:
- Added 'apps.unsplash' to INSTALLED_APPS
- Added context processor (unsplash_modal) for the modal HTML
- Mounted /api/v1/ in config/urls.py to include apps.unsplash.urls
- Restored {{ unsplash_modal_html }} and unsplash-modal.js references in base.html

The frontend modal was already in templates/composer/compose.html — it
was calling composer:unsplash_search which returned 404 because views.py
source was missing (only stale __pycache__/views.cpython-314.pyc existed).

Modal now renders + search endpoint works.
views.py imports rest_framework.permissions/status/generics and the
DRF TokenAuthentication class requires rest_framework.authtoken. None
were installed.

- requirements.txt: add djangorestframework>=3.15,<4.0
- INSTALLED_APPS: add 'rest_framework' + 'rest_framework.authtoken'
- Add REST_FRAMEWORK dict with TokenAuthentication + SessionAuthentication
Restored changes that were lost during incorrect revert cascade:

- apps/unsplash/: full source from origin/feature/unsplash-integration
- config/settings/base.py: DRF + apps.unsplash entry + context processor
- config/settings/base.py: REST_FRAMEWORK config (TokenAuth+SessionAuth)
- config/urls.py: /api/v1/ include for apps.unsplash.urls
- config/urls.py: unguard static() so /media/ works in production
- templates/base.html: uncomment unsplash-modal.js + modal HTML
- requirements.txt: djangorestframework
- entrypoint.sh: restore original (no MEDIA_ROOT export, no cp -r media)
Uses cp -rn (recursive, no-clobber) to fill in media files that are
missing from the Railway volume without overwriting existing uploads.
This is a safe addition to the original entrypoint.sh from 9fccf81.
static() gtk_helper was not producing a working URL pattern in production.
Switched to explicit re_path with django.views.static.serve.
…s/composer/

The upstream main branch has Unsplash integrated directly into apps/composer/
(views.py:unsplash_search, views.py:unsplash_import). The separate apps/unsplash/
plugin from feature/unsplash-integration was never part of the deployed build.

Removed:
- apps/unsplash/ directory and all files
- rest_framework + rest_framework.authtoken from INSTALLED_APPS
- apps.unsplash from LOCAL_APPS
- apps.unsplash.context_processors.unsplash_modal from TEMPLATES
- REST_FRAMEWORK config block
- /api/v1/ include for apps.unsplash.urls from config/urls.py
- djangorestframework from requirements.txt

Kept:
- media serving fix (re_path + serve)
- tzdata for Canada/Eastern timezone
- entrypoint.sh with cp -rn media sync
- base.html unsplash references (benign)
- compose.html inline Alpine modal (untouched — the real Unsplash integration)
…ng is incomplete

processing_status view returns JsonResponse when media isn't ready yet.
HTMX hx-swap='outerHTML' was swapping this JSON into the DOM as raw text.
HX-Reswap: none tells HTMX to leave the existing HTML in place.
The @click handler only handled 'delete' and 'send_to_queue'. All other
action values (submit_for_approval, add_to_queue, next_available) were
silently ignored — the button did nothing.

Added else clause that submits any remaining action value through the
hidden HTMX form, matching the same pattern delete already uses.
The bulk action buttons were clearing the form with innerHTML='', which
destroyed the CSRF token and current_tab hidden input. Subsequent form
submissions failed silently because Django rejected them as CSRF violations.

Now re-adds both elements after clearing form for:
- Apply button (approve/publish actions)
- Delete modal button
- Reject modal button
…uote breakage

insertAdjacentHTML with {% csrf_token %} injects HTML that contains double
quotes, which prematurely terminates the @click="..." attribute. Replaced
with querySelector + cloneNode(true) to preserve the CSRF input element
without generating template-level quote conflicts.
The inline @click handler was fragile with multi-line JS embedded in
a Django template. Replaced with a named Alpine method applyApprovalBulkAction
defined in the x-data object, keeping @click clean: @click="applyApprovalBulkAction"
… x-data methods

Drafts tab (publish_drafts.html):
- Apply button → applyBulkAction() + _submitBulkForm()
- Delete modal → confirmDelete()
- Reject modal → confirmReject()

Queue tab (publish_queue.html):
- Apply button → applyBulkAction()
- Bulk delete modal → confirmDelete()

Approvals tab (publish_approvals.html):
- Delete modal → confirmApprovalDelete()
- Reject modal → confirmApprovalReject()
- Shared _submitBulkForm() helper

Eliminated all multi-line inline JavaScript from publish tab templates.
Every bulk action now calls a named method in the Alpine x-data object.
The approvals bulk_action view returns HTTP 204 (no content), so
the tab content was never refreshed after approve/reject/delete. Added
an htmx.ajax GET to reload the current filter view into #tab-content
after the bulk action completes, matching the active status_filter.
After schedule/publish/queue, the user was dumped to the calendar grid view
which forced them to hunt for the post in the grid, switch back to list view,
find it, and re-enter the composer. Now redirects to the publish list view
(?view=list) so the user lands on the tabbed publish page where they
can immediately see their scheduled/queued posts.
Removed the redirect to calendar/list that fired after schedule,
publish_now, and add_to_queue actions. Now every save (draft, schedule,
publish, queue) displays the "Saved" toast and keeps the user in the
composer so they can continue editing without friction.
@cominop cominop requested a review from JanSchm as a code owner June 30, 2026 04:43
Sharehaus Bot added 11 commits June 30, 2026 23:08
The bulk delete modal submitted action=delete but the backend only
handled approve and reject. Added a delete handler that iterates over
post_ids and calls post.delete() for each, returning proper results.
The dropdown listed Publish but the backend only handled approve, reject,
and delete. Added a publish handler that transitions each post's PlatformPost
children to scheduled status with scheduled_at=now(), enabling one-click
bulk publish from the approvals tab.
- Add Select All checkbox to Approvals and Queue bulk action headers
- Refactor Sent tab: replace fragile inline @click handlers with
  Alpine x-data methods (applyBulkAction, confirmDelete)
- All 4 publish tabs now have consistent select-all + bulk action UI
…ting login form

Added global htmx:beforeSwap handler that detects 302 redirects to login
or login page HTML returned as 200 (server restart mid-request). Prevents
HTMX from injecting the login form into the current tab content area.
…e layout

Auth pages (login, signup, password reset) now extend the minimal
base_entrance.html instead of base.html. This prevents the full app
shell (sidebar, channels, navigation) from rendering behind the login
form when the session expires or the server restarts.
… layout

All pages that extend base_entrance.html now:
- login, logout, signup, email_confirm
- password_reset, password_reset_done
- password_reset_from_key, password_reset_from_key_done
- verification_sent

Added extra_js block to base_entrance.html needed by signup template.
The auth templates reference CSS variables (--text-primary, --border, etc.)
defined in the compiled styles.css. Replaced the Tailwind CDN script with
the static styles.css link so all form fields, buttons, and layouts render
correctly.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant