Skip to content

[Content]: Schedule Unpublish Button - #4028

Open
geodem127 wants to merge 58 commits into
devfrom
feature/3151-schedule-unpublish-button
Open

[Content]: Schedule Unpublish Button#4028
geodem127 wants to merge 58 commits into
devfrom
feature/3151-schedule-unpublish-button

Conversation

@geodem127

@geodem127 geodem127 commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

What

Adds Schedule Unpublish and Cancel Scheduled Unpublish flows to the content item editor.

Changes

New functionality

  • New menu items in ItemEditHeaderActions (published state only): "Schedule Unpublish" / "Unschedule Unpublish"
  • SchedulePublish now accepts scheduledAction="unpublish" prop and routes to a dedicated ScheduleUnpublishDialog component
  • ScheduledUnpublishIndicator badge on the published label (data-cy="ScheduledUnpublishIndicator")
  • PublishStatus component shows scheduled-unpublish badge when the active version has a future unpublishAt
  • Item edit header tooltips (saved / published / scheduled) now resolve and display the acting user's name instead of a raw ZUID

Architecture

  • SchedulePublish refactored into a smart container (index.tsx) and two dumb presentation components:
    • SchedulePublishDialog — publish scheduling flow (exact pre-PR behaviour)
    • ScheduleUnpublishDialog — unpublish scheduling flow (new)
  • All state, handlers, and computed values live in the smart container; dialogs receive only props
  • TooltipTitle now takes a resolved userName prop instead of userZUID + the full users list; each caller (ItemEditHeaderActions, PublishStatus) resolves the name itself

Bug fixes

  • unpublish() thunk now re-throws in .catch() so handleUnscheduleUnpublish step-2 is correctly gated on step-1 success
  • handleSchedulePublish / handleScheduleUnpublish: onScheduleSuccess moved from .finally() to .then() so it only fires on success, not on API error
  • handleUnschedulePublish: .catch() added to prevent unhandled promise rejection leaking to Sentry (error notification is already handled by the thunk)
  • handleSchedulePublish / handleScheduleUnpublish: .catch() added for the same reason
  • hasScheduledUnpublish wrapped in !!() so its type is boolean rather than string | boolean | undefined
  • Audit type imported in ItemEditHeaderActions to satisfy noImplicitAny
  • Cancel-unpublish: pre-deletes conflicting scheduled publish before POSTing unpublishAt: "never" (avoids API "already has scheduled publish event" error)
  • publish() thunk notification correctly shows "Cancelled scheduled unpublish for …" for the publishAt: "now" / unpublishAt: "never" path (success and error branches)
  • handleUnscheduleUnpublish: fixed throw new Error(response?.error?.message)publish()/unpublish() resolve their .catch() to { error: message } where message is a plain string, so .message was always undefined. Now throws response?.error directly so the real error text is preserved.
  • publish() thunk .catch(): added the missing branch for a failed handleScheduleUnpublish call (publishAt: "now" + a future unpublishAt). It previously fell through to the generic "Error publishing …" toast instead of an unpublish-specific one.
  • Fixed a bug flagged by negative-QA: hasScheduledUnpublish (ItemEditHeaderActions), isAlreadyScheduledUnpublish (SchedulePublish), and the PublishStatus badge all gated visibility on the currently loaded/edited version matching the published version's version number. Saving a new draft bumps meta.version, so the badge and "Unschedule Unpublish" menu label disappeared even though the scheduled unpublish was still armed on the published version, with no way to cancel it from the UI. All three now gate on item.publishing.isPublished, which reflects the published version's own state independent of what's currently loaded/edited.
  • handleScheduleUnpublish now mirrors the hasAnyScheduledPublish pre-deletion guard already used by handleUnscheduleUnpublish: it deletes a conflicting scheduled publish before POSTing the new unpublishAt schedule, instead of letting the API reject it with "already has a scheduled publish event." ScheduleUnpublishDialog already warns the user that scheduling an unpublish cancels a pending scheduled publish — this makes that promise actually hold. Flagged in review.

Cypress

  • New specs: Schedules an item for unpublishing, Cancels a scheduled unpublish
  • Each test is self-contained with cy.visit() + cy.wait([items, publishings])
  • "Schedules" test cancels the scheduled unpublish at the end to avoid state leaking into subsequent tests
  • "Cancels" test has a defensive cleanup block at the start for robustness across reruns
Screen.Recording.2026-03-26.at.2.20.33.AM.mov

NOTE: This PR is required to resolve a failing test #4153

