Overview

As discussed in Slack at https://drupal.slack.com/archives/C072JMEPUS1/p1762326259781829, we need the ability to unpublish Canvas pages.

Canvas pages currently lack unpublish functionality that would allow hiding previously published pages from visitors. We cannot simply set status to 0 because draft pages are also unpublished, and we need to distinguish between these states in the UI.

We use the term "unpublish" because it matches existing Drupal concepts (content moderation, entity status). This approach keeps the implementation simple and won't complicate the planned Workspaces integration (Q2 2026).

Proposed resolution

Implement an "Unpublish" action for Canvas pages with a simple approach:

  • Allow users to unpublish previously published pages
  • Unpublishing is staged through auto-save and committed with "Review changes" workflow
  • Homepage cannot be unpublished
  • No Views integration needed (keeps implementation simple)
  • Works alongside the Trash module for complementary workflows

How it works (Technical)

  • New PATCH endpoint updates the auto-save entry's published status
  • When user clicks "Unpublish", it updates the auto-save entry (not the live entity)
  • When "Review changes" is published, the unpublish action takes effect on the live entity
  • Uses existing entityIsConsideredNew logic to distinguish drafts from unpublished pages

Publication States

  • Draft: Page is new (never published), identified by default "Untitled page" title
  • Published: Page is live (status = 1)
  • Unpublished: Page was published before but now has status = 0

User interface changes

Before:

  • No ability to unpublish pages

After:

  • "Unpublish page" link in Page Data dropdown for published pages
  • "Publish page" link for unpublished (non-draft) pages
  • "🕦Unpublish" indicator for pages pending unpublish action in Review changes
  • "Unpublished" status badge for unpublished pages
  • Homepage protection prevents unpublishing

Complementary solutions

Trash module: For soft-deleting pages (different from unpublishing)

  • Trash = page is pending removal (recoverable deletion)
  • Unpublish = page is hidden from visitors but remains fully editable

Canvas is compatible with the Trash module for sites using Drupal CMS.

Remaining questions

All previous questions have been resolved. MR 543 simplifies the approach by:

  • Using the existing auto-save workflow instead of Views plugins
  • Leveraging entityIsConsideredNew to distinguish drafts from unpublished pages
  • Avoiding extra complexity that would complicate Workspaces integration

Previous questions (all resolved)

  1. This issue assumes we need to distinguish between archived and unpublished pages. Currently they both have status = 0, only difference is draft pages have never been published Resolved: Using entityIsConsideredNew logic
  2. Do we want to avoid adding extra field to store publication state? Resolved: No extra field needed
  3. Will Nodes edited directly in Canvas also need the 3 states? Resolved: Yes, handled by same approach
  4. When we use Workspaces will it be a hard dependency? Resolved: Not relevant to this simplified approach
  5. Do we need extra permissions for state changes? Resolved: No extra permissions needed

Follow-up issues

Issue fork canvas-3556265

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

mherchel created an issue. See original summary.

lauriii made their first commit to this issue’s fork.

tedbow’s picture

Status: Active » Needs work

Haven't review but "Needs work" at least for test failures

vipin.mittal18’s picture

StatusFileSize
new4.63 MB

To enable the archive feature, code fixes are needed, as the content cannot be published even it is archiving upon changes. Refer attached video

narendrar made their first commit to this issue’s fork.

tedbow’s picture

There is no information about how this work or the expected behavior which should be in the issue summary.

Also it needs tests, there are existing test classes for the controllers on the phpunit side which would be good to start with.

narendrar’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

With current approach, ApiContentControllers::normalize() - The 'isNew' field indicates if this is a draft (never published) vs archived (previously published). Frontend uses this to show correct badge ("Draft" vs "Archived") and determine available operations.

Current MR addresses most of the feature for this issue, i.e:

  • "Archive" action is displayed in the contextual menu for pages ✅
  • Pages listing (admin/content/pages) indicates if a particular page has been archived ❌
  • Homepage cannot be archived ✅
  • Archived content is marked as unpublished when the changes are published ✅
  • Archived status in the top bar (both inside and outside Canvas) ✅
  • Once a published page is archived, redirect it to 404 ❌

admin/content/pages is a view and it is not differentiating between draft/archived as both are unpublished state.
I am not sure if we should continue with the current approach and modify the view to handle the draft/archived state, or if we should change our approach entirely—perhaps by adding a new field_archived to the page entity, or using existing 'published' field for archive status (2)—or handle it in another way. Open to suggestions.

tedbow’s picture

Issue summary: View changes

@narendrar thanks update in #8, thats very helpful!

admin/content/pages is a view and it is not differentiating between draft/archived as both are unpublished state.

I think this is key problem. @lauriii should confirm but it can't imagine it would acceptable to have admin/content/pages not be able to distinguish betwen draft and archived. It would be very confusing to when you unpublished a bunch of pages they would show at the top of this listing as "unpublished"

I considered these options to solve this problem

Options Considered for Draft vs Archived Differentiation

Problem: Views can't distinguish between draft (never published) and archived (previously published) pages since both have
status=0.

Approaches Considered:

  1. Computed Field (No Storage)
    • Calculate draft/archived state on-the-fly by checking revision history
    • ❌ Rejected: Views can't query computed fields without storage; performance concerns with N+1 queries
  2. Revision History Queries Only
    • Keep current status field, query revision history in Views/reports as needed
    • ❌ Rejected: Complex to implement in Views; poor performance for bulk operations
  3. Content Moderation Module
    • Use Drupal's built-in workflow system with draft/published/archived states
    • ❌ Not chosen: Adds significant complexity; overkill for simple publish/unpublish workflow
  4. Stored publication_state Field with Auto-SyncChosen
    • Add a stored field (draft/published/archived) that updates automatically when status changes
    • Uses onChange() hook to keep field synchronized with publication status
    • For unpublished pages, checks database revision history to determine draft vs archived

Why Chosen:

  • ✅ Views-compatible (can filter, sort, display the field)
  • ✅ Performance (no repeated queries, indexed field)
  • ✅ Immediate consistency (updates on status change)
  • ✅ Correctness (always checks actual database state, not in-memory values)
  • ✅ Self-healing (automatically corrects if manually tampered with)

