Skip to content

feat(editor): per-platform media assignment UI - #12

Merged
Cryptoom merged 3 commits into
mainfrom
claude/tpx-08-a5-vue-ui
Sep 17, 2026
Merged

Cryptoom merged 3 commits into
mainfrom
claude/tpx-08-a5-vue-ui

Conversation

@Cryptoom

@Cryptoom Cryptoom commented Sep 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a media toggle grid to each platform card in the post editor (Edit.vue), letting the user narrow which of the post's media items a specific platform publishes. Default is "all on" (empty pivot = applies to every media item, matching A1's data model and today's behaviour).
  • New platformMediaIds state threads through PostEditorTabs to ScheduleTab to ChannelConfigurator to new MediaAssignmentGrid.vue, mirroring the existing platformContentTypes/platformMeta pattern.
  • Backend contract, confirmed compatible with A4's MCP work (PR feat(mcp): per-platform media parameters for attach and update tools #13): UpdatePostRequest validates platforms.*.media_ids against the media on this same request (IDOR guard), UpdatePost action syncs the PostPlatform::media() pivot (Arr::has distinguishes an omitted key, left untouched, from an explicit empty-array clear). PostController::edit() appends media_ids per PostPlatform instance only (not globally via $appends), so Index/Calendar listings that don't eager-load media aren't hit with an unwanted lazy load under strict model mode.
  • i18n: posts.edit.media_assignment.label/included/excluded added to all 16 locales.
  • Deliberate scope boundary: this PR is web-UI only. The public REST API and MCP tools getting media_ids/post_platform_ids support is A4's PR feat(mcp): per-platform media parameters for attach and update tools #13, not duplicated here.

Review fixes (second commit)

Two real findings from the first review round, both fixed:

  1. Toggle-off-everything bug: deselecting the last remaining media item on a platform flipped the array back to [], which means "unscoped, applies to all" everywhere it's read, silently re-including every item instead of leaving none selected. Fixed by keeping the last remaining item on: a platform always publishes at least one media item once scoped.
  2. Missing card wrapper: MediaAssignmentGrid.vue rendered as a bare div, so two selected platforms produced two identical, unbordered blocks with no visible way to tell which belonged to which platform. Now uses the same card pattern as sibling settings panels (DiscordSettings.vue et al) with a platform icon header, and the checkmark badge reuses the existing Badge variant="success" token instead of duplicating its classes.

Screenshot review

Verified with Playwright against a locally seeded post (2 platforms, 3 media items):

  • Desktop (1920x1080): both platform cards are now visually distinct (own border, own platform icon), scoped platform shows the excluded item dimmed, unscoped platform shows all included.
  • Deselecting item 1, then item 2, then item 3 on a 3-item post: the third click is blocked, item 2 stays on, confirming the toggle-off fix.
  • Toggling persists across a full page reload (round tripped through the real validation plus pivot sync path, not mocked).
  • Mobile (iPhone 15 Pro, 393x852): same grid renders correctly under the Channels mobile tab, touch targets measured at 56x56px, well above the 44px minimum.
  • No em-dashes.

Test plan

Generated with Claude Code

@Cryptoom
Cryptoom force-pushed the claude/tpx-08-a5-vue-ui branch from c5cae71 to ad169c5 Compare September 16, 2026 23:03
Cryptoom and others added 3 commits September 17, 2026 09:36
Adds a media toggle grid to each platform card in the post editor
(Edit.vue), letting the user narrow which media items a specific
platform publishes. Default is "all on", matching today's unscoped
behaviour (empty pivot = applies to every post media item, per A1).

New state `platformMediaIds` threads through PostEditorTabs ->
ScheduleTab -> ChannelConfigurator -> new MediaAssignmentGrid.vue,
mirroring the existing platformContentTypes/platformMeta pattern.

Backend: UpdatePostRequest validates `platforms.*.media_ids` against
the media on this same request (IDOR guard), UpdatePost syncs the
PostPlatform::media() pivot (Arr::has distinguishes an omitted key
from an explicit empty-array clear). PostController::edit() appends
media_ids per PostPlatform instance only (not globally via $appends),
so the Index/Calendar listings that don't eager-load `media` aren't
hit with an unwanted lazy load under strict model mode.

Verified with Playwright against a locally seeded post/platforms:
toggling excludes a media item from that platform's grid only,
persists across reload, and the excluded item stays visually
distinct (checkmark badge uses the existing emerald-200 "success"
token) at both desktop (1920x1080) and mobile (iPhone 15 Pro,
393x852) widths.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two fixes from review:

1. Toggle-off-everything bug: an empty platformMediaIds array means
   "unscoped, applies to all" everywhere it's read (scopedMediaItems(),
   the grid itself), so it can never also mean "explicitly none".
   Deselecting the last remaining item flipped the array back to []
   and silently re-included every item, the opposite of what the user
   just did. MediaAssignmentGrid.toggle() now keeps the last remaining
   item on: a platform always publishes at least one of the post's
   media items when scoped.

2. Design review: MediaAssignmentGrid.vue rendered as a bare div with
   no card wrapper, so two selected platforms produced two identical,
   unbordered "Media for this channel" blocks with no visible way to
   tell which belonged to which platform. It now uses the same card
   pattern as its sibling settings panels (DiscordSettings.vue et al,
   rounded-xl border-2 border-foreground bg-card shadow-2xs) with a
   platform icon header, and the checkmark badge reuses the existing
   Badge variant="success" token instead of duplicating its classes.

Verified with Playwright: toggling item 1, then item 2, then item 3
off on a 3-item post leaves item 2 on (the guard fires on the third
click instead of silently clearing back to "all"), and the two
platform cards are now visually distinct with their own borders and
platform icons.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a browser test for the off-by-one fix in ad169c5: with 3 media
items on a post, deselecting item 1 then item 2 leaves item 3 as the
last remaining selection, and a third click on item 3 must be a no-op
(the platform can never end up scoped to zero items, since an empty
selection is read as "unscoped, applies to all" everywhere else).

Asserts item 3 stays selected AND items 1/2 stay deselected after the
guarded click, which is the part a weaker assertion (checking only
that item 3 is still on) would miss: a regression to the old bug would
silently flip the selection back to "all", re-including items 1 and 2
too.
@Cryptoom
Cryptoom force-pushed the claude/tpx-08-a5-vue-ui branch from f42c054 to 6350a24 Compare September 17, 2026 06:39
@Cryptoom
Cryptoom merged commit dfb7a05 into main Sep 17, 2026
@Cryptoom
Cryptoom deleted the claude/tpx-08-a5-vue-ui branch September 17, 2026 06:39
Cryptoom added a commit that referenced this pull request Sep 17, 2026
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