@geodem127 geodem127 changed the title ]Content: Schedule Unpublish Button [Content]: Schedule Unpublish Button Mar 25, 2026
@geodem127
geodem127 requested review from agalin920 and finnar-bin and removed request for agalin920 March 25, 2026 18:23
@geodem127 geodem127 linked an issue Mar 30, 2026 that may be closed by this pull request
@geodem127
geodem127 force-pushed the feature/3151-schedule-unpublish-button branch from 4c2e3e7 to 6609be8 Compare April 11, 2026 19:47
Comment thread src/shell/components/ScheduleUnpublish/index.tsx Outdated
@geodem127
geodem127 requested a review from agalin920 April 16, 2026 20:52
@github-actions

github-actions Bot commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Overall Coverage

Lines Statements Branches Functions
60.83% 60.40% 55.61% 55.39%

Changed Files Coverage

File Lines Statements Branches Functions
src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/ItemEditHeaderActions.tsx 71.49% 71.49% 73.86% 69.23%
src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/PublishStatus.tsx 100.00% 100.00% 78.12% 100.00%
src/apps/content-editor/src/app/views/ItemEdit/components/ItemEditHeader/TooltipTitle.tsx 100.00% 100.00% 66.66% 100.00%
src/shell/components/SchedulePublish/SchedulePublishDialog.tsx 100.00% 100.00% 85.71% 100.00%
src/shell/components/SchedulePublish/ScheduleUnpublishDialog.tsx 100.00% 100.00% 72.22% 100.00%
src/shell/components/SchedulePublish/index.tsx 90.90% 91.17% 64.86% 83.33%
src/shell/store/content.js 79.82% 79.42% 69.97% 87.09%

Comment thread src/shell/components/SchedulePublish/index.tsx Outdated
Comment thread src/shell/store/content.js Outdated
Comment thread src/shell/components/SchedulePublish/index.tsx Outdated
agalin920
agalin920 previously approved these changes May 11, 2026
@agalin920
agalin920 self-requested a review May 28, 2026 20:45
@agalin920

agalin920 commented May 28, 2026

Copy link
Copy Markdown
Contributor

QA — Gaps & concerns + suggested Cypress coverage

Verified the core Schedule Unpublish flow end-to-end against a live published item (open menu → Schedule Unpublish → set date → schedule → indicator shows → Unschedule → state restored). The POST …/publishings payloads are correct (publishAt:"now" + unpublishAt:<UTC> to schedule, unpublishAt:"never" to cancel). The feature works. A few things to address:

Note: "Solution item 1/2" below refer to the two numbered bullets in issue #3151's Solution section — not to other tickets.

Gaps & concerns

  • Unschedule-Unpublish dialog omits the scheduled date. The body renders "v241 is scheduled to unpublish on ⟨blank⟩ in (GMT-08:00) Pacific Time…". Root cause: src/shell/components/SchedulePublish/index.tsx:155 derives scheduledLocalText only from item.scheduling.publishAt, but a scheduled unpublish date lives in item.publishing.unpublishAt, so it resolves to "". Cosmetic (cancel still works, and the header indicator tooltip is wired to unpublishAt), but the confirmation should show the date being cancelled.
  • "Invalid Time" label persists on the time field even after a valid future time is selected (the UTC preview computes correctly and the confirm button stays enabled). Looks like a stale validation/aria label on FieldTypeDateTime's time sub-field — worth a look as it can confuse users.
  • No edit-in-place for an existing scheduled unpublish date. Issue Content: Schedule Unpublish Button #3151's Solution item 2 mentions "modify the unpublish date later", but currently once a schedule exists the dialog shows only the info-alert + "Cancel Scheduled Unpublish" (no datetime picker), so changing a date is a two-step cancel + re-schedule rather than an edit.
  • Publish-time prompt not included. Issue Content: Schedule Unpublish Button #3151's Solution item 1 ("prompt to set an unpublish date during content publishing") isn't part of this change — unpublish scheduling is a separate post-publish menu action and ConfirmPublishModal is untouched. Flagging in case it's expected here vs. a follow-up.

Suggested Cypress coverage

The new cases in cypress/e2e/content/actions.spec.js ("Schedules an item for unpublishing", "Cancels a scheduled unpublish") cover the indicator appearing/disappearing. Recommended additions in that spec:

  • Assert the API payload on schedule — intercept POST …/publishings and assert the body is { publishAt: "now", unpublishAt: <UTC string>, version }. Locks in the publish-vs-unpublish payload branching that is the heart of this change.
  • Assert the schedule dialog labeling — after clicking UnpublishScheduleButton, assert the header reads "Schedule Unpublish:", the field label reads "Unpublish on", and the confirm button reads "Schedule Unpublish". Guards the publish/unpublish copy from regressing.
  • Assert the date is shown in the Unschedule dialog (would catch the blank-date defect above) — open UnschedulePublishButton and assert the body text contains the scheduled date, not an empty string.
  • Assert the menu toggles to "Unschedule Unpublish" once a schedule exists, and that the header indicator tooltip shows the unpublish date and publisher name.

