Skip to content

🗃️ Make archiving tables/applications a personal option - #2476

Open
AndyScherzinger wants to merge 45 commits into
mainfrom
feat/2125/personal-archiving-option
Open

🗃️ Make archiving tables/applications a personal option#2476
AndyScherzinger wants to merge 45 commits into
mainfrom
feat/2125/personal-archiving-option

Conversation

@AndyScherzinger

@AndyScherzinger AndyScherzinger commented Apr 12, 2026

Copy link
Copy Markdown
Member

🖼️ Screenshots

Archived categories Archive app Archive table Unarchive app Unarchive table
2026-04-12 15_33_25-Tables - Nextcloud — Mozilla Firefox 2026-04-12 15_34_51-Tables - Nextcloud — Mozilla Firefox 2026-04-12 15_34_35-Tables - Nextcloud — Mozilla Firefox 2026-04-12 15_34_05-Tables - Nextcloud — Mozilla Firefox 2026-04-12 15_33_50-Tables - Nextcloud — Mozilla Firefox

The original ticket #2125 defined tables and views, while this PR tacked tables and apps. Given a clear view on how archived views should get displayed this could be added as a follow-up PR to not make this PR grow in review-size and complexity.


Summary

  • This change replaces the existing single-user global archived flag on tables with a two-layer per-user model: an owner-level flag on the entity and a new tables_archive_user table that stores individual user overrides, so archive state is resolved per-user at read time. When an owner archives a table or application, it is automatically treated as archived for every user with access, but any of those users may override this for themselves; an owner unarchiving resets the state for everyone by clearing all per-user overrides.
  • The existing tables_tables.archived column is repurposed from a simple global toggle into the owner-level signal, and the database migration converts all currently-archived tables into owner-archived entries to preserve existing data without disruption.
  • Application (context) archiving is introduced as a completely new capability end-to-end: covering database schema, entity, service, API endpoints, routes, and frontend navigation - while table archiving is substantially reworked from a simple owner toggle into the full per-user propagation system.
  • A new ArchiveService centralizes all business logic for both resource types, four new API endpoints are split across ApiTablesController and ContextController following existing naming conventions, and the frontend gains archive/unarchive actions in both navigation item menus alongside a new collapsible "Archived applications" section mirroring the already-existing "Archived tables" section.

⚠️ Beware

The database migration only covers the owner and direct user shares, not groups, etc. since they can't be resolved directly via SQL (think teams/groups). So in these cases archived tables would resurface until manually archived by the respective user. Also there is no cleanup for teams/groups, if a user is taken out of a team, the archive meta-info would not get deleted. Only simple but confusing way around it would be to prevent archiving/unarchiving for tables/apps where a user is neither the owner nor a user-share receiver. Expensive alternative would be a housekeeping job checking all these archive bits periodically, but that also sounds expensive with little use. Else we would need to consume a team/group changes event and check if some cleanup is needed. WDYT?

🏁 Checklist

  • ⛑️ Tests (unit and/or integration) are included or not needed
  • 🔙 Backport requests are created or not needed: /backport to stableX.X
  • 📅 Milestone is set
  • 🌸 PR title is meaningful (if it should be in the changelog: is it meaningful to users?)

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI

@AndyScherzinger AndyScherzinger added enhancement New feature or request 2. developing Work in progress labels Apr 12, 2026
@AndyScherzinger AndyScherzinger changed the title Personal archiving option 🗃️ Make archiving tables/applications personal option Apr 12, 2026
@AndyScherzinger
AndyScherzinger force-pushed the feat/2125/personal-archiving-option branch 5 times, most recently from 008f4f3 to 0e2bbdc Compare April 12, 2026 15:34
@AndyScherzinger AndyScherzinger changed the title 🗃️ Make archiving tables/applications personal option 🗃️ Make archiving tables/applications a personal option Apr 12, 2026
@AndyScherzinger
AndyScherzinger force-pushed the feat/2125/personal-archiving-option branch 2 times, most recently from d3fce3e to fd235a6 Compare April 12, 2026 18:56
@AndyScherzinger
AndyScherzinger marked this pull request as ready for review April 12, 2026 19:36
@AndyScherzinger AndyScherzinger added 3. to review Waiting for reviews and removed 2. developing Work in progress labels Apr 12, 2026

@jancborchardt jancborchardt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks generally nice, 2 details

  • I’d say to use outline icons instead of filled for both cases, archiving and unarchiving
  • In the cases of "Archive app" and "Unarchive app", the sorting of actions in the menu is off. We always recommend the destructive action (Delete) to be the very last, and then the next most destructive like Archive could be directly above it.

@jancborchardt jancborchardt moved this to 🏗️ At engineering in 🖍 Design team Apr 28, 2026
@AndyScherzinger
AndyScherzinger force-pushed the feat/2125/personal-archiving-option branch from fd235a6 to ba16212 Compare April 28, 2026 13:13
Comment thread lib/Db/UserArchive.php Outdated
Comment thread lib/Db/UserArchiveMapper.php Outdated
@AndyScherzinger
AndyScherzinger force-pushed the feat/2125/personal-archiving-option branch 4 times, most recently from 840a1ab to 086aad3 Compare April 28, 2026 20:06
AndyScherzinger and others added 27 commits September 5, 2026 08:30
…ic also checks for it anyways

The context endpoints lack the #[RequirePermission] attribute that the table endpoints have.
However the mapper enforces the same constraint, but is less explicit for future maintenance and for consistency with other endpoints

AI-assistant: Claude Code v2.1.119 (Claude Sonnet 4.6)
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Setting the parameter inside the loop preventing rebuilding the query representation if this really loops a lot.

Co-authored-by: Christoph Wurst <1374172+ChristophWurst@users.noreply.github.com>
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
…ust getXXX

Co-authored-by: Christoph Wurst <1374172+ChristophWurst@users.noreply.github.com>
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
AI-assistant: Claude Code 2.1.119 (Claude Sonnet 4.6)
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Signed-off-by: Enjeck C. <patrathewhiz@gmail.com>
The middleware only recognizes contexts when the type arrives as the
string 'context'; an integer NODE_TYPE_CONTEXT is rejected as an
invalid node type before the controller runs.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
TableService::find() returns the entity-level archived flag; use
getTableForUser() so GET /tables/{id} reflects the requesting user's
personal archive override.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Resolve the per-user archive state for nav-bar contexts and skip
archived ones so archiving an application also declutters the global
navigation.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
The migration was created in April under version 2020 and would sort
before migrations already released on main (2200-2210); rename it to
2400 with a current date stamp.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Replace hardcoded node type 0 with Application::NODE_TYPE_TABLE.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Prepare the share lookup and the archive check/insert queries a single
time and rebind their parameters per iteration instead of recreating
query builders for every archived table and recipient.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Replace the read-modify-write cycle with IDBConnection::setValues(),
which is portable across all supported databases and closes the race
between concurrent requests hitting the unique index.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
The method had no production caller; tests now verify row counts with
a direct count query instead.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Extract the shared load-overrides-and-merge logic of the table and
context enrichment methods into one private helper.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
…r archive test

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
…ler archive test

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Return early when the archived table or application is no longer in
the local store instead of dereferencing undefined.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Vue 3 proxy reactivity tracks array index assignment, so the splice
swap is unnecessary; drop the unrelated change.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Align the archived-applications counter with the archived-tables one,
which already uses the count prop introduced with nextcloud/vue 9.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Wrap the ArchiveService calls in the same OCP\DB\Exception-to-
InternalError conversion TableService uses so tables and contexts
report archive failures consistently.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
…access

An ownership transfer preserved the outgoing owner's archive view with
a personal override even when they lose access to the node entirely,
leaving orphaned rows in tables_archive_user. Remove the override
inside the transfer transaction when the outgoing owner can no longer
read the table or access the context.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
tables_shares stores node_type as a string ('table'), so filtering with
the integer NODE_TYPE_TABLE constant never matched the direct user
shares (or, on MySQL loose casting, matched every node type). Recipient
archive records were therefore not migrated correctly.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Add UserArchiveMapper::findUserIdsForNode() and deleteAllForUser(),
plus ShareMapper::findNodesByReceiver(), as building blocks for
cleaning up per-user archive overrides when access is lost.

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
Follow the pattern established by server group shares and the circles
app: shares stay group/circle-targeted with dynamic access resolution,
and only the materialized per-user state is removed once membership or
sharing ends. The new ArchiveCleanupService drops a user's archive
override after verifying they no longer have access to the node, and is
triggered when

- a user is removed from a group (UserRemovedEvent),
- a member is removed from a circle (CircleMemberRemovedEvent,
  including inherited members of a removed nested circle),
- a group is deleted or a circle destroyed (the existing receiver
  cleanup now also purges stale overrides on the affected nodes),
- a share is deleted (single receiver check for user shares, full node
  purge for group and circle shares),
- a user account is deleted (all overrides of that user are removed).

Assisted-by: ClaudeCode:claude-fable-5
Signed-off-by: Andy Scherzinger <info@andy-scherzinger.de>
@AndyScherzinger
AndyScherzinger force-pushed the feat/2125/personal-archiving-option branch from 24d6afb to 802890d Compare September 5, 2026 06:30
@AndyScherzinger AndyScherzinger added 3. to review Waiting for reviews and removed 2. developing Work in progress labels Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3. to review Waiting for reviews AI assisted enhancement New feature or request

Projects

Status: 🏗️ At engineering

Development

Successfully merging this pull request may close these issues.

5 participants