I pushed up new PR to explore the chosen https://git.drupalcode.org/project/canvas/-/merge_requests/409

It solves this only for Page but right now this all we support.

I think the problem with this approach is I think there is plan to some day use Workspaces. and we might not need this field any more.

tedbow’s picture

How pushed up another MR https://git.drupalcode.org/project/canvas/-/merge_requests/411

You may have guessed that AI made this summary, and made the code with me helping, but I think this good idea if we still plan on moving to Workspaces and don't want to add a field we will have to get rid of

We could also add the view integration in a follow-up. but good to know it is possible without too much code

Temporary Views-Only Publication State Field

This implementation adds Draft/Published/Archived differentiation in Views without storing data in the database. This approach was chosen
specifically because Canvas will eventually integrate with the Workspaces module, which provides its own content staging and workflow capabilities.

Why Not Store the Field?

Adding a stored publication_state field would create significant backward compatibility issues:

  1. Data migration complexity - Would need update hooks to migrate stored values when switching to Workspaces
  2. Conflicting systems - Having both publication_state and Workspace states would be confusing
  3. Hard to deprecate - Can't easily remove a stored field without breaking existing sites
  4. Technical debt - Would require maintaining two systems during transition

How This Works

The implementation uses SQL CASE expressions with EXISTS subqueries to compute state dynamically:

CASE
    WHEN status = 1 THEN 'published'
    WHEN status = 0 AND EXISTS(published revision) THEN 'archived'
    ELSE 'draft'
  END
  

This provides full Views functionality (display, filter, sort) without touching the entity schema.

Removal Path

When Workspaces support is added:

  1. Delete the 4 Views plugin files (already marked @deprecated)
  2. Update default Views configs to use Workspace fields
  3. Document upgrade path for custom Views

No data migration, no stored field cleanup, no complex update hooks.

Files

  • src/Hook/CanvasViewsHooks.php - Registers field/filter/sort with Views
  • src/Plugin/views/field/PublicationState.php - Display handler
  • src/Plugin/views/filter/PublicationStateFilter.php - Filter handler
  • src/Plugin/views/sort/PublicationStateSort.php - Sort handler

All files are marked as deprecated and reference the future Workspaces integration.

tedbow’s picture

Title: Create ability to unpublish Canvas page » Create ability to archive Canvas pages
Issue summary: View changes
Related issues: +#3493461: Identify roadblocks to staging config in Workspaces (e.g., via wse_config), +#3512616: Document the reasons for not using Workspaces for saving in XB 1.0.0
  1. changing the title to reflect "archive". If we just had to unpublish and did have to distinguish between pages archived page, pages that had been published in the past, and draft, pages that have never been published this would be easier.
  2. I added a "Remaining questions" section to the summary to ask if we want to avoid adding a field, mostly because we might use Workspaces eventually. I did some of the
lauriii’s picture

This issue assumes we need to distinguish between archived and unpublished pages. Currently they both have status = 0, only difference is draft pages have never been published

That would be ideal but we should not implement this at the cost of making the future Workspaces integration significantly more complicated.

Do we want to avoid adding extra field store, to allow distinguishing between draft and archive? We may want avoid this if we eventually want to still use Workspaces.

We are planning to work on Workspaces support in 2026 so we should avoid making the migration to Workspaces significantly more complicated than it's today. Are there drawbacks with the Views solution that we should be aware of because you're generically claiming that it might be better to go with the alternative approach? I assume we want to prioritize easy migration to Workspaces over whatever that is but it might be good to document the downsides explicitly.

tedbow’s picture

Version: 1.0.0-rc2 » 1.x-dev
Assigned: Unassigned » narendrar
Issue tags: +Needs issue summary update

@lauriii

Are there drawbacks with the Views solution that we should be aware of because you're generically claiming that it might be better to go with the alternative approach?

I just generally if we weren't planning to switch Workspaces that it would easier to keep the logic of the Page publiciation state internal to the Page entity rather than relying on special views integration and \Drupal\canvas\AutoSave\AutoSaveManager::entityIsDraft. It would be more understandable and maintainable.

But since we are planning on moving to Workspaces and since we are planning to add Node edits, which also has the problem of only have boolean status, determine 3 possible publication states(if we want display "archived" next to nodes), then rely on special logic in \Drupal\canvas\AutoSave\AutoSaveManager::entityIsDraft makes sense.

So @narendrar I have close the other merge requests and brought in the Views integration into the original MR.

The views integration should solve this problem from #8

Pages listing (admin/content/pages) indicates if a particular page has been archived ❌

We will not need to add any extra fields the Page entity and keep the approach of the original MR

vipin.mittal18’s picture

Issue summary: View changes
StatusFileSize
new275.27 KB
vipin.mittal18’s picture

StatusFileSize
new22.51 MB

Added workflow video

lauriii’s picture

Thanks for the workflow video! A couple of clarifications:

The workflow should allow going directly from Draft to Archived. There are valid use cases where a user might want to archive a draft page without ever publishing it first (e.g., a page that was started but is no longer needed, or content that was created speculatively and then abandoned). Forcing users to publish first just to archive seems unnecessarily restrictive.

Can we confirm the expected behavior for the publishing flow? When you have a draft page and go through "Review changes" → "Publish", this action should publish the page directly. There shouldn't be a separate/explicit "publish" step required before you can use the standard publish flow. In other words: Draft → Review changes → Publish should result in a published page. It should not require Draft → (explicit publish action) → Review changes → Publish which is what I see in the video in #19.

Could you update the workflow diagram to reflect these cases?

vipin.mittal18’s picture

Hello Laurii,
------------------------------------------------------------
Current (Existing) Behavior
- When a page is in Draft state and the user goes through
“Review changes” → “Publish”,
→ all current draft changes are published immediately.
------------------------------------------------------------
New Scenario: “Publish page” option in Top Dropdown
If a user:
- Starts creating a page, and
- Directly clicks “Publish page” from the top dropdown (without going through Review changes),

What is Recommended / Expected Behavior
Q: Should The page be published immediately without even touching top-right unpublished changes section?
Q: Should All unpublished changes visible in the top-right section be included in that published version?

lauriii’s picture

I think there may be some confusion here. When a page is in Draft state, the "Publish page" option should not be visible in the dropdown at all.

