@conduction/nextcloud-vue 2.56.0-beta.36 → 2.56.0-beta.37

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 (136) hide show
  1. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +131 -3
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +54 -18
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  6. package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
  7. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +232 -4
  8. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  9. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +95 -51
  10. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +45 -0
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -12
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  16. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +23 -4
  17. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  20. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +3 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  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 +18 -5
  25. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  26. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  27. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +548 -77
  28. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  29. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +191 -82
  30. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  31. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +11 -2
  32. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  33. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  34. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +70 -2
  35. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  36. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  37. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  38. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +67 -5
  39. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  40. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +181 -124
  41. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  42. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
  43. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +5 -1
  44. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
  45. package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
  46. package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
  47. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +134 -10
  48. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  49. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -2
  50. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  51. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  52. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +6 -3
  53. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  54. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  55. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +1 -1
  56. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +140 -7
  57. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -1
  58. package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -1
  59. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
  60. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +37 -9
  61. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
  62. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +6 -5
  63. package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
  64. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js +53 -1
  65. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js.map +1 -1
  66. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js +3 -2
  67. package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js.map +1 -1
  68. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  69. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +135 -0
  70. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  71. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  72. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
  73. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +19 -0
  74. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
  75. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +37 -19
  76. package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
  77. package/dist/esm/composables/useListView.js +21 -0
  78. package/dist/esm/composables/useListView.js.map +1 -1
  79. package/dist/esm/l10n/en.json.js +23 -1
  80. package/dist/esm/l10n/en.json.js.map +1 -1
  81. package/dist/esm/l10n/nl.json.js +41 -1
  82. package/dist/esm/l10n/nl.json.js.map +1 -1
  83. package/dist/esm/library-style.js +1 -1
  84. package/dist/esm/nextcloud-vue.css +622 -332
  85. package/dist/esm/store/useObjectStore.js +16 -2
  86. package/dist/esm/store/useObjectStore.js.map +1 -1
  87. package/dist/esm/utils/boardColumns.js +8 -1
  88. package/dist/esm/utils/boardColumns.js.map +1 -1
  89. package/dist/esm/utils/boardReferenceColumns.js +278 -0
  90. package/dist/esm/utils/boardReferenceColumns.js.map +1 -0
  91. package/dist/esm/utils/buildManifest.js +4 -1
  92. package/dist/esm/utils/buildManifest.js.map +1 -1
  93. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  94. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  95. package/dist/esm/utils/widgetDispatch.js +8 -2
  96. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  97. package/dist/nextcloud-vue.cjs.js +27964 -25769
  98. package/dist/nextcloud-vue.cjs.js.map +2 -2
  99. package/dist/nextcloud-vue.css +622 -332
  100. package/l10n/en.json +23 -1
  101. package/l10n/nl.json +41 -1
  102. package/package.json +1 -1
  103. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +160 -8
  104. package/src/components/CnBoardView/CnBoardView.vue +262 -6
  105. package/src/components/CnDashboardPage/CnDashboardPage.vue +57 -2
  106. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  107. package/src/components/CnDetailPage/CnDetailPage.vue +23 -4
  108. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +3 -1
  109. package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -5
  110. package/src/components/CnIndexPage/CnIndexPage.vue +586 -25
  111. package/src/components/CnIndexPage/useSelfFetchList.js +10 -1
  112. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +70 -2
  113. package/src/components/CnRegisterMapping/CnRegisterMapping.md +5 -0
  114. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +231 -96
  115. package/src/components/CnSettingsPage/CnSettingsPage.vue +5 -1
  116. package/src/components/CnStatWidget/CnStatWidget.vue +136 -11
  117. package/src/components/CnTabsWidget/CnTabsWidget.vue +6 -3
  118. package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +140 -7
  119. package/src/components/CnVersionInfoCard/CnVersionInfoCard.md +2 -1
  120. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +42 -13
  121. package/src/components/CnWidgetCardGrid/CnWidgetCardGrid.vue +55 -2
  122. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +135 -0
  123. package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +10 -0
  124. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +44 -1
  125. package/src/composables/useListView.js +21 -0
  126. package/src/css/board.css +65 -4
  127. package/src/css/dialog.css +4 -2
  128. package/src/css/index-page.css +49 -0
  129. package/src/css/look-board.css +77 -7
  130. package/src/schemas/app-manifest-v2.schema.json +115 -5
  131. package/src/store/useObjectStore.js +16 -2
  132. package/src/utils/boardColumns.js +8 -1
  133. package/src/utils/boardReferenceColumns.js +274 -0
  134. package/src/utils/buildManifest.js +4 -1
  135. package/src/utils/validateManifestV2.compiled.js +3 -3
  136. package/src/utils/widgetDispatch.js +8 -2
@@ -26,6 +26,15 @@ function resolveInitialQuickFilterIndex(quickFilters) {
26
26
  * @param {Array<object>} filterMaps Already route-resolved filter maps.
27
27
  * @return {object} The merged filter (scalar when a key has one value, array when many).
28
28
  */
29
+ /**
30
+ * Rows one board fetch asks for. A board shows every matching row at once,
31
+ * so it loads far more than a table page (OpenRegister serves up to 1000);
32
+ * past this the board offers "Load more".
33
+ *
34
+ * @type {number}
35
+ */
36
+ export const BOARD_PAGE_SIZE = 500
37
+
29
38
  /**
30
39
  * The sort a quick filter sets while it is active (`sort: { key, order }`, or a
31
40
  * list of those for a multi-column order), as sort keys, or null when it sets
@@ -236,7 +245,7 @@ export function useSelfFetchList(props, instance, inject, extras = {}) {
236
245
  defaultSortKeys: initialSortKeys,
237
246
  defaultActiveFilters: initialActiveFilters,
238
247
  defaultSearchTerm: initialSearchTerm,
239
- defaultPageSize: (props.pagination && props.pagination.limit) || undefined,
248
+ defaultPageSize: props.viewMode === 'board' ? BOARD_PAGE_SIZE : ((props.pagination && props.pagination.limit) || undefined),
240
249
  defaultVisibleColumns: configuredColumnKeys.length ? configuredColumnKeys : null,
241
250
  // `pages[].config.extend` → OpenRegister's repeated `_extend[]`. A
242
251
  // getter so a reactive change re-scopes the next fetch, matching how
@@ -141,9 +141,11 @@
141
141
  import { subscribe, unsubscribe } from '@nextcloud/event-bus'
142
142
  import { translate as t } from '@nextcloud/l10n'
143
143
  import { NcLoadingIcon } from '@nextcloud/vue'
144
+ import { inject, ref } from 'vue'
144
145
  import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
145
146
  import CnIcon from '../CnIcon/CnIcon.vue'
146
147
  import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
148
+ import { useEndpointSource } from '../../composables/useEndpointSource.js'
147
149
  import { actionLink, dispatchAction, interpolateActionTarget } from '../../utils/actionsDispatcher.js'
148
150
  import { isBadgeVariant } from '../../utils/badgeVariants.js'
149
151
  import { ageInDays } from '../../utils/boardDate.js'
@@ -180,7 +182,8 @@ function unwrap(value) {
180
182
  * objects filtered on a reference to this record. The source keys are those of
181
183
  * `object-list` (`register`, `schema`, `filter` with the `@objectId` /
182
184
  * `@object.<field>` / `@workspace.<key>` tokens, `sort`, `limit`, `extend`,
183
- * `rowRoute`, `pageControls`); `avatar` puts a circle with the initials of a name
185
+ * `rowRoute`, `pageControls`), or `endpointSource` (an app endpoint whose
186
+ * payload, after `responsePath`, is the array of rows, as on the object table); `avatar` puts a circle with the initials of a name
184
187
  * template before the title; what differs is the drawing: `presentation` is `rows`, `feed`
185
188
  * or `inline`, and each line is a template (`"{identifier} · {caseTypeName}"`,
186
189
  * `"Termijn {deadline|date}"`).
@@ -213,7 +216,9 @@ export default {
213
216
  * The list config: `{ register, schema, filter?, sort?, limit?, extend?,
214
217
  * presentation?, title?, meta?, text?, label?, pill?, trailing?, icon?,
215
218
  * rowRoute?, rowIdField?, headerLink?, headerNote?, emptyText?, errorText?,
216
- * valueLabels? }`. `valueLabels` maps a field's stored values to the labels
219
+ * valueLabels?, endpointSource? }`. `endpointSource` (`{ url, method?,
220
+ * params?, responsePath? }`) reads the rows from an app endpoint instead
221
+ * of `register` / `schema`. `valueLabels` maps a field's stored values to the labels
217
222
  * the templates show.
218
223
  *
219
224
  * @type {object}
@@ -238,6 +243,28 @@ export default {
238
243
 
239
244
  emits: ['header'],
240
245
 
246
+ setup(props) {
247
+ const objectCtxRaw = inject('cnObjectContext', null)
248
+ const workspaceRaw = inject('cnWorkspaceContext', null)
249
+ const appConfigRaw = inject('cnAppConfig', ref({}))
250
+ const { data, loading, error } = useEndpointSource(
251
+ () => props.content?.endpointSource || null,
252
+ {
253
+ ctx: () => {
254
+ const injected = unwrap(objectCtxRaw) || {}
255
+ return {
256
+ ...injected,
257
+ objectId: props.objectId ?? injected.objectId ?? props.objectData?.id ?? null,
258
+ object: props.objectData || injected.object || null,
259
+ workspace: unwrap(workspaceRaw) || {},
260
+ config: unwrapAppConfig(appConfigRaw) || {},
261
+ }
262
+ },
263
+ },
264
+ )
265
+ return { epData: data, epLoading: loading, epError: error }
266
+ },
267
+
241
268
  data() {
242
269
  return {
243
270
  rows: [],
@@ -294,6 +321,22 @@ export default {
294
321
  return { config: unwrapAppConfig(this.cnAppConfig) }
295
322
  },
296
323
 
324
+ /**
325
+ * Whether the rows come from `endpointSource` rather than OpenRegister.
326
+ *
327
+ * @return {boolean}
328
+ * @spec openspec/changes/endpoint-source-card-grid-record-list/specs/dashboard-page/spec.md#requirement-a-record-list-and-a-card-grid-can-read-an-endpoint
329
+ */
330
+ endpointActive() {
331
+ const source = this.content?.endpointSource
332
+ return Boolean(source && typeof source === 'object' && source.url)
333
+ },
334
+
335
+ /** @return {string} The endpoint read's state, to copy into the list when it changes. */
336
+ endpointKey() {
337
+ return this.endpointActive ? JSON.stringify([this.epData, this.epLoading, this.epError]) : ''
338
+ },
339
+
297
340
  /** @return {number} The fetch cap. */
298
341
  limit() {
299
342
  const value = Number(this.content?.limit)
@@ -398,6 +441,10 @@ export default {
398
441
  requestKey() {
399
442
  this.fetchRows()
400
443
  },
444
+
445
+ endpointKey() {
446
+ this.fetchRows()
447
+ },
401
448
  },
402
449
 
403
450
  mounted() {
@@ -854,6 +901,23 @@ export default {
854
901
  return { rows, total: typeof head.total === 'number' ? head.total : rows.length }
855
902
  },
856
903
 
904
+ /**
905
+ * Copy the endpoint read into the list: its payload must be an array of
906
+ * rows (anything else is an empty list), and a failed read is an error.
907
+ *
908
+ * @spec openspec/changes/endpoint-source-card-grid-record-list/specs/dashboard-page/spec.md#requirement-a-record-list-and-a-card-grid-can-read-an-endpoint
909
+ */
910
+ takeEndpointRows() {
911
+ this.rows = Array.isArray(this.epData) ? this.epData : []
912
+ this.total = this.rows.length
913
+ this.loading = this.epLoading === true
914
+ this.error = Boolean(this.epError)
915
+ this.lenses = {}
916
+ if (!this.loading) {
917
+ this.emitHeader()
918
+ }
919
+ },
920
+
857
921
  /**
858
922
  * Fetch the list. Sends nothing while a required token is unresolved
859
923
  * or the source is incomplete.
@@ -862,6 +926,10 @@ export default {
862
926
  */
863
927
  async fetchRows() {
864
928
  const c = this.content || {}
929
+ if (this.endpointActive) {
930
+ this.takeEndpointRows()
931
+ return
932
+ }
865
933
  if (!c.register || !c.schema || this.waiting) {
866
934
  this.rows = []
867
935
  this.total = 0
@@ -111,6 +111,10 @@ export default {
111
111
  </script>
112
112
  ```
113
113
 
114
+ ## Several groups
115
+
116
+ With more than one group each group starts closed: its heading is a button that shows the group name and how many of its types are configured ("2/8 configured"), and opens the register select and type list on a click or Enter. A page with several registers is a short card until the administrator opens the group they need.
117
+
114
118
  ## Additional props
115
119
 
116
120
  | Prop | Type | Default | Description |
@@ -122,5 +126,6 @@ export default {
122
126
  | `reimporting` | Boolean | `false` | Disables the reimport button and shows a spinner while reimport is in progress |
123
127
  | `saveButtonText` | String | `'Save configuration'` | Label for the save button |
124
128
  | `reimportButtonText` | String | `'Re-import configuration'` | Label for the reimport button |
129
+ | `collapseGroups` | Boolean | `null` | Draw each group as a heading button that opens and closes it, with its configured count shown while closed; the groups start closed. `null` does this when there is more than one group, `true` or `false` forces it |
125
130
  | `autoMatch` | Boolean | `true` | Automatically match schema titles to type slugs when a register is selected |
126
131
  | `labels` | Object | *(see below)* | UI label overrides for all user-visible strings (register, schema, notConfigured, noSchemas, selectRegister, selectSchema, allConfigured, partiallyConfigured) |
@@ -4,7 +4,7 @@
4
4
  :description="description"
5
5
  :docUrl="docUrl"
6
6
  :loading="registersLoading"
7
- loadingMessage="Loading registers..."
7
+ :loadingMessage="t('nextcloud-vue', 'Loading registers…')"
8
8
  :error="!!registersError"
9
9
  :errorMessage="registersError || ''"
10
10
  :onRetry="loadRegisters">
@@ -19,7 +19,7 @@
19
19
  <NcLoadingIcon v-if="saving" :size="20" />
20
20
  <ContentSave v-else :size="20" />
21
21
  </template>
22
- {{ saving ? 'Saving...' : saveButtonText }}
22
+ {{ saving ? t('nextcloud-vue', 'Saving…') : saveButtonText }}
23
23
  </NcButton>
24
24
  <NcButton
25
25
  v-if="showReimportButton"
@@ -30,7 +30,7 @@
30
30
  <NcLoadingIcon v-if="reimporting" :size="20" />
31
31
  <Refresh v-else :size="20" />
32
32
  </template>
33
- {{ reimporting ? 'Importing...' : reimportButtonText }}
33
+ {{ reimporting ? t('nextcloud-vue', 'Importing…') : reimportButtonText }}
34
34
  </NcButton>
35
35
  <slot name="actions" />
36
36
  </template>
@@ -40,9 +40,39 @@
40
40
  <div
41
41
  v-for="(group, groupIdx) in groups"
42
42
  :key="groupIdx"
43
- class="cn-register-mapping__group">
44
- <!-- Group header -->
45
- <slot name="group-header"
43
+ class="cn-register-mapping__group"
44
+ :class="{ 'cn-register-mapping__group--collapsed': !isGroupOpen(groupIdx) }"
45
+ data-testid="cn-register-mapping-group">
46
+ <!-- Group header. With collapsible groups a heading holds the button
47
+ that opens and closes the group (the WAI-ARIA accordion); the
48
+ count stays visible while it is closed. -->
49
+ <h4 v-if="groupsCollapsible" class="cn-register-mapping__group-heading">
50
+ <button
51
+ type="button"
52
+ class="cn-register-mapping__group-toggle"
53
+ :aria-expanded="String(isGroupOpen(groupIdx))"
54
+ :aria-controls="groupBodyId(groupIdx)"
55
+ data-testid="cn-register-mapping-group-toggle"
56
+ @click="toggleGroup(groupIdx)">
57
+ <span class="cn-register-mapping__group-chevron" aria-hidden="true">
58
+ <ChevronUp v-if="isGroupOpen(groupIdx)" :size="20" />
59
+ <ChevronDown v-else :size="20" />
60
+ </span>
61
+ <slot name="group-header"
62
+ :group="group"
63
+ :configuredCount="configuredCount(groupIdx)"
64
+ :totalCount="group.types.length">
65
+ <span class="cn-register-mapping__group-header">
66
+ <span class="cn-register-mapping__group-title">{{ group.name }}</span>
67
+ <span class="cn-register-mapping__group-status" data-testid="cn-register-mapping-group-count">
68
+ {{ configuredCount(groupIdx) }}/{{ group.types.length }} {{ labels.partiallyConfigured }}
69
+ </span>
70
+ </span>
71
+ </slot>
72
+ </button>
73
+ </h4>
74
+ <slot v-else
75
+ name="group-header"
46
76
  :group="group"
47
77
  :configuredCount="configuredCount(groupIdx)"
48
78
  :totalCount="group.types.length">
@@ -56,90 +86,95 @@
56
86
  </div>
57
87
  </slot>
58
88
 
59
- <!-- Group description -->
60
- <p v-if="group.description" class="cn-register-mapping__group-description">
61
- {{ group.description }}
62
- </p>
63
-
64
- <!-- Register selector -->
65
- <div class="cn-register-mapping__register-select">
66
- <label class="cn-register-mapping__label">{{ labels.register }}</label>
67
- <NcSelect
68
- :modelValue="selectedRegister(groupIdx)"
69
- :options="registerSelectOptions"
70
- :placeholder="labels.selectRegister"
71
- :inputLabel="labels.register"
72
- :loading="registersLoading"
73
- label="label"
74
- trackBy="value"
75
- @update:modelValue="handleRegisterChange(groupIdx, $event)" />
76
- </div>
77
-
78
- <!-- Type list -->
79
- <div v-if="selectedRegister(groupIdx)" class="cn-register-mapping__type-list">
80
- <!-- Header row -->
81
- <div class="cn-register-mapping__type-list-header">
82
- <span>Name</span>
83
- <span>Schema</span>
84
- <span />
85
- <span />
89
+ <div
90
+ v-show="isGroupOpen(groupIdx)"
91
+ :id="groupBodyId(groupIdx)"
92
+ class="cn-register-mapping__group-body"
93
+ data-testid="cn-register-mapping-group-body">
94
+ <!-- Group description -->
95
+ <p v-if="group.description" class="cn-register-mapping__group-description">
96
+ {{ group.description }}
97
+ </p>
98
+
99
+ <!-- Register selector -->
100
+ <div class="cn-register-mapping__register-select">
101
+ <NcSelect
102
+ :modelValue="selectedRegister(groupIdx)"
103
+ :options="registerSelectOptions"
104
+ :placeholder="labels.selectRegister"
105
+ :inputLabel="labels.register"
106
+ :loading="registersLoading"
107
+ label="label"
108
+ trackBy="value"
109
+ @update:modelValue="handleRegisterChange(groupIdx, $event)" />
86
110
  </div>
87
111
 
88
- <!-- Type rows -->
89
- <template v-for="type in group.types" :key="type.slug + '-row'">
90
- <div
91
- class="cn-register-mapping__type-row"
92
- :class="{ 'cn-register-mapping__type-row--expanded': isExpanded(groupIdx, type.slug) }"
93
- @click="toggleExpand(groupIdx, type.slug)">
94
- <span class="cn-register-mapping__type-name">{{ type.label }}</span>
95
- <span class="cn-register-mapping__type-schema">
96
- {{ schemaLabel(groupIdx, type) || labels.notConfigured }}
97
- </span>
98
- <span class="cn-register-mapping__type-status">
99
- <span
100
- class="cn-register-mapping__status-dot"
101
- :class="schemaValue(groupIdx, type)
102
- ? 'cn-register-mapping__status-dot--configured'
103
- : 'cn-register-mapping__status-dot--unconfigured'" />
104
- </span>
105
- <span class="cn-register-mapping__type-chevron">
106
- <ChevronUp v-if="isExpanded(groupIdx, type.slug)" :size="20" />
107
- <ChevronDown v-else :size="20" />
108
- </span>
112
+ <!-- Type list -->
113
+ <div v-if="selectedRegister(groupIdx)" class="cn-register-mapping__type-list">
114
+ <!-- Header row -->
115
+ <div class="cn-register-mapping__type-list-header">
116
+ <span>{{ t('nextcloud-vue', 'Object type') }}</span>
117
+ <span>{{ labels.schema }}</span>
118
+ <span />
119
+ <span />
109
120
  </div>
110
121
 
111
- <!-- Expanded detail -->
112
- <transition name="slide">
122
+ <!-- Type rows -->
123
+ <template v-for="type in group.types" :key="type.slug + '-row'">
113
124
  <div
114
- v-if="isExpanded(groupIdx, type.slug)"
115
- class="cn-register-mapping__type-detail">
116
- <p v-if="type.description" class="cn-register-mapping__type-description">
117
- {{ type.description }}
118
- </p>
119
- <NcSelect
120
- :modelValue="selectedSchema(groupIdx, type)"
121
- :options="schemaSelectOptions(groupIdx)"
122
- :placeholder="labels.selectSchema"
123
- :inputLabel="labels.schema"
124
- label="label"
125
- trackBy="value"
126
- @update:modelValue="handleSchemaChange(groupIdx, type, $event)" />
125
+ class="cn-register-mapping__type-row"
126
+ :class="{ 'cn-register-mapping__type-row--expanded': isExpanded(groupIdx, type.slug) }"
127
+ @click="toggleExpand(groupIdx, type.slug)">
128
+ <span class="cn-register-mapping__type-name">{{ type.label }}</span>
129
+ <span class="cn-register-mapping__type-schema">
130
+ {{ schemaLabel(groupIdx, type) || labels.notConfigured }}
131
+ </span>
132
+ <span class="cn-register-mapping__type-status">
133
+ <span
134
+ class="cn-register-mapping__status-dot"
135
+ :class="schemaValue(groupIdx, type)
136
+ ? 'cn-register-mapping__status-dot--configured'
137
+ : 'cn-register-mapping__status-dot--unconfigured'" />
138
+ </span>
139
+ <span class="cn-register-mapping__type-chevron">
140
+ <ChevronUp v-if="isExpanded(groupIdx, type.slug)" :size="20" />
141
+ <ChevronDown v-else :size="20" />
142
+ </span>
127
143
  </div>
128
- </transition>
129
- </template>
130
- </div>
131
144
 
132
- <!-- No register selected -->
133
- <NcNoteCard v-else-if="!registersLoading" type="info">
134
- {{ labels.selectRegister }}
135
- </NcNoteCard>
136
-
137
- <!-- No schemas available -->
138
- <NcNoteCard
139
- v-if="selectedRegister(groupIdx) && schemaSelectOptions(groupIdx).length === 0 && !registersLoading"
140
- variant="warning">
141
- {{ labels.noSchemas }}
142
- </NcNoteCard>
145
+ <!-- Expanded detail -->
146
+ <transition name="slide">
147
+ <div
148
+ v-if="isExpanded(groupIdx, type.slug)"
149
+ class="cn-register-mapping__type-detail">
150
+ <p v-if="type.description" class="cn-register-mapping__type-description">
151
+ {{ type.description }}
152
+ </p>
153
+ <NcSelect
154
+ :modelValue="selectedSchema(groupIdx, type)"
155
+ :options="schemaSelectOptions(groupIdx)"
156
+ :placeholder="labels.selectSchema"
157
+ :inputLabel="labels.schema"
158
+ label="label"
159
+ trackBy="value"
160
+ @update:modelValue="handleSchemaChange(groupIdx, type, $event)" />
161
+ </div>
162
+ </transition>
163
+ </template>
164
+ </div>
165
+
166
+ <!-- No register selected -->
167
+ <NcNoteCard v-else-if="!registersLoading" type="info">
168
+ {{ labels.selectRegister }}
169
+ </NcNoteCard>
170
+
171
+ <!-- No schemas available -->
172
+ <NcNoteCard
173
+ v-if="selectedRegister(groupIdx) && schemaSelectOptions(groupIdx).length === 0 && !registersLoading"
174
+ variant="warning">
175
+ {{ labels.noSchemas }}
176
+ </NcNoteCard>
177
+ </div>
143
178
  </div>
144
179
  </div>
145
180
 
@@ -160,6 +195,8 @@ import Refresh from 'vue-material-design-icons/Refresh.vue'
160
195
  import { buildHeaders, prefixUrl } from '../../utils/headers.js'
161
196
  import { CnSettingsSection } from '../CnSettingsSection/index.js'
162
197
 
198
+ let mappingCount = 0
199
+
163
200
  /**
164
201
  * CnRegisterMapping - OpenRegister register/schema configuration component.
165
202
  *
@@ -284,6 +321,20 @@ export default {
284
321
  default: () => t('nextcloud-vue', 'Re-import configuration'),
285
322
  },
286
323
 
324
+ /**
325
+ * Draw each group as a header that opens and closes it, showing its
326
+ * configured count while closed. The groups start closed. `null` (the
327
+ * default) collapses them when there is more than one group, so a page
328
+ * with several registers is not one long card.
329
+ *
330
+ * @type {boolean|null}
331
+ * @spec openspec/changes/register-mapping-collapsed-groups/specs/register-mapping/spec.md#requirement-register-groups-start-closed-with-their-count
332
+ */
333
+ collapseGroups: {
334
+ type: Boolean,
335
+ default: null,
336
+ },
337
+
287
338
  /** Auto-match schema titles to type slugs on register change */
288
339
  autoMatch: {
289
340
  type: Boolean,
@@ -319,6 +370,9 @@ export default {
319
370
  // Local state
320
371
  localConfig: {},
321
372
  expandedRows: {},
373
+ // The groups opened by the reader, by index.
374
+ openGroups: {},
375
+ uid: ++mappingCount,
322
376
  }
323
377
  },
324
378
 
@@ -326,11 +380,21 @@ export default {
326
380
  /** Register options for NcSelect */
327
381
  registerSelectOptions() {
328
382
  return this.registers.map((r) => ({
329
- label: r.title || r.slug || `Register ${r.id}`,
383
+ label: r.title || r.slug || t('nextcloud-vue', 'Register {id}', { id: r.id }),
330
384
  value: String(r.id),
331
385
  }))
332
386
  },
333
387
 
388
+ /**
389
+ * Whether the groups open and close.
390
+ *
391
+ * @return {boolean}
392
+ * @spec openspec/changes/register-mapping-collapsed-groups/specs/register-mapping/spec.md#requirement-register-groups-start-closed-with-their-count
393
+ */
394
+ groupsCollapsible() {
395
+ return this.collapseGroups === null ? this.groups.length > 1 : this.collapseGroups
396
+ },
397
+
334
398
  /** Whether local config differs from prop */
335
399
  hasChanges() {
336
400
  return JSON.stringify(this.localConfig) !== JSON.stringify(this.configuration)
@@ -353,6 +417,39 @@ export default {
353
417
  },
354
418
 
355
419
  methods: {
420
+ t,
421
+
422
+ /**
423
+ * Whether a group's body shows.
424
+ *
425
+ * @param {number} groupIdx Group index
426
+ * @return {boolean}
427
+ */
428
+ isGroupOpen(groupIdx) {
429
+ return !this.groupsCollapsible || this.openGroups[groupIdx] === true
430
+ },
431
+
432
+ /**
433
+ * Open a closed group, or close an open one.
434
+ *
435
+ * @param {number} groupIdx Group index
436
+ * @return {void}
437
+ * @spec openspec/changes/register-mapping-collapsed-groups/specs/register-mapping/spec.md#requirement-register-groups-start-closed-with-their-count
438
+ */
439
+ toggleGroup(groupIdx) {
440
+ this.openGroups = { ...this.openGroups, [groupIdx]: !this.isGroupOpen(groupIdx) }
441
+ },
442
+
443
+ /**
444
+ * The id of a group's body, for the header's `aria-controls`.
445
+ *
446
+ * @param {number} groupIdx Group index
447
+ * @return {string}
448
+ */
449
+ groupBodyId(groupIdx) {
450
+ return `cn-register-mapping-${this.uid}-group-${groupIdx}`
451
+ },
452
+
356
453
  /**
357
454
  * Get the config key for a group's register.
358
455
  *
@@ -437,7 +534,7 @@ export default {
437
534
  return selected.label
438
535
  }
439
536
  const value = this.schemaValue(groupIdx, type)
440
- return value ? `Schema ${value}` : ''
537
+ return value ? t('nextcloud-vue', 'Schema {id}', { id: value }) : ''
441
538
  },
442
539
 
443
540
  /**
@@ -453,7 +550,7 @@ export default {
453
550
  }
454
551
  const schemas = this.schemasByRegister[reg.value] || []
455
552
  return schemas.map((s) => ({
456
- label: s.title || s.slug || `Schema ${s.id}`,
553
+ label: s.title || s.slug || t('nextcloud-vue', 'Schema {id}', { id: s.id }),
457
554
  value: String(s.id),
458
555
  }))
459
556
  },
@@ -591,7 +688,7 @@ export default {
591
688
  })
592
689
 
593
690
  if (!response.ok) {
594
- this.registersError = `Failed to fetch registers: ${response.statusText}`
691
+ this.registersError = t('nextcloud-vue', 'The registers could not be loaded: {reason}', { reason: response.statusText || response.status })
595
692
  return
596
693
  }
597
694
 
@@ -612,7 +709,7 @@ export default {
612
709
  }
613
710
  }
614
711
  } catch (error) {
615
- this.registersError = error.message || 'Network error fetching registers'
712
+ this.registersError = error.message || t('nextcloud-vue', 'The registers could not be loaded: {reason}', { reason: t('nextcloud-vue', 'no connection') })
616
713
  } finally {
617
714
  this.registersLoading = false
618
715
  }
@@ -671,6 +768,52 @@ export default {
671
768
  margin-bottom: 12px;
672
769
  }
673
770
 
771
+ .cn-register-mapping__group-heading {
772
+ margin: 0 0 12px;
773
+ font: inherit;
774
+ }
775
+
776
+ .cn-register-mapping__group--collapsed .cn-register-mapping__group-heading {
777
+ margin-bottom: 0;
778
+ }
779
+
780
+ .cn-register-mapping__group-toggle {
781
+ box-sizing: border-box;
782
+ display: flex;
783
+ align-items: center;
784
+ gap: 8px;
785
+ width: 100%;
786
+ min-height: 44px;
787
+ margin: 0;
788
+ padding: 0;
789
+ border: 0;
790
+ background: transparent;
791
+ color: inherit;
792
+ font: inherit;
793
+ text-align: start;
794
+ cursor: pointer;
795
+ }
796
+
797
+ .cn-register-mapping__group-toggle:focus-visible {
798
+ outline: 2px solid var(--color-primary-element);
799
+ outline-offset: 2px;
800
+ border-radius: var(--border-radius);
801
+ }
802
+
803
+ .cn-register-mapping__group-chevron {
804
+ display: inline-flex;
805
+ flex: none;
806
+ color: var(--color-text-maxcontrast);
807
+ }
808
+
809
+ .cn-register-mapping__group-toggle .cn-register-mapping__group-header {
810
+ flex: 1 1 auto;
811
+ flex-wrap: wrap;
812
+ gap: 4px 12px;
813
+ min-width: 0;
814
+ margin-bottom: 0;
815
+ }
816
+
674
817
  .cn-register-mapping__group-title {
675
818
  font-size: 16px;
676
819
  font-weight: 600;
@@ -689,14 +832,6 @@ export default {
689
832
  margin: 0 0 12px 0;
690
833
  }
691
834
 
692
- .cn-register-mapping__label {
693
- display: block;
694
- font-weight: 500;
695
- font-size: 13px;
696
- color: var(--color-text-maxcontrast);
697
- margin-bottom: 4px;
698
- }
699
-
700
835
  .cn-register-mapping__register-select {
701
836
  margin-bottom: 16px;
702
837
  max-width: 400px;
@@ -303,6 +303,7 @@ import {
303
303
  import ContentSave from 'vue-material-design-icons/ContentSave.vue'
304
304
  import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
305
305
  import CnRegisterMapping from '../CnRegisterMapping/CnRegisterMapping.vue'
306
+ import CnToggleTableWidget from '../CnToggleTableWidget/CnToggleTableWidget.vue'
306
307
  import CnVersionInfoCard from '../CnVersionInfoCard/CnVersionInfoCard.vue'
307
308
  import { useLook } from '../../composables/useLook.js'
308
309
  import { prefixUrl } from '../../utils/headers.js'
@@ -340,6 +341,8 @@ const COMPONENT_DISCRIMINATOR = Symbol('cn-settings-component-widget')
340
341
  const BUILTIN_SETTINGS_WIDGETS = Object.freeze({
341
342
  'version-info': CnVersionInfoCard,
342
343
  'register-mapping': CnRegisterMapping,
344
+ // Rows with a switch: objects (`bind.mode: 'each'`) or a config map (`'map'`).
345
+ 'toggle-table': CnToggleTableWidget,
343
346
  component: COMPONENT_DISCRIMINATOR,
344
347
  })
345
348
 
@@ -367,7 +370,8 @@ const BUILTIN_SETTINGS_WIDGETS = Object.freeze({
367
370
  * - `widgets[]` — ordered list of widgets. Each entry has a `type`
368
371
  * string and optional `props`. Built-in widget types resolve
369
372
  * first (`version-info` → `CnVersionInfoCard`, `register-mapping`
370
- * → `CnRegisterMapping`); unknown types fall back to the
373
+ * → `CnRegisterMapping`, `toggle-table` → `CnToggleTableWidget`);
374
+ * unknown types fall back to the
371
375
  * `customComponents` registry.
372
376
  *
373
377
  * Widget events (e.g. CnRegisterMapping's `@save`, CnVersionInfoCard's