@conduction/nextcloud-vue 2.98.0 → 2.100.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 (141) hide show
  1. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js +12 -0
  2. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +11 -2
  4. package/dist/esm/components/CnActionsMenu/CnActionsMenu.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 +6 -1
  7. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +2 -1
  9. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  10. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue.js +1 -1
  11. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
  12. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +1 -1
  13. package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
  14. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +25 -8
  15. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +7 -4
  17. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  19. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +11 -8
  20. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +13 -4
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  23. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  24. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +54 -3
  25. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  26. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +16 -2
  27. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  28. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue.js +1 -1
  29. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js +42 -7
  30. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js.map +1 -1
  31. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js +121 -57
  32. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js +10 -0
  34. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js.map +1 -1
  35. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js +2 -1
  36. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js.map +1 -1
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +136 -7
  38. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  39. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  40. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +1 -1
  41. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +26 -7
  42. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -1
  43. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +78 -17
  44. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -1
  45. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  46. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +68 -7
  47. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  48. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +256 -240
  49. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  50. package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +9 -0
  51. package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js.map +1 -0
  52. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js +265 -0
  53. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -0
  54. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +191 -0
  55. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -0
  56. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js +9 -0
  57. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js.map +1 -0
  58. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js +537 -0
  59. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js.map +1 -0
  60. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js +99 -0
  61. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js.map +1 -0
  62. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  63. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +18 -70
  64. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  65. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js +6 -44
  66. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  67. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  68. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +16 -2
  69. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  70. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +40 -25
  71. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  72. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  73. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  74. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
  75. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +7 -7
  76. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  77. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  78. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue.js +9 -0
  79. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue.js.map +1 -0
  80. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue2.js +472 -0
  81. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue2.js.map +1 -0
  82. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue3.js +231 -0
  83. package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue3.js.map +1 -0
  84. package/dist/esm/components/CnWorkingCalendarWidget/index.js +27 -0
  85. package/dist/esm/components/CnWorkingCalendarWidget/index.js.map +1 -0
  86. package/dist/esm/composables/useFlowStore.js +2 -1
  87. package/dist/esm/composables/useFlowStore.js.map +1 -1
  88. package/dist/esm/index.js +3 -0
  89. package/dist/esm/index.js.map +1 -1
  90. package/dist/esm/l10n/en.json.js +101 -1
  91. package/dist/esm/l10n/en.json.js.map +1 -1
  92. package/dist/esm/l10n/nl.json.js +115 -1
  93. package/dist/esm/l10n/nl.json.js.map +1 -1
  94. package/dist/esm/library-style.js +1 -1
  95. package/dist/esm/nextcloud-vue.css +999 -233
  96. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  97. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  98. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  99. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  100. package/dist/esm/utils/workingCalendarMonth.js +85 -0
  101. package/dist/esm/utils/workingCalendarMonth.js.map +1 -0
  102. package/dist/nextcloud-vue.cjs.js +10314 -7778
  103. package/dist/nextcloud-vue.cjs.js.map +2 -2
  104. package/dist/nextcloud-vue.css +999 -233
  105. package/l10n/en.json +101 -1
  106. package/l10n/nl.json +115 -1
  107. package/package.json +1 -1
  108. package/src/components/CnActionsMenu/CnActionsMenu.vue +14 -1
  109. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +7 -1
  110. package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +1 -0
  111. package/src/components/CnDetailPage/CnDetailPage.vue +29 -9
  112. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +13 -4
  113. package/src/components/CnFactsWidget/CnFactsWidget.vue +65 -3
  114. package/src/components/CnFlowsPage/CnFlowBoard.vue +77 -2
  115. package/src/components/CnFlowsPage/CnFlowEditorPage.vue +11 -0
  116. package/src/components/CnIndexPage/CnIndexPage.vue +136 -7
  117. package/src/components/CnOrderedListField/CnOrderedListField.vue +81 -9
  118. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +114 -7
  119. package/src/components/CnSettingsField/CnSettingsField.vue +449 -0
  120. package/src/components/CnSettingsField/index.js +4 -0
  121. package/src/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue +642 -0
  122. package/src/components/CnSettingsFieldsSection/index.js +4 -0
  123. package/src/components/CnSettingsPage/CnSettingsPage.vue +6 -86
  124. package/src/components/CnSetupWizard/CnSetupWizard.vue +37 -3
  125. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  126. package/src/components/CnWizardDialog/CnWizardDialog.vue +7 -7
  127. package/src/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue +725 -0
  128. package/src/components/CnWorkingCalendarWidget/index.js +26 -0
  129. package/src/components/index.js +3 -0
  130. package/src/composables/useFlowStore.js +2 -1
  131. package/src/css/badge.css +32 -16
  132. package/src/css/detail-page.css +15 -0
  133. package/src/css/dialog.css +48 -0
  134. package/src/css/look-board-detail.css +80 -8
  135. package/src/css/look-board-index.css +52 -8
  136. package/src/css/look-board.css +301 -1
  137. package/src/index.js +3 -0
  138. package/src/schemas/app-manifest-v2.schema.json +25 -1
  139. package/src/utils/libraryWidgetKeys.js +1 -0
  140. package/src/utils/validateManifestV2.compiled.js +3 -3
  141. package/src/utils/workingCalendarMonth.js +82 -0