For draft pages, the available action should be "Archive". This allows users to archive a page that has never been published. Archiving a draft means the content won't be published when changes are reviewed/published; it essentially shelves the draft.

To clarify the expected behavior:
Draft page (never published):

  • "Publish page" option: Not visible
  • "Archive" option: Visible → page will remain unpublished when changes are published

Published page:

  • "Publish page" option: Not visible
  • "Archive" option: Visible → unpublishes the page when the changes are published

Archived page:

  • "Publish page" option: Visible → publishes the page when changes are published
  • "Archive" option: Not visible

I believe this was the behavior in an earlier version of the MR but has since changed. We should revert to that previous behavior. The workflow diagram should also be updated to show that Draft → Archived is a valid transition.

tedbow’s picture

Assigned: narendrar » lauriii
Issue summary: View changes
Status: Needs work » Postponed (maintainer needs more info)

@lauriii thanks for the clarification.

re

The workflow should allow going directly from Draft to Archived.

The current approach in 3556265-create-ability-to will not work in this case.
Since "Status" is boolean field it cannot record 3 different states, Draft, Published, Archive.

The current MR was getting around this by relying on

  1. Published: status === 1
  2. Draft: status === 0 AND no published revision exists
  3. Archive: status === 0 and at least 1 published revision exits

\Drupal\canvas\AutoSave\AutoSaveManager::entityIsDraft and the views integration depend on this logic

If you can go from draft -> archive then in that case the above won't work because there can be Archived pages without having a published revision.

I am not saying we can't do it, just we have use a different approach.

In !409 I added a publication_state field which can store 3 states but I closed that MR because in #14 you said

That would be ideal but we should not implement this at the cost of making the future Workspaces integration significantly more complicated.

I thinking adding a publication_state field that we would then need to remove because after "publication state" would be managed by Workspaces would make the migration to Workspaces more complicated.

Probably the migration to Workspaces to will be complicated anyways but if we don't add any new field in this issue we won't be making it worse than 1.x

But now I don't see how we get around adding the publication_state field to Page or some sort of storage. Below I will outline a plan for that but before that I think we need to consider how this would affect editing individual nodes(can't find an issue but 99% certain is on the roadmap).

Would nodes also need the same publication state workflow as pages? I assume yes, but should get confirmation as this would probably affect how we handle Pages now. Nodes will have the same problem of having a boolean status but needing store 3 publication states.

We could use hook_entity_base_field_info_alter to add the same publication_state field to Nodes that we do for Pages. But if we are going to do that we should probably use canvas_publication_state for both so don't have conflicts on node with other modules also altering the base fields(could happen on pages too?)

The other option would be to add a canvas_publication_state content entity type but that seems like we would be starting to reimplement the Workflows\Content Moderation modules

Transition to Workspaces

Assumptions for what Workspaces integration would mean

  1. Workspaces would be a hard requirement. You couldn't choose not to use Workspaces and instead use Canvas and the solution for draft/archive we come up with here. This is just a stop-gap
  2. There will be at least 1 "draft" workspace
  3. All pages/nodes in "draft" will be in this workspace(s?), not in the "live" site
  4. Archive pages/nodes will be in the "live" sites as unpublished, not in an "archived" workspace

If those assumption are correct then in an update hook when we require Workspaces we could

  1. Create a default "draft" workspace(maybe workspaces does this automatically?). We need to ensure a workspace is created if the site is already using workspaces(at least if there >0 drafts)
  2. Move all draft pages/nodes, as specified by the canvas_publication_state into the default workspace
  3. Remove all "draft" Pages/Node from the live site
  4. Remove the canvas_publication_state from both Page and Node. We should mark canvas_publication_state and any isDraft(), isArchived etc functions as internal/deprecated when we add it here, so we are free to do this.

Even if we didn't do the current issue I would assume we would still have to do 2 & 3 we would just determine draft by status === 0 && count(published revisions) === 0

So major complication would be adding canvas_publication_state to both Pages and Node(assuming editing of nodes happens before Workspaces integration), then removing it.

So this bring back to @lauriii's comments(in Italics)

in response to

This issue assumes we need to distinguish between archived and unpublished pages. Currently they both have status = 0, only difference is draft pages have never been published

That would be ideal but we should not implement this at the cost of making the future Workspaces integration significantly more complicated.

and

We are planning to work on Workspaces support in 2026 so we should avoid making the migration to Workspaces significantly more complicated than it's today.

I do think this would make "migration to Workspaces significantly more complicated than it's today.". What I detailed is not that complicated but I think there could definitely be "known unknowns" unless we are going to plan in detail now our migration to workspaces, and even then not sure, a plan is just a plan.

So do we not do this issue at all or do abandon

The workflow should allow going directly from Draft to Archived. There are valid use cases where a user might want to archive a draft page without ever publishing it first (e.g., a page that was started but is no longer needed, or content that was created speculatively and then abandoned). Forcing users to publish first just to archive seems unnecessarily restrictive.

I do understand this a valid use case but is worth making the migration to Workspaces more complicated? question for @laurii

I think the other options for

a page that was started but is no longer needed, or content that was created speculatively and then abandoned)

is just deleting the draft or choosing not to publish and leaving it in draft incase it is needed later. Maybe not ideal but maybe an ok comprise

I added extra questions to "Remaining questions"

mglaman’s picture

I am extremely concerned that Canvas is going to over complicate this and make Content Moderation not possible. I've already spoke to two different organizations who won't adopt Canvas due to lack of Content Moderation support for pages and their compliance requirements. I am also in the camp who believes content moderation and workspaces are not exclusive, so we still have to consider content moderation.

Published: status === 1
Draft: status === 0 AND no published revision exists
Archive: status === 0 and at least 1 published revision exits

This is how I would handle it if pages are not managed by content moderation. And expose a published boolean in the page data.

lauriii’s picture

Assigned: lauriii » Unassigned

Given the complexity of the ideal experience described here, I think we can remove the option to mark draft as archived. In that scenario, we should not display either publish or archive as an option for draft content.

For the use case of abandoning draft pages that are no longer needed, users can either delete the draft entirely or leave it in draft status by not publishing it. Not perfect, but a reasonable compromise that keeps the implementation simpler.