@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Review summary

Solid extension to the existing SchedulePublish flow that reuses the publish endpoint to handle scheduled unpublish. The shared modal + scheduledAction prop is the right shape; the menu refactor with MENU_ACTION_LABELS / SCHEDULE_ACTION_LABELS is a nice cleanup.

A few issues to address before merge — inline comments below for specifics.

Bugs

  1. Wrong date rendered in the "Cancel Scheduled Unpublish" dialog. scheduledLocalText is derived from item.scheduling.publishAt, but when isForUnpublish is true and the item is published with a scheduled unpublishAt, scheduling is empty and the displayed date will be blank. Should fall back to item.publishing.unpublishAt when in the unpublish flow.
  2. "Already scheduled" dialog branch fires for the wrong flow. The condition item?.scheduling?.isScheduled || item?.publishing?.unpublishAt collapses two independent states. If an item is published with a scheduled unpublish AND the user opens "Schedule Publish" (e.g. from dirty state), the modal will render the "this is already scheduled" UI instead of the date picker — the user can't pick a new publish date. The branch should be gated by isForUnpublish alongside the underlying state.
  3. scheduledAction is never reset. When the modal closes via onClose, setScheduledPublishDialogOpen(false) is called directly (not via the wrapped setter), so scheduledAction retains its previous value. If the modal is re-opened via a path that doesn't pass an explicit action, you'll get stale behavior. Reset it on close, or reset alongside the dialog open state.

Test concerns

  1. Inter-test dependencies in the new specs. "Schedules an item for unpublishing" and "Cancels a scheduled unpublish" don't cy.visit() to set up state; they rely on the previous spec leaving the page on a published item. CLAUDE.md notes that runner 4 runs these specs together, so they may pass today, but the tests are no longer self-contained. Add an explicit cy.visit() + cy.wait([items, publishings]) like the sibling "Publishes an item" test does. Also: the cancel test depends on the schedule test having run first — if the schedule test fails, the cancel test silently hides the regression.

Behavior question

  1. Re-publish() to clear a scheduled unpublish. The comment explains the rationale, but worth confirming with backend that a POST /publishings with publishAt: "now" against an already-published item updates the existing publishing record in place (no new version, no audit-trail noise). If it creates a new publishing row each time the user cancels, that'll be visible in publish history.

Comment thread src/shell/components/SchedulePublish/index.tsx Outdated
Comment thread cypress/e2e/content/actions.spec.js
Comment thread src/shell/components/SchedulePublish/index.tsx Outdated
Comment thread src/shell/components/SchedulePublish/index.tsx Outdated
@agalin920

agalin920 commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

QA — Gaps & concerns + suggested Cypress coverage

Verified the core Schedule Unpublish flow end-to-end against a live published item (open menu → Schedule Unpublish → set date → schedule → indicator shows → Unschedule → state restored). The POST …/publishings payloads are correct (publishAt:"now" + unpublishAt:<UTC> to schedule, unpublishAt:"never" to cancel). The feature works. A few things to address:

Note: "Solution item 1/2" below refer to the two numbered bullets in issue #3151's Solution section — not to other tickets.

Gaps & concerns

  • Unschedule-Unpublish dialog omits the scheduled date. The body renders "v241 is scheduled to unpublish on ⟨blank⟩ in (GMT-08:00) Pacific Time…". Root cause: src/shell/components/SchedulePublish/index.tsx:155 derives scheduledLocalText only from item.scheduling.publishAt, but a scheduled unpublish date lives in item.publishing.unpublishAt, so it resolves to "". Cosmetic (cancel still works, and the header indicator tooltip is wired to unpublishAt), but the confirmation should show the date being cancelled.
  • "Invalid Time" label persists on the time field even after a valid future time is selected (the UTC preview computes correctly and the confirm button stays enabled). Looks like a stale validation/aria label on FieldTypeDateTime's time sub-field — worth a look as it can confuse users.
  • No edit-in-place for an existing scheduled unpublish date. Issue Content: Schedule Unpublish Button #3151's Solution item 2 mentions "modify the unpublish date later", but currently once a schedule exists the dialog shows only the info-alert + "Cancel Scheduled Unpublish" (no datetime picker), so changing a date is a two-step cancel + re-schedule rather than an edit.
  • Publish-time prompt not included. Issue Content: Schedule Unpublish Button #3151's Solution item 1 ("prompt to set an unpublish date during content publishing") isn't part of this change — unpublish scheduling is a separate post-publish menu action and ConfirmPublishModal is untouched. Flagging in case it's expected here vs. a follow-up.

Suggested Cypress coverage

