@conduction/nextcloud-vue 2.56.0-beta.30 → 2.56.0-beta.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +3 -3
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +80 -4
  5. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +95 -91
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +85 -6
  9. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +1 -1
  11. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +4 -1
  13. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  14. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  15. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +1 -1
  16. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  17. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +2 -2
  19. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +40 -3
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +19 -11
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +43 -0
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +1 -1
  30. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  31. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +1 -1
  32. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -1
  33. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -1
  34. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js +7 -7
  35. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js.map +1 -1
  36. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue3.js.map +1 -1
  37. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +125 -0
  38. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  40. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +3 -2
  41. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  42. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  43. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +302 -4
  44. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  45. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +417 -244
  46. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  47. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +101 -1
  48. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  49. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +141 -1
  50. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  51. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +60 -0
  52. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  53. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +74 -49
  54. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  55. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +29 -1
  56. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  57. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +48 -4
  58. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  59. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +7 -7
  60. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  61. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  62. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +1 -1
  63. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -1
  64. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +1 -1
  65. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  66. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  67. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +63 -1
  68. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  69. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +8 -1
  70. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  71. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +14 -0
  72. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
  73. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js +5 -1
  74. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
  75. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +13 -6
  76. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  77. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  78. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +84 -1
  79. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  80. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +85 -41
  81. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  82. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +7 -2
  83. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  84. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  85. package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
  86. package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js.map +1 -1
  87. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +197 -6
  88. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  89. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +94 -50
  90. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  91. package/dist/esm/composables/useListView.js +4 -4
  92. package/dist/esm/composables/useListView.js.map +1 -1
  93. package/dist/esm/index.js +10 -1
  94. package/dist/esm/index.js.map +1 -1
  95. package/dist/esm/l10n/en.json.js +8 -1
  96. package/dist/esm/l10n/en.json.js.map +1 -1
  97. package/dist/esm/l10n/nl.json.js +54 -1
  98. package/dist/esm/l10n/nl.json.js.map +1 -1
  99. package/dist/esm/nextcloud-vue.css +198 -8
  100. package/dist/esm/utils/facets.js +75 -3
  101. package/dist/esm/utils/facets.js.map +1 -1
  102. package/dist/esm/utils/fillTemplate.js +7 -1
  103. package/dist/esm/utils/fillTemplate.js.map +1 -1
  104. package/dist/esm/utils/pageControls.js +81 -0
  105. package/dist/esm/utils/pageControls.js.map +1 -0
  106. package/dist/esm/utils/personalLenses.js +40 -3
  107. package/dist/esm/utils/personalLenses.js.map +1 -1
  108. package/dist/esm/utils/resolveFilterTokens.js +1 -1
  109. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  110. package/dist/esm/utils/resolveManifestTokens.js +2 -0
  111. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  112. package/dist/esm/utils/sentinelTokens.js +19 -1
  113. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  114. package/dist/esm/utils/userGroupsPeek.js +91 -0
  115. package/dist/esm/utils/userGroupsPeek.js.map +1 -0
  116. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  117. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  118. package/dist/esm/utils/visibleWhen.js +57 -5
  119. package/dist/esm/utils/visibleWhen.js.map +1 -1
  120. package/dist/esm/utils/widgetVisibility.js +4 -57
  121. package/dist/esm/utils/widgetVisibility.js.map +1 -1
  122. package/dist/nextcloud-vue.cjs.js +4309 -2233
  123. package/dist/nextcloud-vue.cjs.js.map +2 -2
  124. package/dist/nextcloud-vue.css +198 -8
  125. package/l10n/en.json +8 -1
  126. package/l10n/nl.json +54 -1
  127. package/package.json +2 -1
  128. package/scripts/check-docs-mdx.mjs +108 -0
  129. package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
  130. package/src/components/CnAppRoot/CnAppRoot.md +4 -2
  131. package/src/components/CnAppRoot/CnAppRoot.vue +151 -59
  132. package/src/components/CnBannerWidget/CnBannerWidget.vue +112 -7
  133. package/src/components/CnCellRenderer/CnCellRenderer.vue +4 -1
  134. package/src/components/CnConversationThread/CnConversationWidget.vue +1 -1
  135. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +2 -2
  136. package/src/components/CnDashboardPage/CnDashboardPage.vue +40 -3
  137. package/src/components/CnDataTable/CnDataTable.vue +19 -11
  138. package/src/components/CnDetailPage/CnDetailPage.vue +44 -1
  139. package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +1 -1
  140. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +7 -7
  141. package/src/components/CnFactsWidget/CnFactsWidget.vue +125 -0
  142. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +3 -2
  143. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +588 -117
  144. package/src/components/CnFilesBrowser/filesBrowser.js +100 -0
  145. package/src/components/CnFilesBrowser/filesBrowserColumns.js +140 -0
  146. package/src/components/CnFormPage/CnFormPage.vue +81 -0
  147. package/src/components/CnIndexPage/CnIndexPage.vue +56 -3
  148. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +7 -7
  149. package/src/components/CnNextStepCard/CnNextStepWidget.vue +1 -1
  150. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
  151. package/src/components/CnObjectSidebar/CnFilesTab.vue +70 -1
  152. package/src/components/CnObjectSidebar/CnNotesTab.vue +16 -0
  153. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +13 -6
  154. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +180 -3
  155. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +7 -2
  156. package/src/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
  157. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +281 -10
  158. package/src/components/index.js +8 -0
  159. package/src/composables/useListView.js +4 -4
  160. package/src/css/look-board-index.css +16 -8
  161. package/src/index.js +8 -0
  162. package/src/schemas/app-manifest-v2.schema.json +93 -9
  163. package/src/utils/facets.js +74 -2
  164. package/src/utils/fillTemplate.js +7 -1
  165. package/src/utils/pageControls.js +78 -0
  166. package/src/utils/personalLenses.js +39 -2
  167. package/src/utils/resolveFilterTokens.js +3 -1
  168. package/src/utils/resolveManifestTokens.js +2 -0
  169. package/src/utils/sentinelTokens.js +19 -1
  170. package/src/utils/userGroupsPeek.js +87 -0
  171. package/src/utils/validateManifestV2.compiled.js +3 -3
  172. package/src/utils/visibleWhen.js +56 -4
  173. package/src/utils/widgetVisibility.js +7 -56