@@ -39,6 +39,15 @@
39
39
  @click="$emit('run')">
40
40
  {{ t('nextcloud-vue', 'Run') }}
41
41
  </NcButton>
42
+ <NcButton v-if="testSubjectReady"
43
+ :disabled="store.running || !store.flow.id"
44
+ data-testid="flow-board-test"
45
+ @click="testOpen = true">
46
+ <template #icon>
47
+ <TestTube :size="16" />
48
+ </template>
49
+ {{ testLabel }}
50
+ </NcButton>
42
51
  <CnBuildiqEditButton />
43
52
  <NcButton variant="primary"
44
53
  :disabled="store.saving || !store.flow.name || store.graphLocked"
@@ -49,6 +58,32 @@
49
58
  </div>
50
59
  </header>
51
60
 
61
+ <CnDialog v-if="testOpen"
62
+ :name="testLabel"
63
+ data-testid="flow-board-test-dialog"
64
+ @closing="testOpen = false">
65
+ <p class="cn-flow-board__test-intro">
66
+ {{ t('nextcloud-vue', 'The saved flow runs once with the record you pick as its subject.') }}
67
+ </p>
68
+ <CnResourceSelect v-model="testRecord"
69
+ :register="testSubject.register"
70
+ :schema="testSubject.schema"
71
+ :labelField="testSubject.labelField || 'title'"
72
+ :inputLabel="t('nextcloud-vue', 'Record')"
73
+ data-testid="flow-board-test-record" />
74
+ <template #actions>
75
+ <NcButton data-testid="flow-board-test-cancel" @click="testOpen = false">
76
+ {{ t('nextcloud-vue', 'Cancel') }}
77
+ </NcButton>
78
+ <NcButton variant="primary"
79
+ :disabled="!testRecord || store.running"
80
+ data-testid="flow-board-test-run"
81
+ @click="runTest">
82
+ {{ t('nextcloud-vue', 'Run test') }}
83
+ </NcButton>
84
+ </template>
85
+ </CnDialog>
86
+
52
87
  <p class="cn-flow-board__path" data-testid="flow-board-path">
53
88
  <a :href="listHref" class="cn-flow-board__crumb" @click="onCrumbClick($event, detailRoute)">{{ listText }}</a>
54
89
  <span aria-hidden="true"> / </span>
@@ -186,11 +221,14 @@
186
221
  <script>
187
222
  import { translate as t } from '@nextcloud/l10n'
188
223
  import { NcButton, NcCheckboxRadioSwitch, NcNoteCard } from '@nextcloud/vue'