The new cases in cypress/e2e/content/actions.spec.js ("Schedules an item for unpublishing", "Cancels a scheduled unpublish") cover the indicator appearing/disappearing. Recommended additions in that spec:

  • Assert the API payload on schedule — intercept POST …/publishings and assert the body is { publishAt: "now", unpublishAt: <UTC string>, version }. Locks in the publish-vs-unpublish payload branching that is the heart of this change.
  • Assert the schedule dialog labeling — after clicking UnpublishScheduleButton, assert the header reads "Schedule Unpublish:", the field label reads "Unpublish on", and the confirm button reads "Schedule Unpublish". Guards the publish/unpublish copy from regressing.
  • Assert the date is shown in the Unschedule dialog (would catch the blank-date defect above) — open UnschedulePublishButton and assert the body text contains the scheduled date, not an empty string.
  • Assert the menu toggles to "Unschedule Unpublish" once a schedule exists, and that the header indicator tooltip shows the unpublish date and publisher name.

@geodem127 please state the changes that have been introduced due to this review

@geodem127

geodem127 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor Author

@agalin920 — addressed in 64058ee. Here's what was changed:

Gaps & concerns:

  • Blank date in Unschedule Unpublish dialog — already fixed in a prior commit via `dateText = isForUnpublish ? item.publishing.unpublishAt : item.scheduling.publishAt`, so `scheduledLocalText` correctly shows the unpublish date.

  • "Invalid Time" label

  • ℹ️ No edit-in-place for existing scheduled unpublish — will need to reach out to product since design for this scenario has not been included.

  • ℹ️ Publish-time unpublish prompt — will need to reach out to product since design for this scenario has not been included.

Cypress coverage additions:

  • API payload assertion — `cy.wait(publishItem).then()` asserts `publishAt === "now"`, `unpublishAt` matches UTC datetime format, and `version` is a number.
  • Dialog labeling assertion — within `SchedulePublishModal`, asserts "Schedule Unpublish:" header, "Unpublish on" field label, and button text "Schedule Unpublish".
  • Scheduled date in Unschedule dialog — asserts "scheduled to unpublish on" text and a matching date string (`/\w{3} \d{1,2}, \d{4} at \d{1,2}:\d{2}/`) are present.
  • Menu toggle label — after scheduling, opens the menu and asserts `UnpublishScheduleButton` contains "Unschedule Unpublish".

Both new tests are now fully self-contained (`cy.visit` + `cy.wait`); the "Cancels" test schedules an unpublish before cancelling it.

Comment thread src/shell/components/SchedulePublish/index.tsx
Comment thread src/shell/components/SchedulePublish/index.tsx
@github-actions

Copy link
Copy Markdown
Contributor

Code Review — 🔴 1 blocker(s) · 🟡 1 warning(s) — see inline comments

Comment thread src/shell/components/SchedulePublish/index.tsx
@github-actions

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers · 🟡 2 warning(s) — see inline comments

@github-actions

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers

@github-actions

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers

@agalin920

Copy link
Copy Markdown
Contributor

@geodem127 please run ci on this

Comment thread src/shell/components/SchedulePublish/index.tsx
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers · 🟡 2 warning(s) — see inline comments

Comment thread src/shell/components/SchedulePublish/index.tsx
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers · 🟡 2 warning(s) — see inline comments

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers · 🟡 1 warning(s) — see inline comments

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers

@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers · 🟡 1 warning(s) — see inline comments

placement="bottom-start"
>
<Stack
data-cy="ScheduledUnpublishIndicator"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Duplicate data-cy="ScheduledUnpublishIndicator" rendered simultaneously by two components with independent state

ItemEditHeaderActions.tsx (line ~651) also puts data-cy="ScheduledUnpublishIndicator" on its "Published" label whenever its own hasScheduledUnpublish (derived from the legacy state.content[itemZUID].publishing slice) is true. Both components are siblings under the same ItemEditHeader (index.tsx:247 and :255), so when viewing the currently-published version — the common case, since this block has no !== currentVersion guard unlike the sibling activePublishing/scheduledPublishing blocks above it — two DOM nodes will match this selector at once, sourced from different queries (RTK useGetItemPublishingsQuery here vs. legacy Redux state there) that can go out of sync after schedule/unschedule actions. The new Cypress spec asserts cy.getBySelector("ScheduledUnpublishIndicator").should("not.exist") after unscheduling — if one of the two sources lags, that assertion is a flake risk in addition to violating the project's one-selector-one-target convention. Use a distinct data-cy here (e.g. PublishStatusScheduledUnpublishIndicator) or derive both from the same data source.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Code Review — ✅ No blockers · 🟡 1 warning(s) — see inline comments

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.

Content: Schedule Unpublish Button

3 participants