Skip to content

feat(pages): let widgets sit in the tabs of a tabs widget - #3212

Open
rubenvdlinde wants to merge 2 commits into
feat/noid/start-page-autofillfrom
feat/noid/widget-tabs-model
Open

rubenvdlinde wants to merge 2 commits into
feat/noid/start-page-autofillfrom
feat/noid/widget-tabs-model

Conversation

@rubenvdlinde

@rubenvdlinde rubenvdlinde commented Nov 3, 2026 •

Copy link
Copy Markdown

Part of #3099, on top of #3211 (start page autofill). This is the model and the server side of a tabs widget: a widget whose tabs each hold widgets of their own. The UI comes in #3217.

@enjeck, this is the only pull request in the stack that changes the model of #3100: tables_contexts_widget gets parent_id and parent_tab. Do you agree with that model for widgets in tabs? If you'd rather keep #3100 as it is, this pull request and #3217 can be dropped without affecting the others.

Size: 333 lines: 229 code, 83 tests and 21 generated OpenAPI (in a separate commit).

What changes

  • A tabs widget type on the server (GridWidgetTypes): its tabs setting names the tabs, one per line, at most 20.
  • The model: tables_contexts_widget gets parent_id (the tabs widget) and parent_tab (the position of the tab), both nullable, in their own migration Version2500Date20261030010000. The app version goes to 2.5.0-dev.2, one dev step from feat(pages): fill the start page of an application with widgets #3211.
  • Saving a page takes parentIndex (the position of the tabs widget in widgets) and parentTab for a widget in a tab; page responses carry parentId and parentTab. The server checks that the parent is a tabs widget of the same page that sits on the page itself, so tabs go one level deep and a tabs widget never sits in a tab, and that the tab exists. The cap of 100 widgets counts the widgets in tabs too.
  • Moving a user's data keeps widgets in their tabs; a widget whose tabs widget or tab didn't come along is skipped with a warning.

Until #3217, the frontend from #3214 on doesn't offer the tabs type, because it only offers types it can show.

How it was tested

On Nextcloud 34 with PostgreSQL:

  • PageServiceTest: a widget in a tab listed before its tabs widget is saved with its parent; 400 for a tab without tabs widget, a parent that isn't a tabs widget, a parentIndex that isn't a number, tabs in tabs, an unknown tab and more than 20 tabs (one data provider)
  • WidgetServiceTest (database): putting a widget in a tab and taking it out again
  • ContextPagesRoundTripTest (database): widgets in tabs survive a user data import, an orphan is skipped
  • the full unit suite on PostgreSQL and SQLite: 571 tests each
  • composer run rector:check, php -l, composer cs:check and psalm, all clean
  • no frontend changes besides the regenerated types

🖼️ Screenshots

No UI change; #3217 has the screenshots.

🏁 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

A tabs widget names its tabs one per line. A widget in one of its tabs
stores the tabs widget in parent_id and the tab in parent_tab on
tables_contexts_widget, and a page save names them as parentIndex and
parentTab. The server checks that the parent is a tabs widget of the
same page that sits on the page itself, so tabs go one level deep, that
the tab exists, and the existing caps. An import keeps widgets in their
tabs and skips a widget whose tabs widget or tab did not come along.

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Ruben van der Linde <ruben@conduction.nl>
Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Ruben van der Linde <ruben@conduction.nl>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant