@conduction/nextcloud-vue 2.103.0 → 2.105.0

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 (154) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +23 -0
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +94 -72
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  6. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +1 -1
  8. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +18 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +3 -3
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  13. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  14. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +93 -1
  15. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  16. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +6 -3
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +3 -3
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  22. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +39 -2
  23. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +14 -0
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  26. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +99 -3
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
  30. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +1 -1
  31. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
  32. package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +1 -1
  34. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +2 -0
  35. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -1
  36. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +32 -29
  37. package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -1
  38. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue.js +1 -1
  39. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js +73 -4
  40. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js.map +1 -1
  41. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js +74 -27
  42. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +213 -4
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +105 -85
  46. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  47. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +10 -4
  48. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  49. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue.js +9 -0
  50. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue.js.map +1 -0
  51. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue2.js +88 -0
  52. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue2.js.map +1 -0
  53. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue3.js +39 -0
  54. package/dist/esm/components/CnNoticeWidget/CnNoticeWidget.vue3.js.map +1 -0
  55. package/dist/esm/components/CnObjectRow/CnObjectRow.vue.js +1 -1
  56. package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js +57 -5
  57. package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js.map +1 -1
  58. package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js +37 -4
  59. package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js.map +1 -1
  60. package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +1 -1
  61. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -1
  62. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +3 -3
  63. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -1
  64. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  65. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  66. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +16 -4
  67. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  68. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
  69. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
  70. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
  71. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  72. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +11 -0
  73. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  74. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +4 -4
  75. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  76. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +16 -14
  77. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  78. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  79. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +18 -2
  80. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  81. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  82. package/dist/esm/composables/indexSources.js +7 -7
  83. package/dist/esm/composables/indexSources.js.map +1 -1
  84. package/dist/esm/index.js +1 -0
  85. package/dist/esm/index.js.map +1 -1
  86. package/dist/esm/l10n/en.json.js +8 -1
  87. package/dist/esm/l10n/en.json.js.map +1 -1
  88. package/dist/esm/l10n/nl.json.js +8 -1
  89. package/dist/esm/l10n/nl.json.js.map +1 -1
  90. package/dist/esm/library-style.js +1 -1
  91. package/dist/esm/nextcloud-vue.css +680 -328
  92. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  93. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  94. package/dist/esm/utils/nextcloudApps.js +45 -0
  95. package/dist/esm/utils/nextcloudApps.js.map +1 -0
  96. package/dist/esm/utils/nextcloudDirectory.js +18 -1
  97. package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
  98. package/dist/esm/utils/scopeListLayout.js +9 -2
  99. package/dist/esm/utils/scopeListLayout.js.map +1 -1
  100. package/dist/esm/utils/sortOrder.js +61 -0
  101. package/dist/esm/utils/sortOrder.js.map +1 -0
  102. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  103. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  104. package/dist/esm/utils/visibleWhen.js +12 -4
  105. package/dist/esm/utils/visibleWhen.js.map +1 -1
  106. package/dist/nextcloud-vue.cjs.js +4046 -2999
  107. package/dist/nextcloud-vue.cjs.js.map +2 -2
  108. package/dist/nextcloud-vue.css +680 -328
  109. package/l10n/en.json +8 -1
  110. package/l10n/nl.json +8 -1
  111. package/package.json +1 -1
  112. package/src/components/CnActionsBar/CnActionsBar.md +1 -0
  113. package/src/components/CnActionsBar/CnActionsBar.vue +39 -2
  114. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +1 -1
  115. package/src/components/CnAppRoot/CnAppRoot.md +1 -1
  116. package/src/components/CnAppRoot/CnAppRoot.vue +21 -4
  117. package/src/components/CnChartWidget/CnChartWidget.md +2 -0
  118. package/src/components/CnChartWidget/CnChartWidget.vue +93 -1
  119. package/src/components/CnDashboardPage/CnDashboardPage.vue +9 -6
  120. package/src/components/CnDataTable/CnDataTable.md +1 -0
  121. package/src/components/CnDataTable/CnDataTable.vue +49 -0
  122. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  123. package/src/components/CnDetailPage/CnDetailPage.vue +99 -3
  124. package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +1 -1
  125. package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +4 -1
  126. package/src/components/CnFlowsPage/CnFlowBoard.vue +132 -8
  127. package/src/components/CnIndexPage/CnIndexPage.md +5 -2
  128. package/src/components/CnIndexPage/CnIndexPage.vue +228 -9
  129. package/src/components/CnIndexPage/useSelfFetchList.js +9 -3
  130. package/src/components/CnNoticeWidget/CnNoticeWidget.vue +153 -0
  131. package/src/components/CnNoticeWidget/index.js +12 -0
  132. package/src/components/CnObjectRow/CnObjectRow.vue +95 -1
  133. package/src/components/CnSettingsField/CnSettingsField.vue +4 -1
  134. package/src/components/CnSetupWizard/CnSetupWizard.vue +9 -1
  135. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +7 -2
  136. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +15 -4
  137. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  138. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +18 -2
  139. package/src/components/index.js +1 -0
  140. package/src/composables/indexSources.js +7 -7
  141. package/src/css/dialog.css +20 -2
  142. package/src/css/index-page.css +19 -0
  143. package/src/css/look-board-detail.css +77 -4
  144. package/src/css/look-board-index.css +84 -2
  145. package/src/css/look-board.css +97 -12
  146. package/src/index.js +1 -0
  147. package/src/schemas/app-manifest-v2.schema.json +58 -3
  148. package/src/utils/libraryWidgetKeys.js +1 -0
  149. package/src/utils/nextcloudApps.js +50 -0
  150. package/src/utils/nextcloudDirectory.js +17 -0
  151. package/src/utils/scopeListLayout.js +9 -2
  152. package/src/utils/sortOrder.js +58 -0
  153. package/src/utils/validateManifestV2.compiled.js +3 -3
  154. package/src/utils/visibleWhen.js +12 -4