I've already spoke to two different organizations who won't adopt Canvas due to lack of Content Moderation support for pages and their compliance requirements. I am also in the camp who believes content moderation and workspaces are not exclusive, so we still have to consider content moderation.

That's true today. However, I personally believe we can achieve requirements that users of both modules have with a single solution.

tedbow’s picture

Assigned: Unassigned » narendrar
Status: Postponed (maintainer needs more info) » Needs work

@lauriii ok thanks for the clarification

@NarendraR that means we are back to MR 346 with the View integration

narendrar’s picture

Issue summary: View changes
Issue tags: -Needs tests, -Needs issue summary update
narendrar’s picture

Assigned: narendrar » Unassigned
Status: Needs work » Needs review

This feature is now ready for review. Features implemented:

  • A published page can now be archived from Canvas.
  • Homepage/Draft can not be archived from Canvas.
  • Archived page can be published again from Canvas.
  • Page Listing (/admin/content/pages) will show pages with status Draft, Archived and Published
utkarsh_33’s picture

While testing this i had some observations which may or may not be done as a part of this issue but just documenting it on the issue to get opinions:-

  • While archiving any page we need to publish the changes in order to make sure that the correct status of the page is listed on the (/admin/content/pages). The review changes modal contains the title Unpublished changes and when we select any archived page and say publish 1 selected, it might be confusing for the user what's happening all at once. What i suggest is change the title Unpublished changes to something more generic like Untracked changes and publish 1 selected to something more generic(I don't have a suggestion on top of my head).
tedbow’s picture

StatusFileSize
new29.6 KB
new17.85 KB

I am the middle of reviewing this but found this confusing UX, I also agree with #31 that is also confusing

  1. page listing showing archived for both currently archive and going to be archived page
    "published page" is page that is page that is currently published but I clicked "Archive page"
    "A page" is a currently archived page that has no changes

    Both of these having "Archived" label is confusing

  2. similarly in the "Review changes"
    changes listing
    The page that will be archived says "Archived". If didn't already know who this works I would probably
    assume this is currently archived page that is going to have changes made it. I think here it should be changed to "Archive"
tedbow’s picture

.

tedbow’s picture

Assigned: Unassigned » narendrar
Status: Needs review » Needs work

@narendrar getting closer!

I left a review and pushed up a couple commits

tedbow’s picture

Things look good. @NarendraR said he was going address a couple extra things.

It still failing linting

It would be good if someone else could review the JS

tedbow’s picture

I created #3565219: Setting homepage doesn't take into account draft and delete pages for problems I found in 1.x while testing the current issue

@NarendraR can you look that issue and

  1. Double check and make sure I am actually right about the current bug in 1.x
  2. If am correct, are these problems actually solved, by this issue? One or both of them?
  3. If this issue does fix it do we have tests to make sure ?

Thanks

narendrar’s picture

Assigned: narendrar » tedbow
Status: Needs work » Needs review

Re #32,
Now we have an visual identifier to differentiate:

  • When a page is set as archived but changes are not published, it will show Archive*
  • When a page is set as archived and changes are published, it will show Archived

Re #36, #3565219: Setting homepage doesn't take into account draft and delete pages, Problem 1 will not happen when this issue is merged as we are not showing 'Set as Homepage' for draft pages and we have tests to validate that. Problem 2 is still an issue.

We have some cypress tests failing for this MR, not sure if they are related to changes made here. Moving it to Needs review for another review.

narendrar’s picture

Please ignore my comment in #37 related to #36,

Re #36, #3565219: Setting homepage doesn't take into account draft and delete pages, Problem 1 will not happen when this issue is merged as we are not showing 'Set as Homepage' for draft pages and we have tests to validate that.

I have reverted the unnecessary changes in commit https://git.drupalcode.org/project/canvas/-/merge_requests/346/diffs?com... for draft page to not show 'Set as Homepage' as that is existing functionality and should not be altered.

So, both problems identified in #3565219: Setting homepage doesn't take into account draft and delete pages are not solved by this MR and needs to be handled separately in that issue.

wim leers’s picture

Component: … to be triaged » Page
Status: Needs review » Needs work
wim leers’s picture

Oh and the CR talks about relying on Content Moderation, but neither the issue title nor summary say that? 😅 So: is that indeed what is happening?

Clarifying all that would improve reviewability of this MR 🙏

EDIT: skimming the >30 comments above it seems that in the last ~dozen comments there's been some pivots based on feedback from @lauriii in response to Ted, Naren & Matt. Summarizing those into the issue summary is essentially what I'm asking 😇

tedbow’s picture

Issue summary: View changes

remaining questions have been answered

tedbow’s picture

Assigned: tedbow » Unassigned
Issue summary: View changes
Issue tags: -Needs title update, -Needs issue summary update

#40, I have updated the summary. This issue will not require Workspaces but does acknowledge that we will use workspaces eventually and therefore how Pages are determined to be "draft" will change. This also affects how "archived" pages are determinedbecause currently there is no field explicitly tell the difference between draft and archived both unpublished.

Content Moderation will not be used, per Lauriii

Re the change recorded I am not sure we need one, https://git.drupalcode.org/project/canvas/-/merge_requests/346#note_647461. Or if we do maybe it should just simply state you can now archive pages.

mglaman’s picture

Content Moderation will not be used, per Lauriii

But will whatever we do here prevent integration with it by others?

lauriii’s picture

I'm agnostic to whether we should use Content Moderation or not. I believe that in future, Content Moderation and Workspaces will converge. Integrating Canvas with Workspaces is a high priority meaning it's something we'd do likely some time in 2026. Because of that, we should implement this in a way that 1) minimizes any friction for us to integrate with Workspaces 2) keeps implementing this as simple as possible. I don't know how using Content Moderation impacts that.

narendrar’s picture

Status: Needs work » Needs review

Feedback addressed. I've removed the change record link as it's no longer needed. Moving this back to Needs Review.

tedbow’s picture

Assigned: Unassigned » narendrar
Status: Needs review » Needs work

I tried to simplify things. Please review the changes. There is at least 1 comment I didn't get to also

Thanks

narendrar’s picture

Assigned: narendrar » Unassigned
Status: Needs work » Needs review
Related issues: -#3556265: Create ability to unpublish Canvas pages

Moving it again for review.

aneek’s picture

wim leers’s picture

Status: Needs review » Needs work
Issue tags: +Needs update path tests

I did a high-level review: I focused on data integrity/tech debt/long-term thinking.

See MR for details.

I think with an MR of this complexity (deep into the entity system and views, and also affecting our internal HTTP API), it's important that the tests have been thoroughly reviewed.

Naren & Ted: did both of you thoroughly review all of the tests (~50% of this MR AFAICT!)? That's IMHO crucial before merging this, to ensure >1 person understands this 🤝

tedbow’s picture

re #49 I need to re-review the tests still

tedbow’s picture

Assigned: Unassigned » narendrar

@NarendraR, could you address @wimleers' review? Feel free to only address the ones you feel you have clear direction on, and then re-assign them to me to take on the others. Thanks!

narendrar’s picture

Assigned: narendrar » tedbow
Issue tags: -Needs update path tests

Assigned to @tedbow to address remaining feedback.

tedbow’s picture

Assigned: tedbow » narendrar

@NarendraR , thanks for the update test and other changes. Have left a few comments that need to be addressed

It still needs someone to do a review of the changes on the front-end.

narendrar’s picture

Assigned: narendrar » Unassigned
Status: Needs work » Needs review
kunal.sachdev’s picture

StatusFileSize
new99.77 KB

In the middle of testing, I found this case confusing: when I have a published page and I click on Archive page but this change is not yet published, the status is shown as Archive*.

I don't think this makes sense and it is quite confusing. In my opinion, we shouldn't show anything in this case; if the change to archive the page isn't published yet, we shouldn't show any status related to archiving. Attaching a screenshot for reference.

acbramley’s picture

Just chiming in to echo #25, all of our clients need content moderation but we have never used workspaces. I don't see the 2 coverging any time soon personally.

Having a quick skim through the MR a lot of this code would no longer be required if it were using workflow states instead.

mstrelan’s picture

I note in #10 that content moderation was not chosen due to significant complexity. Well now we have two significant complexities that achieve the same thing. Why not standardise one the one that has already existed in core for over a decade?

acbramley’s picture

Further to #57 I'm not sure why CM would be considered a significant complexity in the first place?

In any case, using CM will also give free integration with other already built features such as Scheduled Transitions

catch’s picture

Status: Needs review » Needs work

I don't understand why an extremely complex workaround is being implemented here, just to avoid unpublished (archived) pages being shown alongside unpublished (draft) pages - that's a longstanding issue with every entity type in existence.

At the moment no-one has any unpublished (archived) canvas pages at all because this issue isn't fixed, fixing it isn't going to suddenly flood the UI with them.

Ordering the list by created descending would generally move archived pages to the bottom. Seems like a pretty small temporary UX niggle compared to the complexity added here, and then actual workflow and separation of listings can be added on top.

All of this time would be much better spent actually figuring out workspaces support instead.

On content moderation vs. workspaces, once #3486378: [Plan] Allow for / implement simplified content workflow with workspaces is done (it's a couple of hard issues away at the moment), content moderation will use workspaces under the hood. The back end and UI of workspaces are already completely separated in core so you can use the underlying mechanism without exposing separate workspaces in the interface.

I don't know the specific plans for how canvas is planning to implement with workspaces but it shouldn't end up being an either/or choice as such, unless workspaces implements an essentially hard-coded workflow UI on top. But again - defining that would be more productive than complex CASE THEN ELSE queries and adding views plugins which will eventually need to be deprecated then removed again.

narendrar’s picture

Status: Needs work » Needs review
lauriii’s picture

We've considered Content Moderation, but there are two challenges. First, to integrate it with Canvas we'd need to support only a subset of its functionality, so it would end up being an opinionated experience regardless. Second, adopting Content Moderation now could make our eventual Workspaces integration harder.

I've talked to users who have Content Moderation enabled and would benefit from Workspaces-style staging, but the UX friction and complexity of Workspaces has prevented adoption. That's a challenge Canvas will need to solve by providing a simplified/opinionated experience on top of the underlying system.

Even when #3486378: [Plan] Allow for / implement simplified content workflow with workspaces lands, Canvas will still need its own opinionated layer to deliver the experience we're targeting.

We're currently planning to work on Workspaces integration in Q2. In the meantime, if the team is willing to maintain this implementation and handle a potential deprecation path when Workspaces support arrives, I'm comfortable with the approach in this MR. The distinction between archived and draft states improves consistency between the Drupal admin UI and Canvas, which is a UX improvement even if it's not strictly required.

kunal.sachdev’s picture

Status: Needs review » Reviewed & tested by the community

Looking good!!

catch’s picture

Status: Reviewed & tested by the community » Needs work

Left comments on the MR about the upgrade path for the view.

Would also like to state here for the record that I don't think this workaround should be committed at all. It's 3000+ lines of code including new views filters etc. which will then have to be undone again when the actual solution is committed, including even more views updates, deprecations etc. the time would have been better spent working on the actual feature, instead of a simulacrum of the feature.

tedbow’s picture

StatusFileSize
new49.02 KB

Here is an idea to limit the scope of this issue.

After thinking about this, I think one thing to consider is that the view at admin/content/pages is probably not that useful now.

The most confusing thing about this view is that all the "draft" pages currently show up as "Untitled Page". This is regardless of what the title of these pages is in the Canvas UI. As soon as you update a page's title in the Canvas UI, everywhere in the Canvas UI you will see the new title. You see it in at least 4 places: 1) the page data form, 2) the preview, 3) the navigation dropdown, and 4) unpublished changes.

So if you have 10 "draft" pages that you have updated the title for, you will see those titles in all those places.

At admin/content/pages, you will see all those 10 pages as "Untitled Page". So as far as listing pages and directing you to view or edit the pages, as soon as you have more than 2 pages, this listing is pretty useless. The "Updated" time is also pretty useless because it is only ever going to show you the time it was created. You would just have to click on various "Untitled pages" until you find the one you want.

The reason this happens is that after you first create a page, until you publish a page, we never do a real entity save. So the draft entities, as far as the view at admin/content/pages is concerned, never change until you publish, and they are no longer drafts.