224
+ import TestTube from 'vue-material-design-icons/TestTube.vue'
189
225
  import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
226
+ import CnDialog from '../CnDialog/CnDialog.vue'
190
227
  import CnFlowDetail from '../CnFlowDetail/CnFlowDetail.vue'
191
228
  import CnFlowRunSummary from '../CnFlowDetail/CnFlowRunSummary.vue'
192
229
  import CnFlowSidebar from '../CnFlowDetail/CnFlowSidebar.vue'
193
230
  import CnFlowStepLog from '../CnFlowDetail/CnFlowStepLog.vue'
231
+ import CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'
194
232
  import { FLOW_RUN_ACTIVE_STATUSES, useFlowStore } from '../../composables/useFlowStore.js'
195
233
  import { formatRelativeDate } from '../../utils/boardDate.js'
196
234
  import { flowOutcomeLevels, flowRunDurationMs, flowRunLogLines, flowRunName, formatFlowDuration } from '../../utils/flowRunLog.js'
@@ -204,7 +242,7 @@ import { CnStatusBadge } from '../CnStatusBadge/index.js'
204
242
  export default {
205
243
  name: 'CnFlowBoard',
206
244
 
207
- components: { CnBuildiqEditButton, CnFlowDetail, CnFlowRunSummary, CnFlowSidebar, CnFlowStepLog, CnStatusBadge, NcButton, NcCheckboxRadioSwitch, NcNoteCard },
245
+ components: { CnBuildiqEditButton, CnDialog, CnFlowDetail, CnResourceSelect, TestTube, CnFlowRunSummary, CnFlowSidebar, CnFlowStepLog, CnStatusBadge, NcButton, NcCheckboxRadioSwitch, NcNoteCard },
208
246
 
209
247
  props: {
210
248
  /** The flow uuid, or `new`. */
@@ -225,6 +263,12 @@ export default {
225
263
  notice: { type: String, default: '' },
226
264
  /** The pill beside the title ("Case type flow"). */
227
265
  pill: { type: String, default: '' },
266
+ /**
267
+ * The records a test run can take as its subject: `{ register, schema,
268
+ * labelField?, label? }`. Draws a header button ("Testen met een zaak",
269
+ * DqFlow) that runs the saved flow once with a picked record.
270
+ */
271
+ testSubject: { type: Object, default: null },
228
272
  },
229
273
 
230
274
  emits: ['save', 'run', 'retried'],
@@ -234,10 +278,20 @@ export default {
234
278
  },
235
279
 
236
280
  data() {
237
- return { runTab: 'step' }
281
+ return { runTab: 'step', testOpen: false, testRecord: null }
238
282
  },
239
283
 
240
284
  computed: {
285
+ /** @return {boolean} Whether a test subject is declared well enough to pick one. */
286
+ testSubjectReady() {
287
+ return Boolean(this.testSubject && this.testSubject.register && this.testSubject.schema)
288
+ },
289
+
290
+ /** @return {string} The test button's label: the page's own, else the library's. */
291
+ testLabel() {
292
+ return (this.testSubject && this.testSubject.label) || t('nextcloud-vue', 'Test with a record')
293
+ },
294
+
241
295
  /** @return {boolean} Whether the palette card is drawn. */
242
296
  showPalette() {
243
297
  return this.palette === 'column' && !this.store.inRunView
@@ -454,6 +508,22 @@ export default {
454
508
  },
455
509
 
456
510
  methods: {
511
+ /**
512
+ * Run the saved flow once with the picked record as its subject, then
513
+ * close the dialog; the run shows on the canvas as any run does.
514
+ *
515
+ * @return {Promise<void>}
516
+ */
517
+ async runTest() {
518
+ const uuid = Array.isArray(this.testRecord) ? this.testRecord[0] : this.testRecord
519
+ if (!uuid) {
520
+ return
521
+ }
522
+ await this.store.run({ uuid, register: this.testSubject.register, schema: this.testSubject.schema })
523
+ this.testOpen = false
524
+ this.testRecord = null
525
+ },
526
+
457
527
  t,
458
528
 
459
529
  /**
@@ -581,6 +651,11 @@ export default {
581
651
  gap: 8px;
582
652
  }
583
653
 
654
+ .cn-flow-board__test-intro {
655
+ margin: 0 0 12px;
656
+ color: var(--color-text-maxcontrast);
657
+ }
658
+
584
659
  .cn-flow-board__path {
585
660
  margin: 0;
586
661
  color: var(--color-text-maxcontrast);
@@ -11,6 +11,7 @@
11
11
  :paletteFootnote="paletteFootnote ? cnTranslate(paletteFootnote) : ''"
12
12
  :notice="notice ? cnTranslate(notice) : ''"
13
13
  :pill="pill ? cnTranslate(pill) : ''"
14
+ :testSubject="testSubject ? { ...testSubject, label: testSubject.label ? cnTranslate(testSubject.label) : '' } : null"
14
15
  @save="onSave"
15
16
  @run="onRun" />
16
17
  <CnFlowDetail v-else
@@ -122,6 +123,16 @@ export default {
122
123
  default: '',
123
124
  },
124
125
 
126
+ /**
127
+ * Board look: the records a test run can take as its subject,
128
+ * `{ register, schema, labelField?, label? }`. Manifest key
129
+ * `config.testSubject`; `label` is translated with the app's catalogue.
130
+ */
131
+ testSubject: {
132
+ type: Object,
133
+ default: null,
134
+ },
135
+
125
136
  /**
126
137
  * Board look: an info notice under the path. Manifest key
127
138
  * `config.notice`, translated with the app's catalogue.
@@ -1578,6 +1578,8 @@ export default {
1578
1578
  */
1579
1579
  inject: {
1580
1580
  cnCustomComponents: { default: () => ({}) },
1581
+ /** The permissions the user holds, provided by CnAppRoot; for gated page tabs. */
1582
+ cnPermissions: { default: () => [] },
1581
1583
  /** The app id CnAppRoot provides, for the plural form of `countSubtitle`. */
1582
1584
  cnAppId: { default: '' },
1583
1585
  /**
@@ -1668,7 +1670,9 @@ export default {
1668
1670
 
1669
1671
  /**
1670
1672
  * A note after the breadcrumb and a middle dot (manifest `config.headerMeta`),
1671
- * such as "{total} open": `{total}` and `{shown}` are the list's counts.
1673
+ * such as "{total} open": `{total}` and `{shown}` are the list's counts,
1674
+ * and `{count:<name>}` the count of a list named in `headerMetaCounts`
1675
+ * ("{total} contracten bij {count:suppliers} leveranciers").
1672
1676
  *
1673
1677
  * @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
1674
1678
  */
@@ -1677,12 +1681,29 @@ export default {
1677
1681
  default: '',
1678
1682
  },
1679
1683
 
1684
+ /**
1685
+ * The other lists `headerMeta` counts (manifest `config.headerMetaCounts`):
1686
+ * `{ <name>: { register, schema, filter? } }`, each read as one list
1687
+ * total and filled in for `{count:<name>}`. While a count loads, or when
1688
+ * it fails, the note is left out rather than shown with a gap.
1689
+ *
1690
+ * @type {Record<string, {register: string, schema: string, filter?: object}>|null}
1691
+ */
1692
+ headerMetaCounts: {
1693
+ type: Object,
1694
+ default: null,
1695
+ },
1696
+
1680
1697
  /**
1681
1698
  * Tabs to sibling pages above the toolbar (manifest `config.pageTabs`),
1682
1699
  * each `{ id?, label, route?, params?, query?, href?, countFrom?: {
1683
- * register, schema, filter? } }`: DqPrivacy's "AVG-verzoeken |
1684
- * Datalekken". The current page's tab is marked; `countFrom` adds the
1685
- * count of that list. Wins over a named source's own page tabs.
1700
+ * register, schema, filter? }, adminOnly?, permission? }`: DqPrivacy's
1701
+ * "AVG-verzoeken | Datalekken". The current page's tab is marked;
1702
+ * `countFrom` adds the count of that list. A tab with `adminOnly` shows
1703
+ * only to an instance admin, one with `permission` only to a user who
1704
+ * holds it. That hides the tab and nothing more: the page it points to
1705
+ * must still refuse the user on the server. Wins over a named source's
1706
+ * own page tabs.
1686
1707
  *
1687
1708
  * @type {Array<object>|null}
1688
1709
  * @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs
@@ -3722,6 +3743,8 @@ export default {
3722
3743
  filterStripCounts: null,
3723
3744
  /** The `countFrom` counts of the manifest page tabs, by index. */
3724
3745
  pageTabCounts: {},
3746
+ /** The `headerMetaCounts` totals, by name; a failed one is null. */
3747
+ headerMetaCountValues: {},
3725
3748
  /** The tabs `quickFiltersFrom` produced, or null before they load. */
3726
3749
  generatedQuickFilters: null,
3727
3750
  /** Count per saved-view id, for the views control; null = none. */
@@ -6202,9 +6225,13 @@ export default {
6202
6225
  if (!Array.isArray(this.pageTabs) || this.pageTabs.length === 0) {
6203
6226
  return this.sourcePageTabs
6204
6227
  }
6228
+ // The index is the tab's place in `pageTabs`, which is how its count
6229
+ // is keyed, so the tabs are numbered before any is left out.
6205
6230
  return this.pageTabs
6206
- .filter((tab) => tab && typeof tab.label === 'string' && tab.label !== '' && (tab.route || tab.to || tab.href))
6207
- .map((tab, index) => {
6231
+ .map((tab, index) => ({ tab, index }))
6232
+ .filter(({ tab }) => tab && typeof tab.label === 'string' && tab.label !== '' && (tab.route || tab.to || tab.href))
6233
+ .filter(({ tab }) => this.pageTabVisible(tab))
6234
+ .map(({ tab, index }) => {
6208
6235
  const to = tab.route
6209
6236
  ? { name: tab.route, ...(tab.params ? { params: tab.params } : {}), ...(tab.query ? { query: tab.query } : {}) }
6210
6237
  : (tab.to || tab.href)
@@ -6646,9 +6673,41 @@ export default {
6646
6673
  return ''
6647
6674
  }
6648
6675
  const total = this.effectivePagination?.total
6649
- return this.cnTranslate(headerMeta)
6676
+ let missing = false
6677
+ const text = this.cnTranslate(headerMeta)
6650
6678
  .replace('{shown}', String(this.effectiveObjects.length))
6651
6679
  .replace('{total}', typeof total === 'number' ? String(total) : '')
6680
+ .replace(/\{count:([\w-]+)\}/g, (match, name) => {
6681
+ const value = this.headerMetaCountValues[name]
6682
+ if (typeof value !== 'number') {
6683
+ missing = true
6684
+ return ''
6685
+ }
6686
+ return String(value)
6687
+ })
6688
+ return missing ? '' : text
6689
+ },
6690
+
6691
+ /**
6692
+ * Whether the header note carries a count, so the board leaves out its
6693
+ * own "{shown} of {total}" line (DqPrivacy, DqContracten).
6694
+ *
6695
+ * @return {boolean}
6696
+ */
6697
+ headerMetaCarriesCount() {
6698
+ const headerMeta = this.presented('headerMeta')
6699
+ return typeof headerMeta === 'string' && /\{(shown|total|count:[\w-]+)\}/.test(headerMeta)
6700
+ },
6701
+
6702
+ /**
6703
+ * What the header note's other counts depend on, as one key.
6704
+ *
6705
+ * @return {string}
6706
+ */
6707
+ headerMetaCountKey() {
6708
+ const sources = this.headerMetaCounts && typeof this.headerMetaCounts === 'object' ? this.headerMetaCounts : {}
6709
+ const valid = Object.entries(sources).filter(([, from]) => from && from.register && from.schema)
6710
+ return valid.length > 0 ? JSON.stringify(valid) : ''
6652
6711
  },
6653
6712
 
6654
6713
  headerDescription() {
@@ -6661,6 +6720,10 @@ export default {
6661
6720
  // reads "14 van 14" in Dutch; a page's own template goes
6662
6721
  // through the app's label lookup.
6663
6722
  const countText = this.presented('countText')
6723
+ // The note after the breadcrumb already says how many there are.
6724
+ if (!countText && !this.countSubtitle && this.headerMetaCarriesCount) {
6725
+ return this.description
6726
+ }
6664
6727
  if (!countText && !this.countSubtitle) {
6665
6728
  return t('nextcloud-vue', '{shown} of {total}', { shown, total })
6666
6729
  }
@@ -6907,6 +6970,13 @@ export default {
6907
6970
  },
6908
6971
  },
6909
6972
 
6973
+ headerMetaCountKey: {
6974
+ immediate: true,
6975
+ handler() {
6976
+ this.loadHeaderMetaCounts()
6977
+ },
6978
+ },
6979
+
6910
6980
  pageTabCountKey: {
6911
6981
  immediate: true,
6912
6982
  /** @spec openspec/changes/index-page-tabs-notice/specs/index-page/spec.md#requirement-an-index-page-can-link-its-sibling-pages-as-tabs */
@@ -7326,6 +7396,65 @@ export default {
7326
7396
  ]
7327
7397
  },
7328
7398
 
7399
+ /**
7400
+ * Whether Nextcloud says the user administers this instance; false
7401
+ * when the user cannot be read.
7402
+ *
7403
+ * @return {boolean}
7404
+ */
7405
+ viewerIsAdmin() {
7406
+ try {
7407
+ return getCurrentUser()?.isAdmin === true
7408
+ } catch {
7409
+ return false
7410
+ }
7411
+ },
7412
+
7413
+ /**
7414
+ * Whether a manifest page tab shows to this user: an `adminOnly` tab to
7415
+ * an instance admin, a tab with a `permission` to a user who holds it
7416
+ * (as a menu entry: with no permissions known, it shows). This only
7417
+ * decides whether the tab is drawn. It is not access control; the
7418
+ * page behind the tab must enforce its own access on the server.
7419
+ *
7420
+ * @param {object} tab The tab declaration.
7421
+ * @return {boolean} True when the tab is drawn.
7422
+ */
7423
+ pageTabVisible(tab) {
7424
+ if (tab.adminOnly === true && !this.viewerIsAdmin()) {
7425
+ return false
7426
+ }
7427
+ if (tab.permission) {
7428
+ const held = Array.isArray(this.cnPermissions) ? this.cnPermissions : []
7429
+ if (held.length > 0 && !held.includes(tab.permission)) {
7430
+ return false
7431
+ }
7432
+ }
7433
+ return true
7434
+ },
7435
+
7436
+ /**
7437
+ * Read the totals of the lists `headerMetaCounts` names, one list
7438
+ * request each. A failed count is null and leaves the note out.
7439
+ *
7440
+ * @return {Promise<void>}
7441
+ */
7442
+ async loadHeaderMetaCounts() {
7443
+ const key = this.headerMetaCountKey
7444
+ if (key === '') {
7445
+ this.headerMetaCountValues = {}
7446
+ return
7447
+ }
7448
+ const ctx = typeof this.selfFetchTokenCtx === 'function' ? this.selfFetchTokenCtx() : {}
7449
+ const values = {}
7450
+ await Promise.all(JSON.parse(key).map(async ([name, from]) => {
7451
+ values[name] = await fetchListTotal(from.register, from.schema, from.filter || {}, ctx).catch(() => null)
7452
+ }))
7453
+ if (key === this.headerMetaCountKey) {
7454
+ this.headerMetaCountValues = values
7455
+ }
7456
+ },
7457
+
7329
7458
  /**
7330
7459
  * Count the lists behind the manifest page tabs (`countFrom`), one list
7331
7460
  * request each. A failed count shows no number.
@@ -19,7 +19,16 @@
19
19
  class="cn-ordered-list-field__row"
20
20
  :data-testid="`cn-ordered-list-row-${row.value}`">
21
21
  <span class="cn-ordered-list-field__position" aria-hidden="true">{{ index + 1 }}</span>
22
- <span class="cn-ordered-list-field__text">{{ row.label }}</span>
22
+ <span v-if="row.icon" class="cn-ordered-list-field__tile" aria-hidden="true">
23
+ <CnIcon :name="row.icon" :size="18" />
24
+ </span>
25
+ <span class="cn-ordered-list-field__text">
26
+ <span class="cn-ordered-list-field__name">{{ row.label }}</span>
27
+ <span v-if="row.description"
28
+ :id="descriptionId(row)"
29
+ class="cn-ordered-list-field__description"
30
+ :data-testid="`cn-ordered-list-description-${row.value}`">{{ row.description }}</span>
31
+ </span>
23
32
  <span class="cn-ordered-list-field__actions">
24
33
  <button
25
34
  type="button"
@@ -27,6 +36,7 @@
27
36
  :disabled="disabled || index === 0"
28
37
  :aria-label="t('nextcloud-vue', 'Move {item} up', { item: row.label })"
29
38
  :data-testid="`cn-ordered-list-up-${row.value}`"
39
+ :aria-describedby="row.description ? descriptionId(row) : null"
30
40
  @click="move(index, index - 1)">
31
41
  <ChevronUp :size="20" />
32
42
  </button>
@@ -36,6 +46,7 @@
36
46
  :disabled="disabled || index === rows.length - 1"
37
47
  :aria-label="t('nextcloud-vue', 'Move {item} down', { item: row.label })"
38
48
  :data-testid="`cn-ordered-list-down-${row.value}`"
49
+ :aria-describedby="row.description ? descriptionId(row) : null"
39
50
  @click="move(index, index + 1)">
40
51
  <ChevronDown :size="20" />
41
52
  </button>
@@ -45,6 +56,7 @@
45
56
  :disabled="disabled"
46
57
  :aria-label="t('nextcloud-vue', 'Remove {item}', { item: row.label })"
47
58
  :data-testid="`cn-ordered-list-remove-${row.value}`"
59
+ :aria-describedby="row.description ? descriptionId(row) : null"
48
60
  @click="remove(index)">
49
61
  <Close :size="20" />
50
62
  </button>
@@ -66,7 +78,19 @@
66
78
  :filterable="!searchable"
67
79
  data-testid="cn-ordered-list-add"
68
80
  @search="onSearch"
69
- @update:modelValue="add" />
81
+ @update:modelValue="add">
82
+ <template #option="option">
83
+ <span class="cn-ordered-list-field__option">
84
+ <span v-if="option.icon" class="cn-ordered-list-field__tile" aria-hidden="true">
85
+ <CnIcon :name="option.icon" :size="18" />
86
+ </span>
87
+ <span class="cn-ordered-list-field__text">
88
+ <span class="cn-ordered-list-field__name">{{ option.label }}</span>
89
+ <span v-if="option.description" class="cn-ordered-list-field__description">{{ option.description }}</span>
90
+ </span>
91
+ </span>
92
+ </template>
93
+ </NcSelect>
70
94
  </div>
71
95
  <p class="hidden-visually" aria-live="polite" data-testid="cn-ordered-list-live">
72
96
  {{ announcement }}
@@ -80,6 +104,7 @@ import { NcSelect } from '@nextcloud/vue'
80
104
  import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
81
105
  import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
82
106
  import Close from 'vue-material-design-icons/Close.vue'
107
+ import CnIcon from '../CnIcon/CnIcon.vue'
83
108
 
84
109
  let orderedListUid = 0
85
110
 
@@ -109,7 +134,7 @@ let orderedListUid = 0
109
134
  export default {
110
135
  name: 'CnOrderedListField',
111
136
 
112
- components: { ChevronDown, ChevronUp, Close, NcSelect },
137
+ components: { ChevronDown, ChevronUp, Close, CnIcon, NcSelect },
113
138
 
114
139
  props: {
115
140
  /** The values in their order. */
@@ -119,8 +144,11 @@ export default {
119
144
  },
120
145
 
121
146
  /**
122
- * The choices, `{ value, label }` (a plain string is its own label).
123
- * A value in `modelValue` without a choice shows the value itself.
147
+ * The choices, `{ value, label, description?, icon? }` (a plain string
148
+ * is its own label). `description` is a muted second line ("19 open
149
+ * cases"), in the row and in the picker; `icon` an MDI name drawn in a
150
+ * small tile before the label. A value in `modelValue` without a
151
+ * choice shows the value itself.
124
152
  */
125
153
  options: {
126
154
  type: Array,
@@ -185,17 +213,22 @@ export default {
185
213
  choices() {
186
214
  return (Array.isArray(this.options) ? this.options : [])
187
215
  .map((option) => (option && typeof option === 'object'
188
- ? { value: String(option.value ?? option.id ?? ''), label: String(option.label ?? option.title ?? option.value ?? '') }
189
- : { value: String(option), label: String(option) }))
216
+ ? {
217
+ value: String(option.value ?? option.id ?? ''),
218
+ label: String(option.label ?? option.title ?? option.value ?? ''),
219
+ description: typeof option.description === 'string' ? option.description : '',
220
+ icon: typeof option.icon === 'string' ? option.icon : '',
221
+ }
222
+ : { value: String(option), label: String(option), description: '', icon: '' }))
190
223
  .filter((option) => option.value !== '')
191
224
  },
192
225
 
193
- /** @return {Array<{value: string, label: string}>} The rows, in order. */
226
+ /** @return {Array<{value: string, label: string, description: string, icon: string}>} The rows, in order. */
194
227
  rows() {
195
228
  return (Array.isArray(this.modelValue) ? this.modelValue : []).map((value) => {
196
229
  const key = String(value)
197
230
  const choice = this.choices.find((option) => option.value === key)
198
- return { value: key, label: choice ? choice.label : key }
231
+ return choice || { value: key, label: key, description: '', icon: '' }
199
232
  })
200
233
  },
201
234
 
@@ -214,6 +247,16 @@ export default {
214
247
  methods: {
215
248
  t,
216
249
 
250
+ /**
251
+ * The id of a row's description, which its buttons name as their description.
252
+ *
253
+ * @param {{value: string}} row The row.
254
+ * @return {string}
255
+ */
256
+ descriptionId(row) {
257
+ return `${this.labelId}-desc-${String(row.value).replace(/[^\w-]/g, '-')}`
258
+ },
259
+
217
260
  /**
218
261
  * Emit the new order.
219
262
  *
@@ -383,4 +426,33 @@ export default {
383
426
  margin: 0;
384
427
  color: var(--color-text-maxcontrast);
385
428
  }
429
+
430
+ .cn-ordered-list-field__tile {
431
+ display: inline-flex;
432
+ flex: none;
433
+ align-items: center;
434
+ justify-content: center;
435
+ width: 32px;
436
+ height: 32px;
437
+ border-radius: var(--border-radius-element, 8px);
438
+ background: var(--color-background-dark);
439
+ color: var(--color-main-text);
440
+ }
441
+
442
+ .cn-ordered-list-field__name {
443
+ display: block;
444
+ }
445
+
446
+ .cn-ordered-list-field__description {
447
+ display: block;
448
+ color: var(--color-text-maxcontrast);
449
+ font-size: 13px;
450
+ }
451
+
452
+ .cn-ordered-list-field__option {
453
+ display: flex;
454
+ align-items: center;
455
+ gap: 10px;
456
+ min-width: 0;
457
+ }
386
458
  </style>