@@ -163,8 +163,15 @@
163
163
  gap: 8px;
164
164
  }
165
165
 
166
+ /* A chip is a <button>: the nldesign theme sets every button's radius and
167
+ weight with `!important` through its button tokens, which drew the chips
168
+ as 8px boxes at 700 (DqTeamwachtrij), so the chip hands its pill radius and
169
+ 600 to those tokens.
170
+ @spec openspec/changes/board-chips-theme/specs/board-chips-theme/spec.md#requirement-board-chips-stay-pills-under-a-theme */
166
171
  .cn-look-board .cn-saved-views__chips .cn-saved-views__chip,
167
172
  .cn-look-board .cn-quick-filter-bar .cn-quick-filter-bar__tab {
173
+ --nldesign-component-button-border-radius: 19px;
174
+ --nldesign-component-button-font-weight: 600;
168
175
  display: inline-flex;
169
176
  align-items: center;
170
177
  gap: 8px;
@@ -267,12 +274,30 @@
267
274
  padding: 0;
268
275
  }
269
276
 
270
- /* Filter: a labelled secondary button, 36px, radius 8, with the active count. */
277
+ /* The toolbar's buttons are the board's neutral control (DqTeamwachtrij
278
+ "Filter"): the main text at 600 on the main background with a grey border.
279
+ The nldesign theme paints a secondary button in the primary colour with
280
+ `!important` through its button tokens, so the bar hands the board's values
281
+ to those tokens.
282
+ @spec openspec/changes/board-toolbar-buttons-theme/specs/board-toolbar-buttons-theme/spec.md#requirement-board-toolbar-buttons-are-neutral-under-a-theme */
283
+ .cn-look-board .cn-actions-bar:not(.cn-look-nextcloud *) {
284
+ --nldesign-component-button-font-weight: 600;
285
+ --nldesign-component-button-secondary-action-color: var(--color-main-text);
286
+ --nldesign-component-button-secondary-action-background-color: var(--color-main-background);
287
+ --nldesign-component-button-secondary-action-border-color: var(--color-border-dark);
288
+ --nldesign-component-button-secondary-action-hover-background-color: var(--color-background-hover);
289
+ }
290
+
291
+ /* Filter: a labelled secondary button, 36px, radius 8, 12px at both sides
292
+ and 14px text, with the active count. NcButton with an icon pads its start
293
+ 4px and sets 15px. */
271
294
  .cn-look-board .cn-actions-bar__filter-button.button-vue,
272
295
  .cn-look-board .cn-actions-bar__filter-menu .button-vue {
273
296
  min-height: 36px;
274
297
  height: 36px;
298
+ padding: 0 12px;
275
299
  border-radius: 8px;
300
+ font-size: 14px;
276
301
  font-weight: 600;
277
302
  }
278
303
 
@@ -372,6 +397,17 @@
372
397
  cursor: not-allowed;
373
398
  }
374
399
 
400
+ /* `searchPlacement: "toolbar"`: the search field on row 1 takes the room
401
+ before Filter and the view switch, which keep the end of the row. */
402
+ .cn-look-board .cn-actions-bar--board > .cn-actions-bar__row--views > .cn-actions-bar__search--toolbar {
403
+ flex: 1 1 220px;
404
+ min-width: 180px;
405
+ }
406
+
407
+ .cn-look-board .cn-actions-bar--board > .cn-actions-bar__row--views > .cn-actions-bar__search--toolbar ~ .cn-actions-bar__view-controls {
408
+ margin-inline-start: 0;
409
+ }
410
+
375
411
  /* The search field: 40px, radius 8, not a pill. */