@@ -31,10 +31,12 @@
31
31
 
32
32
  Hosts a per-user `NcAppSettingsDialog` that any descendant can open
33
33
  via the injected `cnOpenUserSettings()` method. CnAppNav binds the
34
- inject to manifest entries with `action: "user-settings"`. Apps
35
- populate the modal by passing `NcAppSettingsSection`s into the
36
- `#user-settings` slot; the slot falls back to a single placeholder
37
- section when no content is supplied.
34
+ inject to manifest entries with `action: "user-settings"`. The modal
35
+ draws the library's default sections (notifications, credentials,
36
+ walkthrough replay, integrations). Apps add their own
37
+ `NcAppSettingsSection`s through `#user-settings-sections` and place,
38
+ order or hide the defaults with the `userSettingsSections` prop. The
39
+ older `#user-settings` slot replaces the defaults instead.
38
40
 
39
41
  A SECOND, distinct `NcAppSettingsDialog` hosts admin-only, app-level
40
42
  (not per-user) settings, driven GENERICALLY by `manifest.adminSettings[]`
@@ -534,18 +536,33 @@
534
536
  User-settings modal. Always mounted so descendants can
535
537
  open it via the `cnOpenUserSettings` inject (CnAppNav
536
538
  wires this to manifest entries with
537
- `action: "user-settings"`). The `#user-settings` slot
538
- hosts NcAppSettingsSection children; the placeholder
539
- fallback below renders when no slot content is supplied.
539
+ `action: "user-settings"`).
540
+
541
+ The dialog draws its sections in the order
542
+ `userSettingsSectionOrder()` returns: `app` (the app's own
543
+ sections from the slots), `notifications`, `credentials`,
544
+ `walkthrough` and `integrations`. A key that is left out is
545
+ not drawn. Two slots carry the app's sections:
546
+ - `#user-settings-sections` adds them: the library's default
547
+ sections stay unless `userSettingsSections` leaves them out.
548
+ - `#user-settings` (the original slot) replaces the defaults,
549
+ as it always did: only the app's content and the
550
+ integrations section are drawn, unless `userSettingsSections`
551
+ names more.
540
552
  -->
541
553
  <NcAppSettingsDialog
542
554
  :open="userSettingsOpen"
543
555
  :showNavigation="true"
544
556
  :name="resolvedUserSettingsTitle"
545
557
  @update:open="userSettingsOpen = $event">
546
- <!-- @slot user-settings Sections rendered inside the host NcAppSettingsDialog. Pass NcAppSettingsSection children. Defaults to the notification-preferences pane when omitted. -->
547
- <slot name="user-settings">
548
- <CnNotificationPreferences v-if="userSettingsOpen" />
558
+ <template v-for="section in userSettingsSectionOrder()" :key="section">
559
+ <template v-if="section === 'app'">
560
+ <!-- @slot user-settings The app's own sections (NcAppSettingsSection children), REPLACING the library's default sections: filling it draws only this content and the integrations section, unless the `userSettingsSections` prop names more. Prefer `#user-settings-sections`. -->
561
+ <slot name="user-settings" />
562
+ <!-- @slot user-settings-sections The app's own sections (NcAppSettingsSection children), ADDED to the library's default sections (notifications, credentials, walkthrough, integrations). Place, order or hide the defaults with the `userSettingsSections` prop. -->
563
+ <slot name="user-settings-sections" />
564
+ </template>
565
+ <CnNotificationPreferences v-else-if="section === 'notifications' && userSettingsOpen" />
549
566
  <!--