The reason for this is that we have a separate autosave mechanism, but of course a user who doesn't know how Canvas works would have no idea about this and really should not have to.

The thing is, as far as I know, nobody has filed an issue for this. My guess is because everyone is just using the Canvas UI navigation to find their draft pages. If people were actually using admin/content/pages to find and manage draft pages, we would have heard complaints by now.

While this issue as is would at least show you the "draft" and "archived" status, it would not solve the "untitled page" problem on admin/content/pages. We could try to solve that, regardless of what we do here, but given we are going to add Workspaces integration AND nobody has complained, I am not sure it is worth it.

So, here are my thoughts of what we could do:

  1. Remove all changes related to Views. Just assume people will use the Canvas UI for draft pages
  2. Update the view we ship in /config/optional to filter out unpublished pages titled "Untitled page". This would effectively remove all drafts from the view because draft pages never change their title in a real entity save. Technically it could also filter out an "archived" page that is titled "Untitled page" but that seems unlikely.

    This would leave admin/content/pages showing Published and "archived" pages labeled as "Unpublished"(we can easily fix that, see next point)

    We could even output a message on this page pointing to /canvas with the text "See your draft page in the Canvas editor"

    This would not help existing sites but we could put a mention in the release notes about how to manually import the view config/optional if they want to.

  3. In addition to the point above, we change the view at admin/content/pages to show "Archived" instead of unpublished. This would be a one line change in config/optional/views.view.canvas_pages.yml since we already use a custom format to display "Unpublished"

If we did this then for new sites, admin/content/pages would show only "Published" and "Archived" pages with the same labels they have in the Canvas UI.

mstrelan’s picture

The thing is, as far as I know, nobody has filed an issue for this. My guess is because everyone is just using the Canvas UI navigation to find their draft pages. If people were actually using admin/content/pages to find and manage draft pages, we would have heard complaints by now.

I never use the Canvas UI navigation to switch pages, I find that concept confusing and don't understand how it will scale well to hundreds of pages. Nevertheless, I have indeed noticed "Untitled page" show in the Pages view, but I've only ever had one draft at a time so far.

tedbow’s picture

I pushed up an MR for the suggestion in #64. It does remove some extra complexity around views but this is still a lot of code. Still I would say archiving is big feature so maybe ok.

Re #63

It's 3000+ lines of code including new views filters etc. which will then have to be undone again when the actual solution is committed, including even more views updates, deprecations etc. the time would have been better spent working on the actual feature, instead of a simulacrum of the feature.

This does remove the views plugins, so won't have worry about deprecating them.

Also I was surprised the original MR was 3000 lines. I saw that we had needless text fixture that was a copy of unupdated View. I removed that which removed almost 1000 lines

tedbow’s picture

re #66
@mstrelan I agree that current navigation is not going to scale to hundreds of pages. But we could improve the in UI navigation to avoid someone having to leave the Canvas editor to find pages

amateescu’s picture

I was wondering if the Trash module was considered for this functionality, but found nothing after a quick search.

It's already included in Drupal CMS, and provides the archiving capability requested here. Trashed pages would be hidden from the default view provided by Canvas, they would be available under /admin/content/trash/canvas_page, and they can be restored (or purged) when needed.

penyaskito’s picture

IMHO archiving + trash are different concepts, specially since trash allows to permanently delete content based on when it was deleted. I might want to archive for historic reasons, so no published revision of this content is visible to the public, but I have to keep the content around for whatever reason, including but not limited to legal reasons.

AFAIK there's no incompatibility with the trash module, but if there is it should be a different issue.

tedbow’s picture

Title: Create ability to archive Canvas pages » 🤦‍♂️ ability to archive Canvas pages

🤦‍♂️ @amateescu I think that is a pretty good idea.

We could just recommend the module now and it would work with the View already at "admin/content/pages."

Also, I checked and our "delete" link inside the Canvas Editor works too for sending items to the trash. This removes them from the Canvas UI, but you can still see them at `admin/content/trash/canvas_page`. If you take them out of the trash, they will then show up again in the Canvas UI.

Honestly, this might be better UX than we have now in either of our MRs right now. Currently, if you "archive" a page, it is still present in the Canvas UI. So, if you used Canvas for a number of years with our approaches in the MR, the Canvas UI navigation would start to fill up with archived pages, which is probably not what you would want.

So out of the box, Trash would just work. We could, though, make it better with a couple of small tweaks on our side.

  1. Add a 🗑️ (trash icon) somewhere in our UI that just takes you to admin/content/trash/canvas_page, where you could take pages out of the trash and/or delete them permanently.
  2. Tweak for draft pages. Right now, if I have a "my draft page" that has never been published and I use "delete" in the canvas UI with trash enabled, it does go into trash, but it is put in as "Untitled page" because that is the last real saved version. See the reason for this in #64.

    We could tweak \Drupal\canvas\Controller\ApiContentControllers::delete to fix this. Not sure exactly yet, but it seems solvable.

That could be enough, especially since Drupal CMS uses this, so presumably, it would make the experience similar for pages and nodes.

At a later date, we could make a fancy trash UI inside the editor.

tedbow’s picture

Title: 🤦‍♂️ ability to archive Canvas pages » Create ability to archive Canvas pages

whoops didn't mean to change the title 🤦‍♂️🤦‍♂️

lauriii’s picture

I agree with @penyaskito that Trash is a different concept from archiving content, and I don't believe the Trash module is a 1:1 replacement for the functionality proposed here.

The most important distinction is how this interacts with the Canvas publishing workflow. The benefit of the Archive functionality in this MR is that it treats "unpublishing" as a state change that can be staged. This allows a user to "take down" pages as part of a larger set of changes. For example, if a user wants to launch a page and remove the old one simultaneously, they can draft the new page and set the old one to "archive" status, then publish both changes in a single action.

Another difference is that you cannot edit pages in the trash. Trash is almost like a "soft delete" state and the content is pending removal and would have to be restored to be modified. There's also currently no way to publish the content again after restoring from trash. However, if we implemented this issue, the page from trash would become archived and could now be published.

mstrelan’s picture

The benefit of the Archive functionality in this MR is that it treats "unpublishing" as a state change that can be staged. This allows a user to "take down" pages as part of a larger set of changes.

This problem is not unique to canvas, we should solve this in workspaces / content moderation. Currently we use the scheduled_transitions module for this purpose but each entity is scheduled independently. Batching the changes like in workspaces would be better.

tedbow’s picture

Assigned: Unassigned » lauriii

@lauriii, I think the decision is up to you.

The case for Trash

This issue started from a Slack thread which didn't give reasons for why they wanted to unpublish, so as far as what users asked for, I am not sure the distinction between what the Trash module gives us is meaningful. I agree the UX is better in the two merge requests that are open. However, I wonder whether it is worth the technical debt, considering we plan to work on workspaces in a few months.

In #14 regarding adding the "publication status" field, you said:

I assume we want to prioritize easy migration to Workspaces over whatever that is but it might be good to document the downsides explicitly.

We are planning to work on Workspaces support in 2026 so we should avoid making the migration to Workspaces significantly more complicated than it's today.

I think the two merge requests would definitely make the migration to Workspaces significantly more complicated than it is today, compared to using the Trash module.

Re #73

There's also currently no way to publish the content again after restoring from trash.

I checked this. If you restore a page that was published, it will immediately be published. A restored draft page will be a draft.

I think the remaining question is, when we implement this, will it be a hard requirement? We could say Trash is good enough for now because we don't want to implement what we will get from Workspaces eventually.

Another advantage for Trash is that Drupal CMS will use it, so this will give the user a better experience for consistency between other entities and pages.

Sunk costs?

If @amateescu had suggested Trash the day after we made this issue, would we have put all this effort into our custom solution knowing Workspaces work was coming up?

The existing MRs

If we are going to use one of the MRs, I suggest we do https://git.drupalcode.org/project/canvas/-/merge_requests/543 because it does not have Views plugins and does not update the view on existing sites. I think the UX is probably better or as good.

pameeela’s picture

amateescu’s picture

For example, if a user wants to launch a page and remove the old one simultaneously, they can draft the new page and set the old one to "archive" status, then publish both changes in a single action.

That's exactly the experience that Workspaces + Trash gives you out of the box, the ability to stage content (soft-)deletions.

This problem is not unique to canvas, we should solve this in workspaces / content moderation.

It's already solved by Workspaces / Content Moderation :) Unless that "in" was meant as "with", to which I wholeheartedly agree!

We could say Trash is good enough for now because we don't want to implement what we will get from Workspaces eventually.

I also agree with @tedbow's analysis: the easiest way forward at the moment is to direct users to use Trash for a quick way to enable a "pseudo-archiving" feature, while the future work of integrating Canvas with WS / CM would provide Archive as an actual content workflow state. And even after that, archiving and soft-deleting pages are complementary features, you can both soft-delete and archive different pages in a set of content changes.

mstrelan’s picture

It's already solved by Workspaces / Content Moderation :) Unless that "in" was meant as "with", to which I wholeheartedly agree!

Even better! I haven't used workspaces so wasn't sure.

If that's the goal for the future, and this issue is just trying to resolve a UX issue that previously published pages appear as drafts, why not just temporarily introduce a computed field that stores if an unpublished entity has been published before? If yes then it's treated as archived, if no then it's draft.

Edit: wait, do we have revisions? Maybe this is not possible.

catch’s picture

If @amateescu had suggested Trash the day after we made this issue, would we have put all this effort into our custom solution

#3530375: Update deletion confirmation message for Canvas entities for compatibility with trash module was opened by @pameela 9 months ago in the Canvas queue. There are eight comments on it, this issue is coming up to 80.

mherchel’s picture

@lauriii asked me to chime in.

After reading through all the trashy comments, it seems that'd solve the 80% use case.

The remaining use cases (being able to edit unpublished content etc) are still important, but since I'm not sure of the state of the MRs, I can't really say what's preferable.

pameeela’s picture

I think Trash is a decent temporary solution to this, not perfect but preferable to introducing a bunch of complexity that will be problematic later.

tedbow’s picture

Issue summary: View changes

In #61 @laurii states

We're currently planning to work on Workspaces integration in Q2. In the meantime, if the team is willing to maintain this implementation and handle a potential deprecation path when Workspaces support arrives, I'm comfortable with the approach in this MR.

Above, in the "Transition to Workspaces" section, we have

Workspaces would be a hard requirement. You couldn't choose not to use Workspaces and instead use Canvas and the solution for draft/archive we come up with here. This is just a stop-gap

But in #63 I stated

I think the remaining question is, when we implement this, will it be a hard requirement?

I have confirmed with @lauriii directly that this decision has not been made yet.
In relation to the current work, he told me

I'm not sure if that's relevant though because doesn't Workspaces still use the published flag for marking unpublished content? Workspaces is just to bundle forward revisions (and drafts) so that they can be published at once.

Thinking more about this, I think it is very relevant.

What makes the whole situation complex without Workspaces is that we have a boolean field status but we need to store 3 states: draft, published, and archived. Since Workspaces would allow us to create new published pages (and nodes etc.) without those pages appearing on the live site, we could just create those pages as published in the workspace. Therefore, we would no longer need status === 0 for both draft and archive pages. AFAICT, there would be no need for a "draft" of an individual page inside of a Workspace because the whole Workspace would be a draft.

Possibilities for Workspaces integration

Workspaces is optional

  1. Draft, published, and archived still have to work if you choose not to use Workspaces.
  2. This means whatever we build here is not a temporary solution.
  3. We probably don't have to worry about deprecations.
  4. To not make the logic of the module very complex, we probably want certain things to work the same whether you have workspaces enabled or not. For instance, we might want to use some sort of "draft" state inside workspaces also for individual pages.

Workspaces is NOT optional

  1. We can create published pages directly in the Canvas editor.
  2. What we create is a temporary solution.
  3. We probably DO have to worry about deprecations for whatever we create here.
tedbow’s picture

Assigned: lauriii » tedbow

Ok. I tried to take a look to see if we could make Merge request 543 work.

I found out that the major change to \Drupal\canvas\AutoSave\AutoSaveManager::entityIsConsideredNew we can't actually remove 🎉.

The existing behavior in 1.x where we base this off the default title still works. There is no way in the UI to change the title of a saved page before publishing. That also is how 1.x works.

Sorry to go back and forth. I am beginning to think this method might actually work. I don't think we have to think of this as 3 states anymore. We really just have published and unpublished. We just have a special UI workflow around "new" pages which we are calling drafts, but this is not a complete separate state like you would have in Content Moderation.

One addition that will make this work better is if we prevent publishing a page with the default title, "Untitled page". I think this likely could cause a problem in 1.x too; we just never looked into it. \Drupal\canvas\AutoSave\AutoSaveManager::entityIsConsideredNew would tell you a published page is new in 1.x if you never changed the title. So we could do this in a separate issue if we don't want the existing MR to be more complex, and take on existing problems.

I will look at this again tomorrow, but I am hopeful.

Also, I don't think this method would stop us from also making the module compatible with Trash. See #3572636: Trash module compatiblity: Can't publish restored page with existing auto-save, which is a very small fix and would not conflict with this.

catch’s picture

Workspaces is optional
Draft, published, and archived still have to work if you choose not to use Workspaces.

If you always want to track published to unpublished as 'archived' regardless of whether any kind of moderation solution is in place, that's what the workflow module in core is for.

tedbow’s picture

Title: Create ability to archive Canvas pages » Create ability to unpublish Canvas pages
Assigned: tedbow » Unassigned
Status: Needs work » Needs review
Related issues: +#3572851: Dynamically use current auto-save title instead of "untitled page" in views, delete dialog, and elsewhere in the UI, +#3572636: Trash module compatiblity: Can't publish restored page with existing auto-save

re #83. I now definitely think we should go with Merge request 543

Caveat, I changed all mentions of "archive" to "unpublish" in the MR. It is more accurate to what we are doing. The rest of my reasoning assumes this.

What 543 Does

Major Backend changes

  1. Introduces a new route canvas.api.content.patch which calls the new \Drupal\canvas\Controller\ApiContentControllers::patch

    This updates the auto-save entry for a content entity (limited to pages for now). Limited to allowing changing $entity_type->getKey('published') for now. It also disallows unpublishing the homepage.

  2. Updates \Drupal\canvas\Controller\ApiAutoSaveController::post to not publish all content entities it is processing.

    This is needed because of point 1. Before this when saving any entity there it would be upublished.

  3. Changes \Drupal\canvas\Hook\PageHooks::preventHomepageDeletion to preventHomepageModification
    to prevent unpublishing the homepage.

    Note to self: check if the logic here is correct

  4. Adds unpublish and publish operations to \Drupal\canvas\Controller\ApiContentControllers::getEntityOperations

Front-end changes

Handles new unpublish/publish links.
Displays an "unpublished" status for pages that we don't consider "new" and are unpublished. "New" is not a new concept in this MR and is used in 1.x for displaying "draft".
Displays "🕦Unpublish" for pages that are currently published but where the user has clicked "unpublish".

"new" meaning

We are not changing \Drupal\canvas\AutoSave\AutoSaveManager::entityIsConsideredNew. This existing

"New" pages are ones that still have the original title; basically, they have never been saved (besides auto-save) since they were created in the UI. There are built-in assumptions in Canvas that all pages will be created within the Canvas UI and therefore we control the title. This also is not new in this MR.

How "new" relates to this MR: We don't show the "publish" action link that will update the auto-save on "new" pages even though they are unpublished. This is because all "new" pages will be updated automatically when submitting the "Review X changes" form.

Workspaces integration

Since my comment in #83, I reverted the change in \Drupal\canvas\AutoSave\AutoSaveManager::entityIsConsideredNew that gave me concern in how it will affect eventual Workspace integration. We may or may not need to update this based on whether workspaces are optional, but that doesn't change with this MR.

Having gone over and described the changes in this MR without needing to introduce an "archive" state, I think we are just introducing an overlooked ability to unpublish pages, and this shouldn't affect Workspaces.

The AutoSaveManager may need to change when we integrate with Workspaces, but it will likely not go away. It is used for allowing quick saves and saving entity states even if they do not completely validate (though this is caught before actually saving them).

admin/content/pages View concerns

This does not solve the problems I have listed in #63, but it doesn't make them worse. I have opened up #3572851: Dynamically use current auto-save title instead of "untitled page" in views, delete dialog, and elsewhere in the UI that should help with this View. It should also help with the Trash module or other places we list Pages in Views.

Trash Module

These changes should not be incompatible with the Trash module. I think it is important that the UI changes in this MR now say "Unpublish" vs "archive" because unpublish is the existing concept in Drupal, and therefore the users don't have to think about what the difference is between archive and trash, and archive and unpublish.

I opened #3572636: Trash module compatiblity: Can't publish restored page with existing auto-save, which is an issue regardless of this one. I mentioned that #3572851: Dynamically use current auto-save title instead of "untitled page" in views, delete dialog, and elsewhere in the UI helps with Trash Views but also will help dialogs in Trash to not show "untitled page" for all "new" pages.

tedbow’s picture

Created follow-up #3573408: Prevent unpublishing a Canvas Page if it is the home page,

I explain in that issue why I think it can be a follow-up.

  1. The current MR prevents this from happening if you use the Canvas UI to set the homepage
  2. Even when setting the homepage outside of Canvas UI you have to do the steps in a particular, unlikely order to reproduce the problem
tedbow’s picture

Issue summary: View changes

tedbow changed the visibility of the branch 3556265-create-ability-to to hidden.

tedbow’s picture

I chatted with @lauriii and he is ok with the latest approach. I did a self review and now someone else should review.

kunal.sachdev’s picture

Status: Needs review » Reviewed & tested by the community

Looks good!!

  • tedbow committed 24183129 on 1.x
    feat: #3556265 Create ability to unpublish Canvas pages
    
    By: mherchel
    By...
tedbow’s picture

Status: Reviewed & tested by the community » Fixed

Thanks for the work and input everyone! I think it was good we decided to scale back the scope of this issue

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

penyaskito’s picture

penyaskito’s picture

Issue tags: +openapi

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

mlncn’s picture

Does not appear this needed fix is on the 1.3.x branch yet. Is the relationship to this 1.x dev branch to the three (!) supported branches (1.2.x, 1.3.x, and 1.4.x) documented anywhere, like if/when fixes get (cherry-picked?) over to the release branches?