376
412
  .cn-look-board .cn-actions-bar--board .cn-actions-bar__search {
377
413
  flex: 1 1 260px;
@@ -502,9 +538,18 @@
502
538
 
503
539
  /* The header row is near-white on the boards (#fafbfc), not the hover grey,
504
540
  its labels 14px muted. The ink also goes to --nldesign-color-on-surface,
505
- which a theme that forces the body ink onto every span (thematiq) reads. */
541
+ which a theme that forces the body ink onto every span (thematiq) reads.
542
+ The nldesign theme paints every `th` with `!important` through its table
543
+ header tokens, and the sort button in it through its button weight token
544
+ (bold), so the cell hands the board's ground, ink and weight to those
545
+ tokens too.
546
+ @spec openspec/changes/board-table-head-theme/specs/board-table-head-theme/spec.md#requirement-a-board-table-header-keeps-the-boards-ground-ink-and-weight-under-a-theme */
506
547
  .cn-look-board .cn-table-container--board .cn-data-table th {
507
548
  --nldesign-color-on-surface: var(--color-text-maxcontrast);
549
+ --nldesign-component-table-header-background-color: var(--cn-board-table-head-bg, color-mix(in srgb, var(--color-background-hover) 40%, var(--color-main-background)));
550
+ --nldesign-component-table-header-color: var(--color-text-maxcontrast);
551
+ --nldesign-component-table-header-font-weight: 600;
552
+ --nldesign-component-button-font-weight: 600;
508
553
  padding: 12px 16px;
509
554
  border-bottom: 0;
510
555
  background: var(--cn-board-table-head-bg, color-mix(in srgb, var(--color-background-hover) 40%, var(--color-main-background)));
@@ -1108,3 +1153,40 @@
1108
1153
  .cn-look-board:has(.cn-index-page) > .app-sidebar__toggle {
1109
1154
  display: none;
1110
1155
  }
1156
+
1157
+ /* The folder pane as a card (DqAfdelingen): 280px, a hairline border, 12px
1158
+ corners, with each folder's count in a grey pill. */
1159
+ .cn-look-board .cn-index-page__folder-pane {
1160
+ flex: 0 0 280px;
1161
+ align-self: flex-start;
1162
+ margin-inline-end: 20px;
1163
+ padding: 14px;
1164
+ border: 1px solid var(--cn-board-hairline);
1165
+ border-radius: var(--cn-board-card-radius, 12px);
1166
+ background: var(--color-main-background);
1167
+ }
1168
+
1169
+ .cn-look-board .cn-index-page__folder-pane .cn-folder-tree__count {
1170
+ display: inline-flex;
1171
+ align-items: center;
1172
+ justify-content: center;
1173
+ min-width: 20px;
1174
+ height: 20px;
1175
+ padding: 0 6px;
1176
+ box-sizing: border-box;
1177
+ border-radius: 10px;
1178
+ background: var(--color-background-dark);
1179
+ color: var(--color-main-text);
1180
+ font-size: 12px;
1181
+ font-weight: 600;
1182
+ }
1183
+
1184
+ /* The selected folder's heading above the list: a 20px title over a soft line. */
1185
+ .cn-look-board .cn-index-page__scope-heading {
1186
+ margin-bottom: 16px;
1187
+ }
1188
+
1189
+ .cn-look-board .cn-index-page__scope-title {
1190
+ font-size: 20px;
1191
+ font-weight: 700;
1192
+ }
@@ -27,6 +27,8 @@
27
27
  /* Cards. */
28
28
  --cn-board-card-radius: 12px;
29
29
  --cn-board-card-padding: 20px 22px;
30
+ /* The side inset of a dashboard card: its title and its rows. */
31
+ --cn-board-dashboard-card-inset: 24px;
30
32
  /* Hairline inside a card, and the soft secondary text. */
31
33
  --cn-board-hairline: var(--color-border);
32
34
  --cn-board-text-soft: var(--color-main-text);
@@ -56,6 +58,23 @@
56
58
  margin-inline: 0;
57
59
  }
58
60
 
61
+ /* The page shows the content area's ground (the theme's content surface,
62
+ light grey on the boards) and draws none of its own; only its cards are
63
+ white. A theme rule that paints every `[class*="panel"]` with the main
64
+ background (the nldesign theme's element overrides) caught an index page
65
+ with a side panel (`cn-index-page--with-side-panel`) and drew the whole
66
+ page white (DqTeamwachtrij). That rule carries an id through
67
+ `:not(#header *)`, so this one names an id in `:not()` to outrank it.
68
+ @spec openspec/changes/board-page-ground/specs/board-page-ground/spec.md#requirement-a-board-page-draws-no-ground-of-its-own */
69
+ .cn-look-board .cn-index-page:not(#cn-board-page-ground):not(.cn-look-nextcloud *),
70
+ .cn-look-board .cn-detail-page:not(#cn-board-page-ground):not(.cn-look-nextcloud *),
71
+ .cn-look-board .cn-dashboard-page:not(#cn-board-page-ground):not(.cn-look-nextcloud *),
72
+ .cn-look-board .cn-settings-page:not(#cn-board-page-ground):not(.cn-look-nextcloud *),
73
+ .cn-look-board .cn-store-page:not(#cn-board-page-ground):not(.cn-look-nextcloud *),
74
+ .cn-look-board .cn-admin-settings-shell:not(#cn-board-page-ground):not(.cn-look-nextcloud *) {
75
+ background-color: transparent;
76
+ }
77
+
59
78
  .cn-look-board .cn-index-page:not(.cn-look-nextcloud *),
60
79
  .cn-look-board .cn-settings-page:not(.cn-look-nextcloud *) {
61
80
  gap: var(--cn-board-section-gap);
@@ -155,6 +174,27 @@
155
174
  gap: 10px;
156
175
  }
157
176
 
177
+ /* The nldesign theme paints every primary and secondary button with
178
+ `!important` through its button tokens: weight 700, and a secondary in
179
+ the primary colour with a primary border on no ground. The board draws
180
+ header buttons at 600, the secondary on the main background with a grey
181
+ border (DqMijnWerk, DqZaak, DqZaken). The look hands the theme those values
182
+ through the same tokens, so the theme's own rules draw the board's button.
183
+ @spec openspec/changes/board-header-buttons-theme/specs/board-header-buttons-theme/spec.md#requirement-board-header-buttons-keep-the-boards-weight-and-ground-under-a-theme */
184
+ .cn-look-board .cn-index-page__header-buttons:not(.cn-look-nextcloud *),
185
+ .cn-look-board .cn-detail-page__header-actions:not(.cn-look-nextcloud *),
186
+ .cn-look-board .cn-dashboard-page__header-actions:not(.cn-look-nextcloud *),
187
+ .cn-look-board .cn-settings-page__header-buttons:not(.cn-look-nextcloud *),
188
+ .cn-look-board .cn-admin-settings-shell__header-buttons:not(.cn-look-nextcloud *),
189
+ .cn-look-board .cn-action-buttons:not(.cn-look-nextcloud *) {
190
+ --nldesign-component-button-font-weight: 600;
191
+ --nldesign-component-button-primary-action-font-weight: 600;
192
+ --nldesign-component-button-secondary-action-color: var(--color-main-text);
193
+ --nldesign-component-button-secondary-action-background-color: var(--color-main-background);
194
+ --nldesign-component-button-secondary-action-border-color: var(--color-border-dark);
195
+ --nldesign-component-button-secondary-action-hover-background-color: var(--color-background-hover);
196
+ }
197
+
158
198
  .cn-look-board .cn-index-page__header-buttons:not(.cn-look-nextcloud *) .button-vue,
159
199
  .cn-look-board .cn-detail-page__header-actions:not(.cn-look-nextcloud *) .button-vue,
160
200
  .cn-look-board .cn-dashboard-page__header-actions:not(.cn-look-nextcloud *) .button-vue,
@@ -1158,6 +1198,20 @@
1158
1198
  line-height: 1.3;
1159
1199
  }
1160
1200
 
1201
+ /* The board draws no rule under a dashboard card's title (DqMijnWerk,
1202
+ DqDashboard, PqMijnWerk): the title sits 20px from the card's top and on
1203
+ the card's 24px side inset, the line its rows start on, 12px above the
1204
+ content. A borderless card has no edge to inset from and keeps the
1205
+ wrapper's padding.
1206
+ @spec openspec/changes/board-dashboard-card-header/specs/board-dashboard-card-header/spec.md#requirement-a-board-dashboard-card-draws-no-header-rule */
1207
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--nc-dashboard) .cn-widget-wrapper__header {
1208
+ border-bottom: none;
1209
+ }
1210
+
1211
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--nc-dashboard):not(.cn-widget-wrapper--borderless) .cn-widget-wrapper__header {
1212
+ padding: 20px var(--cn-board-dashboard-card-inset) 12px;
1213
+ }
1214
+
1161
1215
  /* ---- Record list on a dashboard card (PtDashboard) ---------------------- */
1162
1216
  /* The dashboard's card body is flush, so the rows carry their own inset: the
1163
1217
  board's card pads 24px at the sides and each row 14px, a soft hairline
@@ -1274,6 +1328,47 @@
1274
1328
  display: none;
1275
1329
  }
1276
1330
 
1331
+ /* ---- Widgets that drew their rows on the card's edge -------------------- */
1332
+ /* The card body is flush, so a widget carries the card's side inset itself,
1333
+ as the record list and saved views do. Without it the task list's
1334
+ checkboxes, the step bar and its legend, the week strip's days, the bar
1335
+ list and the activity items started on the card's edge, and a table's
1336
+ first column 16px in (DqMijnWerk "Mijn taken", "Mijn 14 zaken per stap",
1337
+ "Termijnen deze week", "Recente activiteit"; DqDashboard "Zaken per
1338
+ status", "Vastgelopen zaken"). The top inset is 4px: with the header's
1339
+ 12px it is the board's 16px from title to bar. This also replaces the
1340
+ step bar's and week strip's opt-in `content.inset` on a board dashboard.
1341
+ A borderless card stands on the page ground and keeps its rows flush.
1342
+ @spec openspec/changes/board-dashboard-widget-inset/specs/board-dashboard-widget-inset/spec.md#requirement-a-board-dashboard-widget-keeps-the-cards-side-inset */
1343
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-tasks-widget {
1344
+ padding: 0 var(--cn-board-dashboard-card-inset) 10px;
1345
+ }
1346
+
1347
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-stacked-bar,
1348
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-week-strip {
1349
+ box-sizing: border-box;
1350
+ padding: 4px var(--cn-board-dashboard-card-inset) 22px;
1351
+ }
1352
+
1353
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-chart-widget__html {
1354
+ padding: 4px var(--cn-board-dashboard-card-inset) 20px;
1355
+ }
1356
+
1357
+ /* An activity item pads itself 4px, so the body takes 4px less. */
1358
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-nc-widget-widget__body {
1359
+ padding: 0 calc(var(--cn-board-dashboard-card-inset) - 4px) 16px;
1360
+ }
1361
+
1362
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-widget-object-table .cn-data-table th:first-child,
1363
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-widget-object-table .cn-data-table td:first-child {
1364
+ padding-inline-start: var(--cn-board-dashboard-card-inset);
1365
+ }
1366
+
1367
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-widget-object-table .cn-data-table th:last-child,
1368
+ .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:not(.cn-widget-wrapper--borderless) .cn-widget-object-table .cn-data-table td:last-child {
1369
+ padding-inline-end: var(--cn-board-dashboard-card-inset);
1370
+ }
1371
+
1277
1372
  /* ---- Week strip (DqMijnWerk "Termijnen deze week") ---------------------- */
1278
1373
  /* The days wrap inside the card instead of scrolling sideways: a row holds
1279
1374
  as many days of at least 120px as the card fits, and the rest go to the
@@ -1293,23 +1388,13 @@
1293
1388
  flex-wrap: wrap;
1294
1389
  }
1295
1390
 
1296
- /* ---- Bar-list and matrix cards (DqDashboard) ----------------------------- */
1297
- /* The board draws no rule under the title of a card holding a bar list or a
1298
- matrix. Other cards, plain stat tiles among them, keep theirs.
1299
- @spec openspec/changes/chart-bar-list-and-matrix/specs/chart-bar-list-and-matrix/spec.md#requirement-board-bar-list-and-matrix-cards-draw-no-header-rule */
1300
- .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:has(.cn-chart-widget__html) .cn-widget-wrapper__header {
1301
- border-bottom: none;
1302
- }
1303
-
1304
1391
  /* ---- A card of boxed stat tiles (DcRapportages "Vergaderingen en efficiëntie") -- */
1305
1392
  /* The board draws this card 22px from its edge on every side but the bottom
1306
- (20px), the header 14px above the tiles, and no rule under the header. The
1307
- card-fit cell padding (8px 14px) plus the grid's own 24px came to 38px at
1308
- the sides, with the wrapper's divider under the title.
1393
+ (20px) and the header 14px above the tiles. The card-fit cell padding
1394
+ (8px 14px) plus the grid's own 24px came to 38px at the sides.
1309
1395
  @spec openspec/changes/stats-box-card-inset/specs/stats-box-card-inset/spec.md#requirement-a-card-of-boxed-stat-tiles-keeps-the-boards-inset */
1310
1396
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:has(.cn-stats-block-widget--tiles-box) .cn-widget-wrapper__header {
1311
1397
  padding: 20px 22px 0;
1312
- border-bottom: none;
1313
1398
  }
1314
1399
 
1315
1400
  .cn-look-board .cn-dashboard-page:not(.cn-look-nextcloud *) .cn-widget-wrapper:has(.cn-stats-block-widget--tiles-box) .cn-widget-wrapper__content {
package/src/index.js CHANGED
@@ -220,6 +220,7 @@ export {
220
220
  CnNotepadWidget,
221
221
  CnNotepadWidgetForm,
222
222
  CnNotesCard,
223
+ CnNoticeWidget,
223
224
  CnNotificationMatrix,
224
225
  CnNotificationPreferences,
225
226
  CnObjectAccessTab,
@@ -3,7 +3,7 @@
3
3
  "$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
4
4
  "title": "Conduction App Manifest v2",
5
5
  "description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
6
- "version": "2.126.0",
6
+ "version": "2.128.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -120,6 +120,7 @@
120
120
  "optionsByParent": { "type": "object", "description": "choice: map of parent-choice value → options[] for a dependent (dependsOn) choice. Same option item shape as `options`.", "additionalProperties": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["value", "label"], "properties": { "value": {}, "label": { "type": "string" } } } } },
121
121
  "suggestFrom": { "type": "string", "description": "choice: the configKey of an earlier choice this step pre-fills a suggestion from. When that value is set and the user has not yet picked here, the option matching suggestMap[parentValue] is auto-selected (still user-overridable)." },
122
122
  "default": { "description": "choice: the option value chosen when the step opens (an array of values with `multiple`). Not applied to a step the server already completed." },
123
+ "noteStyle": { "type": "string", "enum": ["line", "card"], "default": "line", "description": "choice: how `note` is drawn: a muted line under the options (`line`) or an info note card (`card`)." },
123
124
  "suggestMap": { "type": "object", "description": "choice: map of suggestFrom value → suggested option value. Used to auto-suggest a default (e.g. RGS template suggested from organisation type).", "additionalProperties": { "type": "string" } },
124
125
  "multiple": { "type": "boolean", "default": false, "description": "choice: allow multi-select." },
125
126
  "loadAction": { "type": "string", "minLength": 1, "description": "choice with display cards: an actionId each option card loads itself with. Every card except the one whose value is `none` gets its own Load button, which POSTs { dataset: <value> } to /api/setup/action/{loadAction}, spins while it runs and shows the result on that card. A successful load also selects the card. With loadAction the separate run-action step that loaded the picked dataset is no longer needed; manifests that keep it still work." },
@@ -1998,6 +1999,7 @@
1998
1999
  "items": { "$ref": "#/$defs/action" },
1999
2000
  "description": "Declarative row/widget actions rendered by CnWidgetObjectTable. object-op patch/delete render per row; object-op create renders as a widget-scoped footer affordance."
2000
2001
  },
2002
+ "pageSize": { "type": "integer", "minimum": 1, "description": "Rows per page: the table shows one page with the board pager under it (\"4 of 7\", DqCatalogus). Under a pager `source.limit` is how many rows are read (2.127.0)." },
2001
2003
  "rowClass": {
2002
2004
  "type": "array",
2003
2005
  "description": "#91: declarative per-row CSS class rules compiled into CnDataTable's rowClass function. Each rule { when: { field, op?, value }, class } adds `class` to a row when the shared visibleWhen predicate holds against that row (field = dot-path into the row, op = eq|neq|gt|gte|lt|lte default eq, value = literal). Rules evaluate in order and every matching class is space-joined — so overdue / at-risk rows can be highlighted from the manifest. A host-supplied rowClass FUNCTION (runtime-only) still passes straight through.",
@@ -2208,6 +2210,31 @@
2208
2210
  }
2209
2211
  }
2210
2212
  },
2213
+ {
2214
+ "description": "notice built-in widget (CnNoticeWidget): one toned box with an icon, a bold title and a text, through the host's translate, in the side column or the grid (DqSociaalDomein, 2.128.0).",
2215
+ "if": {
2216
+ "properties": { "widgetKey": { "const": "notice" } },
2217
+ "required": ["widgetKey"]
2218
+ },
2219
+ "then": {
2220
+ "properties": {
2221
+ "props": {
2222
+ "type": "object",
2223
+ "properties": {
2224
+ "content": {
2225
+ "type": "object",
2226
+ "properties": {
2227
+ "variant": { "type": "string", "enum": ["info", "warning", "error", "success"], "default": "info" },
2228
+ "title": { "type": "string" },
2229
+ "text": { "type": "string" },
2230
+ "icon": { "type": "string", "minLength": 1 }
2231
+ }
2232
+ }
2233
+ }
2234
+ }
2235
+ }
2236
+ }
2237
+ },
2211
2238
  {
2212
2239
  "description": "audit-trail built-in widget (CnAuditTrailWidget, Wave 1): the object change-log card. All props are optional — register/schema/objectId normally arrive via the detail-page object-context merge; explicit props win.",
2213
2240
  "if": {
@@ -2268,6 +2295,8 @@
2268
2295
  "emptyLabel": { "type": "string", "description": "Empty-state message rendered instead of the chart when the resolved series contain no data points." },
2269
2296
  "barMax": { "type": "number", "exclusiveMinimum": 0, "description": "chartKind 'bar-list': the value a full track stands for (100 for percentages). Omitted = the largest value." },
2270
2297
  "footnote": { "type": "string", "description": "A line under the chart, through the host's translate: under the rows of a bar-list or matrix, and under the plot of a line, area or bar chart." },
2298
+ "referenceLines": { "type": "array", "description": "line, area and bar charts: dashed horizontal reference lines on the value axis, such as a norm. Each `{ value, label?, color? }`; the label goes through the host's translate, the colour defaults to the theme's error colour.", "items": { "type": "object", "required": ["value"], "properties": { "value": { "type": "number" }, "label": { "type": "string" }, "color": { "type": "string" } } } },
2299
+ "categoryFormat": { "type": "string", "enum": ["", "month", "month-year"], "description": "How date categories read on the x-axis: `month` (\"jan\") or `month-year` (\"jan 2026\"), in the user's language. Categories that are not dates are left as is." },
2271
2300
  "emptyCellLabel": { "type": "string", "description": "chartKind 'matrix': the text of a cell without a value (\"geen\"), through the host's translate. Empty leaves the cell blank. Passed through to CnChartWidget." },
2272
2301
  "highlight": {
2273
2302
  "type": "object",
@@ -3247,9 +3276,9 @@
3247
3276
  },
3248
3277
  "op": {
3249
3278
  "type": "string",
3250
- "enum": ["eq", "neq", "gt", "gte", "lt", "lte", "empty", "notEmpty", "in", "anyOf"],
3279
+ "enum": ["eq", "neq", "gt", "gte", "lt", "lte", "empty", "notEmpty", "in", "anyOf", "contains"],
3251
3280
  "default": "eq",
3252
- "description": "Comparison operator (default eq). eq/neq compare loosely-normalised primitives; ordering operators coerce both sides to Number. `in` holds when the value is one of the listed `value` array (2.87.0). `anyOf` holds when the value, or any entry of a list value, is one of the listed `value` array: a record whose `dataCategories` holds \"health\" among others matches `anyOf [\"health\", \"ethnicity\"]` (2.126.0). `empty` / `notEmpty` take no `value` and treat undefined, null, '' and an empty array alike — the one question eq cannot ask, because an unset field arrives in a shape that depends on the store and String(null) is the word 'null' rather than blank."
3281
+ "description": "Comparison operator (default eq). eq/neq compare loosely-normalised primitives; ordering operators coerce both sides to Number. `in` holds when the value is one of the listed `value` array (2.87.0). `anyOf` holds when the value, or any entry of a list value, is one of the listed `value` array: a record whose `dataCategories` holds \"health\" among others matches `anyOf [\"health\", \"ethnicity\"]` (2.126.0). `contains` holds when a list value holds the given `value`, e.g. a case type's overviewCards switching a card on (2.128.0). `empty` / `notEmpty` take no `value` and treat undefined, null, '' and an empty array alike — the one question eq cannot ask, because an unset field arrives in a shape that depends on the store and String(null) is the word 'null' rather than blank."
3253
3282
  },
3254
3283
  "value": {
3255
3284
  "description": "The right-hand side of the comparison, resolved through the shared @-token grammar so `@me` names the signed-in reader rather than a literal only the server knows. Omitted for `empty` / `notEmpty`."
@@ -4052,6 +4081,8 @@
4052
4081
  "additionalProperties": true,
4053
4082
  "description": "type='index': the opt-in folder navigation pane (CnFolderSidebar) rendered left of the list. Selecting a folder filters the list by `filterField` (or `field`); \"All\" clears it. Only the per-scope list-layout keys are typed here; the pane's own keys (source, register, schema, idField, nameField, folders, allLabel, title, allowCreate, ...) stay free-form. Maps to CnIndexPage `folderSidebar`.",
4054
4083
  "properties": {
4084
+ "heading": { "type": "boolean", "description": "true heads the list with the selected folder's name, its parent folder and its count (always on a `groups` pane, 2.127.0)." },
4085
+ "countText": { "type": "string", "minLength": 1, "description": "The count in that heading, `{count}` filled in, through the app's translate; a groups pane defaults to \"{count} members\" (2.127.0)." },
4055
4086
  "pane": { "type": "boolean", "default": true, "description": "false keeps the folder scope (a deep link such as `?caseType=<uuid>` still applies the scope's look and filter) without drawing the pane (dossiq's simple profile, 2.126.0)." },
4056
4087
  "folders": {
4057
4088
  "type": "array",
@@ -4215,6 +4246,16 @@
4215
4246
  ]
4216
4247
  }
4217
4248
  },
4249
+ "tabsFrom": {
4250
+ "type": "string",
4251
+ "minLength": 1,
4252
+ "description": "type='detail': a field path, read like a `visibleWhen` field, whose value picks the tabs of the page's tabs widgets and their order for this record, e.g. `@context.caseType.tabs` (DqAanbesteding). A non-empty list of tab ids keeps those tabs in that order; otherwise the declared tabs stay, and edit mode shows them all (2.128.0)."
4253
+ },
4254
+ "sideColumnFrom": {
4255
+ "type": "string",
4256
+ "minLength": 1,
4257
+ "description": "type='detail': a field path, read like a `visibleWhen` field, whose value picks the side column and its order for this record, e.g. `@context.caseType.sideCards` (a case type that names the cards its cases show). When the value is a non-empty list of widget ids, the side column is those of `sideColumn` in that order and the rest are left out; each still follows its own `visibleWhen`. Any other value, or no value, keeps `sideColumn` as declared. Maps to CnDetailPage `sideColumnFrom`."
4258
+ },
4218
4259
  "headerActions": {
4219
4260
  "type": "array",
4220
4261
  "description": "Page-level header actions rendered inside CnActionsBar's overflow dropdown (between the built-in Refresh action and the `#action-items` slot). On a `type:'detail'` page the same entries render in the header's Actions menu, above the Request-a-feature / Report-a-bug / Documentation trio. Same shape as the row-level `actions[]` ($ref action), but handlers receive NO row context — they are page-level. Reserved ids (`refresh`, `import`, `export`, `copy`, `delete`) are dropped at render time to avoid shadowing built-ins. See manifest-icons-and-page-actions for the dispatch semantics.",
@@ -4714,10 +4755,24 @@
4714
4755
  "variant": { "type": "string", "enum": ["primary", "secondary"], "default": "secondary" },
4715
4756
  "icon": { "type": "string", "minLength": 1, "description": "MDI icon name." },
4716
4757
  "format": { "type": "string", "enum": ["csv", "excel"], "default": "csv", "description": "For `export`." },
4758
+ "run": { "type": "string", "minLength": 1, "description": "For `export`: the id of a `headerActions` entry the button runs instead of the export dialog. On a named or endpoint source an Export without `run` (and without a mass-export listener) is not drawn (2.127.0)." },
4717
4759
  "items": { "type": "array", "minItems": 1, "items": { "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "object", "additionalProperties": false, "required": ["heading"], "properties": { "heading": { "type": "string", "minLength": 1, "description": "i18n key of a group heading." } } } ] }, "description": "For `actions-menu`: the menu's entries, in order. Each is `header-actions` (every header action), the id of one `headerActions` entry, or a built-in of the actions bar's own menu, which the bar drops while header buttons show: `import`, `export`, `refresh`, `copy-selected`, `delete-selected` (disabled until a row is selected) or `documentation` (a link to the page's documentationUrl, left out without one). An unknown name is left out. Absent: the header actions only (screens-index-header-actions-menu, 2.90.0). Entries can be grouped: `{ \"heading\": \"<i18n key>\" }` draws a group heading and `\"separator\"` a rule, each only when an entry follows it, so a group whose entries are all left out leaves no empty heading (screens-index-header-menu-headings, 2.93.0)." }
4718
4760
  }
4719
4761
  }
4720
4762
  },
4763
+ "search": { "type": "boolean", "default": true, "description": "type='index': `false` draws no inline search in the actions bar, also in the board look, which otherwise draws one on every index page. Wins over `inlineSearch` (2.127.0)." },
4764
+ "searchPlacement": { "type": "string", "enum": ["", "toolbar"], "default": "", "description": "type='index', board look: where the search field sits. `toolbar` puts it on row 1 before the Filter button and the view switch instead of opening row 2 (DqTeamwachtrij, 2.127.0)." },
4765
+ "defaultOrder": { "description": "type='index', self-fetch: the order the page opens on when neither the address nor sortKeys gives one: `\"-createdAt\"`, `\"priority:desc,title\"`, an `_order` map or a `[{ key, order }]` list (2.127.0).", "anyOf": [ { "type": "string", "minLength": 1 }, { "type": "object", "additionalProperties": { "type": "string", "enum": ["asc", "desc", "ASC", "DESC"] } }, { "type": "array", "items": { "type": "object", "required": ["key"], "properties": { "key": { "type": "string", "minLength": 1 }, "order": { "type": "string", "enum": ["asc", "desc"] } } } } ] },
4766
+ "listConfig": {
4767
+ "type": "object",
4768
+ "additionalProperties": true,
4769
+ "description": "type='index': the field mapping of the list view's rows (CnObjectRow): titleField, subtitleField, imageField, iconField, iconName, badgeField and the badge keys stay free-form. 2.127.0 types the row cells and the meta template (DqTeamwachtrij).",
4770
+ "properties": {
4771
+ "subtitleTemplate": { "type": "string", "minLength": 1, "description": "The meta line: `{field}` tokens filled from the row; empty parts and their separators drop." },
4772
+ "leading": {"type": "object", "required": ["key"], "additionalProperties": true, "properties": {"key": {"type": "string", "minLength": 1}, "widget": {"type": "string", "minLength": 1}, "widgetProps": {"type": "object", "additionalProperties": true}, "formatter": {"type": "string", "minLength": 1}, "width": {"type": "string", "minLength": 1, "description": "CSS width, lining the column up across rows."}}, "description": "One cell before the title, drawn as a table column draws it (`{ key: \"createdAt\", widget: \"age\" }`)."},
4773
+ "trailing": { "description": "A cell, or a list of cells, after the badges, each drawn as a table column draws it.", "anyOf": [ {"type": "object", "required": ["key"], "additionalProperties": true, "properties": {"key": {"type": "string", "minLength": 1}, "widget": {"type": "string", "minLength": 1}, "widgetProps": {"type": "object", "additionalProperties": true}, "formatter": {"type": "string", "minLength": 1}, "width": {"type": "string", "minLength": 1, "description": "CSS width, lining the column up across rows."}}}, { "type": "array", "items": {"type": "object", "required": ["key"], "additionalProperties": true, "properties": {"key": {"type": "string", "minLength": 1}, "widget": {"type": "string", "minLength": 1}, "widgetProps": {"type": "object", "additionalProperties": true}, "formatter": {"type": "string", "minLength": 1}, "width": {"type": "string", "minLength": 1, "description": "CSS width, lining the column up across rows."}}} } ] }
4774
+ }
4775
+ },
4721
4776
  "rowTitle": {
4722
4777
  "type": "string",
4723
4778
  "enum": ["link", "plain"],
@@ -58,6 +58,7 @@ export const BUILT_IN_WIDGET_KEYS = Object.freeze([
58
58
  'object-table',
59
59
  'form-renderer',
60
60
  'map-viewer',
61
+ 'notice',
61
62
  'object-geo',
62
63
  'card-grid',
63
64
  'nav-card-grid',
@@ -0,0 +1,50 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+
6
+ import { generateOcsUrl } from '@nextcloud/router'
7
+
8
+ /**
9
+ * The display names of the apps in Nextcloud's navigation, by app id, read
10
+ * once per page load from the OCS navigation endpoint with the reader's
11
+ * session. An app outside the navigation, or a failed read, has no entry.
12
+ *
13
+ * @module utils/nextcloudApps
14
+ */
15
+
16
+ let pending = null
17
+
18
+ /**
19
+ * The app display names by app id.
20
+ *
21
+ * @param {object} axios The axios instance.
22
+ * @return {Promise<Record<string, string>>} The names; empty when the read fails.
23
+ */
24
+ export function appDisplayNames(axios) {
25
+ if (!pending) {
26
+ pending = axios.get(generateOcsUrl('core/navigation/apps'), { params: { format: 'json' }, headers: { 'OCS-APIRequest': 'true' } })
27
+ .then((res) => {
28
+ const entries = (res && res.data && res.data.ocs && res.data.ocs.data) || []
29
+ const names = {}
30
+ for (const entry of Array.isArray(entries) ? entries : Object.values(entries)) {
31
+ const id = entry && (entry.app || entry.id)
32
+ if (id && typeof entry.name === 'string' && entry.name !== '') {
33
+ names[String(id)] = entry.name
34
+ }
35
+ }
36
+ return names
37
+ })
38
+ .catch(() => ({}))
39
+ }
40
+ return pending
41
+ }
42
+
43
+ /**
44
+ * Forget the names read, so the next call reads them again.
45
+ *
46
+ * @return {void}
47
+ */
48
+ export function clearAppDisplayNames() {
49
+ pending = null
50
+ }
@@ -64,6 +64,23 @@ export async function fetchGroupMembers(axios, groupId) {
64
64
  return Array.isArray(users) ? users : Object.values(users)
65
65
  }
66
66
 
67
+ /**
68
+ * Every user the reader may list, with their details: all users for an
69
+ * admin, the users of their groups for a sub-admin. Refused for others.
70
+ *
71
+ * @param {object} axios The axios instance.
72
+ * @param {{limit?: number}} [options] How many to read.
73
+ * @return {Promise<Array<object>>} Nextcloud's user details, one per user.
74
+ */
75
+ export async function fetchUsers(axios, options = {}) {
76
+ const res = await axios.get(generateOcsUrl('cloud/users/details'), {
77
+ params: { limit: options.limit || 500, format: 'json' },
78
+ headers: OCS_HEADERS,
79
+ })
80
+ const users = (ocsData(res) && ocsData(res).users) || {}
81
+ return Array.isArray(users) ? users : Object.values(users)
82
+ }
83
+
67
84
  /**
68
85
  * Whether a user is in today: an out-of-office period that runs now (the
69
86
  * dav app's absence, Nextcloud 28 and later), else their user status.
@@ -153,8 +153,15 @@ export function resolveScopeLayout({ scope = null, row = null, pageColumns = []
153
153
  return
154
154
  }
155
155
  // The page's own entry is used, not the scope's, so the label and
156
- // formatter a column was declared with survive being selected.
157
- columns.push(byKey.get(key))
156
+ // formatter a column was declared with survive being selected. A
157
+ // column the page hides by default shows: the scope asked for it.
158
+ const own = byKey.get(key)
159
+ if (own && typeof own === 'object' && own.hidden === true) {
160
+ const { hidden, ...shown } = own
161
+ columns.push(shown)
162
+ return
163
+ }
164
+ columns.push(own)
158
165
  })
159
166
 
160
167
  return { columns, sortKeys, searchFields }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+
6
+ /**
7
+ * Read an order a manifest writes into the `[{ key, order }]` list a self-fetch
8
+ * index page sorts by. Accepted forms:
9
+ *
10
+ * - `"title"` ascending, `"-createdAt"` descending, `"priority:desc"`;
11
+ * - several of those, comma separated, in priority order (`"-priority,title"`);
12
+ * - an OpenRegister `_order` map, `{ "priority": "desc", "title": "asc" }`;
13
+ * - a `[{ key, order? }]` list.
14
+ *
15
+ * @param {string|object|Array|null|undefined} value The order.
16
+ * @return {Array<{key: string, order: 'asc'|'desc'}>} The sort keys, possibly empty.
17
+ */
18
+ export function orderToSortKeys(value) {
19
+ const direction = (order) => (String(order || '').toLowerCase() === 'desc' ? 'desc' : 'asc')
20
+ if (Array.isArray(value)) {
21
+ return value
22
+ .filter((entry) => entry && typeof entry.key === 'string' && entry.key !== '')
23
+ .map((entry) => ({ key: entry.key, order: direction(entry.order) }))
24
+ }
25
+ if (value && typeof value === 'object') {
26
+ return Object.entries(value)
27
+ .filter(([key]) => key !== '')
28
+ .map(([key, order]) => ({ key, order: direction(order) }))
29
+ }
30
+ if (typeof value !== 'string') {
31
+ return []
32
+ }
33
+ return value.split(',')
34
+ .map((part) => part.trim())
35
+ .filter(Boolean)
36
+ .map((part) => {
37
+ if (part.startsWith('-')) {
38
+ return { key: part.slice(1), order: 'desc' }
39
+ }
40
+ const [key, order] = part.split(':')
41
+ return { key, order: direction(order) }
42
+ })
43
+ .filter((entry) => entry.key !== '')
44
+ }
45
+
46
+ /**
47
+ * Whether two sort key lists order the same way.
48
+ *
49
+ * @param {Array<{key: string, order: string}>} a One list.
50
+ * @param {Array<{key: string, order: string}>} b The other.
51
+ * @return {boolean} True when they name the same keys, directions and priority.
52
+ */
53
+ export function sameSortKeys(a, b) {
54
+ const left = Array.isArray(a) ? a : []
55
+ const right = Array.isArray(b) ? b : []
56
+ return left.length === right.length
57
+ && left.every((entry, index) => entry.key === right[index].key && (entry.order || 'asc') === (right[index].order || 'asc'))
58
+ }