550
567
  Credential broker (OpenRegister). Lets the user manage the
551
568
  secrets OR holds on their behalf; apps call external providers
@@ -553,9 +570,9 @@
553
570
  `credentials[]` declarations drive the informational "Apps
554
571
  requesting credentials" list.
555
572
  -->
556
- <NcAppSettingsSection v-if="userSettingsOpen"
573
+ <NcAppSettingsSection v-else-if="section === 'credentials' && userSettingsOpen"
557
574
  id="credentials"
558
- :name="translate('Credentials')">
575
+ :name="libraryCopy('Credentials')">
559
576
  <CnCredentials
560
577
  scope="personal"
561
578
  :appId="appId"
@@ -567,7 +584,7 @@
567
584
  when the manifest declares an enabled tour, so apps
568
585
  without a walkthrough never show an empty section.
569
586
  -->
570
- <NcAppSettingsSection v-if="walkthroughEnabled"
587
+ <NcAppSettingsSection v-else-if="section === 'walkthrough' && walkthroughEnabled"
571
588
  id="cn-walkthrough"
572
589
  :name="restartWalkthroughSectionName">
573
590
  <p class="cn-app-root__walkthrough-hint">
@@ -595,48 +612,47 @@
595
612
  </NcButton>
596
613
  </div>
597
614
  </NcAppSettingsSection>
598
- </slot>
599
- <!--
600
- Integrations (ADR-110) — the links that leave this app for
601
- another one, gathered out of the navigation into one place.
602
-
603
- DELIBERATELY OUTSIDE the #user-settings slot. Six fleet apps
604
- override that slot, and anything placed inside the default
605
- fallback disappears the moment an app supplies its own
606
- content — silently, because an unmatched slot renders nothing
607
- and reports nothing. A contract every app is supposed to
608
- honour cannot live somewhere six apps switch off by accident.
609
-
610
- Renders nothing when the app declares no integration
611
- entries, so apps without any never show an empty section.
612
- -->
613
- <NcAppSettingsSection v-if="userSettingsOpen && integrationEntries.length"
614
- id="cn-integrations"
615
- :name="translate('Integrations')">
616
- <p class="cn-app-root__integrations-hint">
617
- {{ translate('These open in another app.') }}
618
- </p>
619
- <ul class="cn-app-root__integrations">
620
- <li v-for="entry in integrationEntries" :key="entry.id">
621
- <!--
622
- One uniform external-link icon rather than the
623
- entry's own: every row here goes to a different
624
- app, which is the thing worth signalling, and
625
- CnIcon falls back to a question mark for any
626
- name an app has not registered — a broken-looking
627
- glyph on a link that works fine.
628
- -->
629
- <NcButton variant="tertiary"
630
- :href="entry.href"
631
- :aria-label="entry.label">
632
- <template #icon>
633
- <OpenInNew :size="20" />
634
- </template>
635
- {{ entry.label }}
636
- </NcButton>
637
- </li>
638
- </ul>
639
- </NcAppSettingsSection>
615
+ <!--
616
+ Integrations (ADR-110): the links that leave this app for
617
+ another one, gathered out of the navigation into one place.
618
+
619
+ Drawn whether or not the app fills `#user-settings`: six
620
+ fleet apps fill that slot, and a section placed inside its
621
+ fallback disappeared for all of them in silence. Only an
622
+ explicit `userSettingsSections` without `integrations`
623
+ hides it, for an app whose board does not draw it.
624
+
625
+ Renders nothing when the app declares no integration
626
+ entries, so apps without any never show an empty section.
627
+ -->
628
+ <NcAppSettingsSection v-else-if="section === 'integrations' && userSettingsOpen && integrationEntries.length"
629
+ id="cn-integrations"
630
+ :name="translate('Integrations')">
631
+ <p class="cn-app-root__integrations-hint">
632
+ {{ libraryCopy('These open in another app.') }}
633
+ </p>
634
+ <ul class="cn-app-root__integrations">
635
+ <li v-for="entry in integrationEntries" :key="entry.id">
636
+ <!--
637
+ One uniform external-link icon rather than the
638
+ entry's own: every row here goes to a different
639
+ app, which is the thing worth signalling, and
640
+ CnIcon falls back to a question mark for any
641
+ name an app has not registered, a broken-looking
642
+ glyph on a link that works fine.
643
+ -->
644
+ <NcButton variant="tertiary"
645
+ :href="entry.href"
646
+ :aria-label="entry.label">
647
+ <template #icon>
648
+ <OpenInNew :size="20" />
649
+ </template>
650
+ {{ entry.label }}
651
+ </NcButton>
652
+ </li>
653
+ </ul>
654
+ </NcAppSettingsSection>
655
+ </template>
640
656
  </NcAppSettingsDialog>
641
657
 
642
658
  <!--
@@ -781,6 +797,12 @@ const KNOWN_REGISTRY_KINDS = Object.keys(REGISTRY_KIND_REQUIRED_FIELDS)
781
797
  */
782
798
  const OR_STORE_LINK = '/index.php/settings/apps/integration/openregister'
783
799
 
800
+ /**
801
+ * The sections of the user-settings modal, in their default order. `app`
802
+ * stands for the app's own slot content.
803
+ */
804
+ const USER_SETTINGS_SECTIONS = ['app', 'notifications', 'credentials', 'walkthrough', 'integrations']
805
+
784
806
  export default {
785
807
  name: 'CnAppRoot',
786
808
 
@@ -1625,6 +1647,28 @@ export default {
1625
1647
  default: '',
1626
1648
  },
1627
1649
 
1650
+ /**
1651
+ * The sections of the user-settings modal, in the order they are
1652
+ * drawn. Keys: `app` (the content of the `#user-settings-sections`
1653
+ * and `#user-settings` slots), `notifications`, `credentials`,
1654
+ * `walkthrough` and `integrations`. A key that is left out is not
1655
+ * drawn, so `['app', 'credentials', 'walkthrough']` hides the
1656
+ * notifications pane and the integrations section. Unknown keys are
1657
+ * ignored. The app's slot content is always drawn: without `app` in
1658
+ * the list it comes first.
1659
+ *
1660
+ * `null` (the default) keeps what each slot always did: all
1661
+ * defaults after the `#user-settings-sections` content, or only the
1662
+ * `#user-settings` content and the integrations section when an app
1663
+ * fills that older slot.
1664
+ *
1665
+ * @type {Array<string>|null}
1666
+ */
1667
+ userSettingsSections: {
1668
+ type: Array,
1669
+ default: null,
1670
+ },
1671
+
1628
1672
  /**
1629
1673
  * Title rendered at the top of the admin-settings modal
1630
1674
  * (NcAppSettingsDialog `name` prop). Defaults to the
@@ -3017,7 +3061,7 @@ export default {
3017
3061
  },
3018
3062
 
3019
3063
  resolvedUserSettingsTitle() {
3020
- return this.userSettingsTitle || this.translate('User settings')
3064
+ return this.userSettingsTitle || this.libraryCopy('User settings')
3021
3065
  },
3022
3066
 
3023
3067
  /**
@@ -3036,7 +3080,7 @@ export default {
3036
3080
  * @return {string}
3037
3081
  */
3038
3082
  restartWalkthroughSectionName() {
3039
- return this.translate('Walkthrough')
3083
+ return this.libraryCopy('Walkthrough')
3040
3084
  },
3041
3085
 
3042
3086
  /**
@@ -3045,7 +3089,7 @@ export default {
3045
3089
  * @return {string}
3046
3090
  */
3047
3091
  restartWalkthroughHint() {
3048
- return this.translate('Take the guided tour of this app again.')
3092
+ return this.libraryCopy('Take the guided tour of this app again.')
3049
3093
  },
3050
3094
 
3051
3095
  /**
@@ -3054,7 +3098,7 @@ export default {
3054
3098
  * @return {string}
3055
3099
  */
3056
3100
  restartWalkthroughLabel() {
3057
- return this.translate('Restart walkthrough')
3101
+ return this.libraryCopy('Restart walkthrough')
3058
3102
  },
3059
3103
 
3060
3104
  /**
@@ -3389,6 +3433,54 @@ export default {
3389
3433
  return legacy || null
3390
3434
  },
3391
3435
 
3436
+ /**
3437
+ * The keys of the user-settings sections to draw, in order. A method
3438
+ * rather than a computed, because which slots an app fills is read
3439
+ * from `$slots`, which a computed does not track.
3440
+ *
3441
+ * @return {Array<string>} Keys out of `USER_SETTINGS_SECTIONS`.
3442
+ * @spec openspec/changes/user-settings-sections/specs/user-settings-sections/spec.md#requirement-the-user-settings-dialog-keeps-its-default-sections-next-to-the-apps-own
3443
+ */
3444
+ userSettingsSectionOrder() {
3445
+ const replaces = Boolean(this.$slots['user-settings'])
3446
+ const adds = Boolean(this.$slots['user-settings-sections'])
3447
+ let order
3448
+ if (Array.isArray(this.userSettingsSections)) {
3449
+ order = [...new Set(this.userSettingsSections.filter((key) => USER_SETTINGS_SECTIONS.includes(key)))]
3450
+ } else if (replaces) {
3451
+ order = ['app', 'integrations']
3452
+ } else {
3453
+ order = [...USER_SETTINGS_SECTIONS]
3454
+ }
3455
+ if ((replaces || adds) && !order.includes('app')) {
3456
+ order.unshift('app')
3457
+ }
3458
+ return order
3459
+ },
3460
+
3461
+ /**
3462
+ * Copy for a part of the user settings dialog the library draws itself
3463
+ * (its title, the credentials and walkthrough sections). The library's
3464
+ * own catalogue answers first, because the app's catalogue may give
3465
+ * the same English word another meaning: learniq translates
3466
+ * "Credentials" as "Diploma's" for its own records, and the dialog
3467
+ * showed that above the vault pane. Only when the library has no
3468
+ * translation in the user's language does the host `translate` get a
3469
+ * turn, then the text as written.
3470
+ *
3471
+ * @param {string} key The English source string.
3472
+ * @return {string} The translated copy.
3473
+ * @spec openspec/changes/dutch-user-settings-dialog/specs/dutch-user-settings-dialog/spec.md#requirement-the-user-settings-dialog-reads-dutch-for-a-dutch-reader
3474
+ */
3475
+ libraryCopy(key) {
3476
+ const own = t('nextcloud-vue', key)
3477
+ if (typeof own === 'string' && own !== '' && own !== key) {
3478
+ return own
3479
+ }
3480
+ const host = this.translate(key)
3481
+ return typeof host === 'string' && host !== '' ? host : key
3482
+ },
3483
+
3392
3484
  /**
3393
3485
  * Return the translated copy for `key`, or `fallback` when the
3394
3486
  * `translate` prop leaves the key unchanged (its default is the
@@ -7,7 +7,7 @@
7
7
  <section
8
8
  v-if="visible && isAttention"
9
9
  class="cn-banner-widget cn-banner-widget--attention"
10
- :class="'cn-banner-widget--' + resolvedVariant"
10
+ :class="['cn-banner-widget--' + resolvedVariant, { 'cn-banner-widget--stacked': isStacked }]"
11
11
  :aria-labelledby="headingId"
12
12
  data-testid="cn-banner-widget-attention">
13
13
  <div class="cn-banner-widget__body">
@@ -65,7 +65,9 @@
65
65
  import { translate as t } from '@nextcloud/l10n'
66
66
  import { NcNoteCard } from '@nextcloud/vue'
67
67
  import { normalizeLook } from '../../composables/useLook.js'
68
+ import { fillTemplate } from '../../utils/fillTemplate.js'
68
69
  import { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'
70
+ import { readPath } from '../../utils/readPath.js'
69
71
  import { safeHref } from '../../utils/safeHref.js'
70
72
  import { nextUid } from '../../utils/uid.js'
71
73
  import { compareVisibleWhen, readVisibleWhenValue } from '../../utils/visibleWhen.js'
@@ -266,6 +268,18 @@ export default {
266
268
  default: null,
267
269
  },
268
270
 
271
+ /**
272
+ * Attention card: stack the actions under the text instead of beside it,
273
+ * the "Begin hier" card of PqMijnWerk (16px title, the action under the
274
+ * reason). Falls back to the `content` blob.
275
+ *
276
+ * @spec openspec/changes/screens-start-here-banner/specs/start-here-banner/spec.md#requirement-a-card-can-stack-its-action-under-the-text
277
+ */
278
+ stacked: {
279
+ type: Boolean,
280
+ default: false,
281
+ },
282
+
269
283
  /**
270
284
  * Stored content blob (CnDashboardPage registry branch) carrying the
271
285
  * same keys as the flat props: `{ variant, text, visibleWhen, route,
@@ -326,6 +340,12 @@ export default {
326
340
  * the text wherever it says `{value}`.
327
341
  */
328
342
  conditionValue: null,
343
+ /**
344
+ * The first row of the `visibleWhen` source query (its `order`
345
+ * applied), the card's subject: `{subject.<field>}` in the texts
346
+ * and `@subject.<field>` in an action route read it.
347
+ */
348
+ conditionRow: null,
329
349
  /** Whether the visibleWhen request itself failed (not: answered "no"). */
330
350
  conditionFailed: false,
331
351
  /** Why it failed, as the request reported it. */
@@ -383,6 +403,15 @@ export default {
383
403
  return (this.layout || (this.content && this.content.layout) || 'banner') === 'attention'
384
404
  },
385
405
 
406
+ /**
407
+ * Whether the actions sit under the text.
408
+ *
409
+ * @return {boolean}
410
+ */
411
+ isStacked() {
412
+ return this.stacked || Boolean(this.content && this.content.stacked)
413
+ },
414
+
386
415
  /**
387
416
  * The attention card's kicker (prop, else content).
388
417
  *
@@ -391,7 +420,7 @@ export default {
391
420
  */
392
421
  resolvedKicker() {
393
422
  const kicker = this.kicker || (this.content && this.content.kicker) || ''
394
- return kicker ? this.tl(kicker) : ''
423
+ return kicker ? this.fillValue(this.tl(kicker)) : ''
395
424
  },
396
425
 
397
426
  /**
@@ -459,7 +488,7 @@ export default {
459
488
  let target = null
460
489
  let href = ''
461
490
  if (action.route) {
462
- target = typeof action.route === 'string' ? { name: action.route } : action.route
491
+ target = typeof action.route === 'string' ? { name: action.route } : this.resolveSubject(action.route)
463
492
  href = resolveHref(target, this.$router)
464
493
  } else if (typeof action.href === 'string' && action.href !== '') {
465
494
  const safe = safeHref(action.href)
@@ -518,7 +547,7 @@ export default {
518
547
  */
519
548
  uncheckedText() {
520
549
  const title = this.title || (this.content && this.content.title) || this.resolvedText
521
- if (!title || title.includes('{value}')) {
550
+ if (!title || title.includes('{value}') || title.includes('{subject.')) {
522
551
  return t('nextcloud-vue', 'Could not check')
523
552
  }
524
553
  // `escape: false`: the result is rendered through `{{ }}`, which escapes.
@@ -590,10 +619,55 @@ export default {
590
619
  * @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-attention-card
591
620
  */
592
621
  fillValue(text) {
593
- if (this.conditionValue === null || !text.includes('{value}')) {
622
+ let out = text
623
+ if (this.conditionValue !== null && out.includes('{value}')) {
624
+ out = out.replaceAll('{value}', String(this.conditionValue))
625
+ }
626
+ return this.fillSubject(out)
627
+ },
628
+
629
+ /**
630
+ * Fill `{subject.<field>}` (and `{subject.<field>|date}`, `|days`) from
631
+ * the card's subject row. A token with no value empties the whole text,
632
+ * as in a record list. Text without a subject token is untouched.
633
+ *
634
+ * @param {string} text The text.
635
+ * @return {string} The filled text.
636
+ * @spec openspec/changes/screens-start-here-banner/specs/start-here-banner/spec.md#requirement-a-banner-can-name-the-first-row-of-a-query
637
+ */
638
+ fillSubject(text) {
639
+ if (typeof text !== 'string' || !text.includes('{subject.')) {
594
640
  return text
595
641
  }
596
- return text.replaceAll('{value}', String(this.conditionValue))
642
+ // `{value}` is not a subject token: keep it out of the template fill.
643
+ const guard = '\u0000value\u0000'
644
+ const filled = fillTemplate(text.replaceAll('{value}', guard), { subject: this.conditionRow || {} })
645
+ return filled.replaceAll(guard, '{value}')
646
+ },
647
+
648
+ /**
649
+ * Replace `@subject.<field>` strings anywhere in an action route
650
+ * (params, query) by the subject's value.
651
+ *
652
+ * @param {unknown} node A route name, location or any nested part of one.
653
+ * @return {unknown} The same shape with the tokens resolved.
654
+ * @spec openspec/changes/screens-start-here-banner/specs/start-here-banner/spec.md#requirement-a-banner-can-name-the-first-row-of-a-query
655
+ */
656
+ resolveSubject(node) {
657
+ if (typeof node === 'string') {
658
+ if (!node.startsWith('@subject.')) {
659
+ return node
660
+ }
661
+ const value = this.conditionRow ? readPath(this.conditionRow, node.slice('@subject.'.length)) : undefined
662
+ return value === undefined || value === null ? '' : String(value)
663
+ }
664
+ if (Array.isArray(node)) {
665
+ return node.map((item) => this.resolveSubject(item))
666
+ }
667
+ if (node && typeof node === 'object') {
668
+ return Object.fromEntries(Object.entries(node).map(([key, value]) => [key, this.resolveSubject(value)]))
669
+ }
670
+ return node
597
671
  },
598
672
 
599
673
  /**
@@ -636,6 +710,7 @@ export default {
636
710
  if (outcome) {
637
711
  this.conditionMet = outcome.met === true
638
712
  this.conditionValue = outcome.value !== undefined ? outcome.value : null
713
+ this.conditionRow = outcome.row && typeof outcome.row === 'object' ? outcome.row : null
639
714
  this.conditionFailed = outcome.failed === true
640
715
  this.conditionFailureReason = outcome.failed === true && typeof outcome.reason === 'string' ? outcome.reason : ''
641
716
  return
@@ -644,12 +719,15 @@ export default {
644
719
  if (!cond) {
645
720
  this.conditionMet = null
646
721
  this.conditionValue = null
722
+ this.conditionRow = null
647
723
  this.conditionFailed = false
648
724
  this.conditionFailureReason = ''
649
725
  return
650
726
  }
651
727
  try {
652
- const value = await readVisibleWhenValue(cond)
728
+ const sink = {}
729
+ const value = await readVisibleWhenValue(cond, undefined, sink)
730
+ this.conditionRow = sink.row || null
653
731
  this.conditionValue = value
654
732
  this.conditionMet = compareVisibleWhen(value, cond.op || 'eq', cond.value)
655
733
  this.conditionFailed = false
@@ -662,6 +740,7 @@ export default {
662
740
  console.warn('[CnBannerWidget] visibleWhen could not be evaluated, banner hidden:', error?.message || error)
663
741
  this.conditionMet = false
664
742
  this.conditionValue = null
743
+ this.conditionRow = null
665
744
  this.conditionFailed = true
666
745
  this.conditionFailureReason = typeof error?.message === 'string' ? error.message : String(error ?? '')
667
746
  }
@@ -784,6 +863,32 @@ export default {
784
863
  color: var(--color-text-maxcontrast);
785
864
  }
786
865
 
866
+ /* Stacked (PqMijnWerk "Begin hier"): the text, then the action under it. */
867
+ .cn-banner-widget--stacked {
868
+ flex-direction: column;
869
+ align-items: flex-start;
870
+ gap: 10px;
871
+ }
872
+
873
+ .cn-banner-widget--stacked .cn-banner-widget__body {
874
+ flex: 0 0 auto;
875
+ gap: 10px;
876
+ }
877
+
878
+ .cn-banner-widget--stacked .cn-banner-widget__title {
879
+ font-size: 16px;
880
+ line-height: 1.4;
881
+ }
882
+
883
+ .cn-banner-widget--stacked .cn-banner-widget__reason {
884
+ font-size: 14px;
885
+ }
886
+
887
+ .cn-banner-widget--stacked .cn-banner-widget__action {
888
+ min-height: var(--cn-board-control-height, 40px);
889
+ font-size: 14px;
890
+ }
891
+
787
892
  .cn-banner-widget__actions {
788
893
  display: flex;
789
894
  flex-wrap: wrap;
@@ -882,11 +882,14 @@ export default {
882
882
  * a `{ value: variant }` map (e.g. `{ placed: 'primary', delivered: 'success' }`)
883
883
  * that CnStatusBadge resolves per label. Null when unset (falls back to
884
884
  * `badgeVariant`). Lets a manifest colour a status column without code.
885
+ * A column-level `colorMap` (it reaches the cell as the property's
886
+ * `colorMap`) works too, so `{ widget: 'badge', colorMap }` and
887
+ * `{ widget: 'badge', widgetProps: { colorMap } }` read the same.
885
888
  *
886
889
  * @return {object|null}
887
890
  */
888
891
  badgeColorMap() {
889
- return (this.widgetProps && this.widgetProps.colorMap) || null
892
+ return (this.widgetProps && this.widgetProps.colorMap) || this.enumColorMap || null
890
893
  },
891
894
 
892
895
  /**
@@ -29,7 +29,7 @@ import { readPath } from '../../utils/readPath.js'
29
29
  * `content: { field, endpoint?, authorField?, timeField?, textField?, sideField?, usValue? }`.
30
30
  * `@objectId` in `endpoint` is replaced by the record's id. A reply the
31
31
  * endpoint accepts is shown at once, until the record is read again.
32
- * Resolved by its registry key, not exported.
32
+ * Resolved by its registry key, and exported for an app that renders it itself.
33
33
  */
34
34
  export default {
35
35
  name: 'CnConversationWidget',
@@ -424,14 +424,14 @@ export default {
424
424
  * anything whose height is whatever it turned out to have.
425
425
  *
426
426
  * @param {object} item the layout item.
427
- * @return {string|undefined} `'true'`, a row minimum as a string, or undefined.
427
+ * @return {string|undefined} `'true'`, a row maximum as a string, or undefined.
428
428
  */
429
429
  sizeToContentAttr(item) {
430
430
  const v = item && item.sizeToContent
431
431
  if (v === true) {
432
432
  return 'true'
433
433
  }
434
- // A number is GridStack's "size to content, but never below N rows".
434
+ // A number is GridStack's "size to content, but never taller than N rows" (a maximum).
435
435
  if (Number.isFinite(v) && v > 0) {
436
436
  return String(v)
437
437
  }
@@ -933,7 +933,7 @@ import { normalizeLook } from '../../composables/useLook.js'
933
933
  import { pageViews } from '../../mixins/pageViews.js'
934
934
  import { mergeUserLayout, resolveUserLayoutApi } from '../../store/plugins/dashboardLayouts.js'
935
935
  import { isAppInstalled } from '../../utils/appInstalled.js'
936
- import { compareVisibleWhen, readVisibleWhenValue } from '../../utils/visibleWhen.js'
936
+ import { compareVisibleWhen, evaluateWorkspaceCondition, isWorkspaceCondition, readVisibleWhenValue } from '../../utils/visibleWhen.js'
937
937
  import { canonicalWidgetType } from '../../utils/widgetTypeAliases.js'
938
938
  import { CnActionButtons } from '../CnActionButtons/index.js'
939
939
  import { CnActionsMenu } from '../CnActionsMenu/index.js'
@@ -2256,6 +2256,10 @@ export default {
2256
2256
  const items = []
2257
2257
  for (const id of ids) {
2258
2258
  if (this.getWidgetDef(id) || this.hasWidgetSlot(id)) {
2259
+ // A tile a page control hides surrenders its cell.
2260
+ if (!this.gridEditable && this.isHiddenByWorkspace(id)) {
2261
+ continue
2262
+ }
2259
2263
  items.push({ widgetId: id })
2260
2264
  } else if (process.env.NODE_ENV !== 'production') {
2261
2265
  // eslint-disable-next-line no-console
@@ -3435,6 +3439,11 @@ export default {
3435
3439
  if (!visibleWhen) {
3436
3440
  return false
3437
3441
  }
3442
+ // A workspace condition (a page control's choice) is answered at
3443
+ // once from the reactive bag; nothing is fetched for it.
3444
+ if (isWorkspaceCondition(visibleWhen)) {
3445
+ return !evaluateWorkspaceCondition(visibleWhen, this.workspaceContext)
3446
+ }
3438
3447
  const outcome = this.widgetConditionOutcome[item.widgetId]
3439
3448
  if (outcome && outcome.failed === true && layout === 'attention' && this.isBannerDef(def)) {
3440
3449
  return false
@@ -3442,6 +3451,23 @@ export default {
3442
3451
  return !outcome || outcome.met !== true
3443
3452
  },
3444
3453
 
3454
+ /**
3455
+ * Whether a page control (a workspace condition on `visibleWhen`)
3456
+ * hides this widget right now. Other conditions are not looked at.
3457
+ *
3458
+ * @param {string} widgetId The widget id.
3459
+ * @return {boolean} True when the widget must not draw.
3460
+ * @spec openspec/changes/screens-page-lenses/specs/page-lenses/spec.md#requirement-a-widget-can-be-shown-by-a-workspace-value
3461
+ */
3462
+ isHiddenByWorkspace(widgetId) {
3463
+ const def = this.getWidgetDef(widgetId)
3464
+ if (!def || !def.type) {
3465
+ return false
3466
+ }
3467
+ const { visibleWhen } = this.widgetDisplayConfig(def)
3468
+ return isWorkspaceCondition(visibleWhen) && !evaluateWorkspaceCondition(visibleWhen, this.workspaceContext)
3469
+ },
3470
+
3445
3471
  /**
3446
3472
  * Evaluate every conditional widget's `visibleWhen` into
3447
3473
  * `widgetConditionOutcome` — the verdict AND the raw field value,
@@ -3464,7 +3490,8 @@ export default {
3464
3490
  async evaluateWidgetConditions() {
3465
3491
  const defs = [...(this.widgets || []), ...this.normalizedViews.flatMap((view) => view.widgets)]
3466
3492
  const conditional = defs.filter((def) => def && def.id && def.type
3467
- && this.widgetDisplayConfig(def).visibleWhen)
3493
+ && this.widgetDisplayConfig(def).visibleWhen
3494
+ && !isWorkspaceCondition(this.widgetDisplayConfig(def).visibleWhen))
3468
3495
  // Skip when the CONDITIONAL SET is unchanged: the deep `widgets`
3469
3496
  // watch fires on any def edit, but only an added/removed/edited
3470
3497
  // predicate warrants refetching every conditional endpoint.
@@ -3484,8 +3511,13 @@ export default {
3484
3511
  const cond = this.widgetDisplayConfig(def).visibleWhen
3485
3512
  let outcome
3486
3513
  try {
3487
- const value = await readVisibleWhenValue(cond)
3514
+ // `sink` keeps the first row of a source query, for a banner that names it.
3515
+ const sink = {}
3516
+ const value = await readVisibleWhenValue(cond, undefined, sink)
3488
3517
  outcome = { met: compareVisibleWhen(value, cond.op || 'eq', cond.value), value }
3518
+ if (sink.row) {
3519
+ outcome.row = sink.row
3520
+ }
3489
3521
  } catch (error) {
3490
3522
  // Fail-safe: not met. But the failure is kept, and said
3491
3523
  // out loud: a failed count and "nothing to report" look
@@ -3985,6 +4017,11 @@ export default {
3985
4017
  const def = this.getWidgetDef(item.widgetId)
3986
4018
  const value = item.showTitle !== undefined ? item.showTitle : def?.showTitle
3987
4019
  if (value === undefined || value === null) {
4020
+ // A banner that names no title draws no title row: it used to show
4021
+ // its widget id ("attention-first") as the card title.
4022
+ if (this.isBannerDef(def) && !item.customTitle && !def?.customTitle && !def?.title) {
4023
+ return false
4024
+ }
3988
4025
  return !this.isCardWidget(item)
3989
4026
  }
3990
4027
  return value !== false