@conduction/nextcloud-vue 2.102.0 → 2.104.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 (147) 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/CnAppRoot/CnAppRoot.vue2.js +18 -1
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +3 -3
  8. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  9. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  10. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +73 -0
  11. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  12. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +99 -64
  13. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  14. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
  15. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +46 -0
  16. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  17. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +37 -2
  18. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -6
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +39 -2
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +14 -0
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +40 -2
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  30. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  31. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +113 -20
  32. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  33. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +387 -359
  34. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +173 -3
  36. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +107 -87
  38. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  39. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +7 -2
  40. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  41. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  42. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +10 -1
  43. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  45. package/dist/esm/components/CnObjectRow/CnObjectRow.vue.js +1 -1
  46. package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js +57 -5
  47. package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js.map +1 -1
  48. package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js +37 -4
  49. package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js.map +1 -1
  50. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  51. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +97 -3
  52. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  53. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +46 -2
  54. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  55. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +9 -0
  56. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js.map +1 -0
  57. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js +165 -0
  58. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -0
  59. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js +85 -0
  60. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -0
  61. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  62. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +54 -6
  63. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  64. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +3 -1
  65. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  66. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  67. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +8 -1
  68. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  69. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  70. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
  71. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
  72. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
  73. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  74. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +18 -2
  75. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  76. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  77. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
  78. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  79. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  80. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  81. package/dist/esm/components/CnWizardDialog/CnWizardDialog.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 +4 -0
  87. package/dist/esm/l10n/en.json.js.map +1 -1
  88. package/dist/esm/l10n/nl.json.js +4 -0
  89. package/dist/esm/l10n/nl.json.js.map +1 -1
  90. package/dist/esm/library-style.js +1 -1
  91. package/dist/esm/mixins/pageViews.js +8 -1
  92. package/dist/esm/mixins/pageViews.js.map +1 -1
  93. package/dist/esm/nextcloud-vue.css +586 -416
  94. package/dist/esm/utils/nextcloudDirectory.js +18 -1
  95. package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
  96. package/dist/esm/utils/sortOrder.js +61 -0
  97. package/dist/esm/utils/sortOrder.js.map +1 -0
  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/visibleWhen.js +13 -2
  101. package/dist/esm/utils/visibleWhen.js.map +1 -1
  102. package/dist/nextcloud-vue.cjs.js +12270 -10936
  103. package/dist/nextcloud-vue.cjs.js.map +2 -2
  104. package/dist/nextcloud-vue.css +586 -416
  105. package/l10n/en.json +4 -0
  106. package/l10n/nl.json +4 -0
  107. package/package.json +1 -1
  108. package/src/components/CnActionsBar/CnActionsBar.md +1 -0
  109. package/src/components/CnActionsBar/CnActionsBar.vue +39 -2
  110. package/src/components/CnAppRoot/CnAppRoot.md +1 -1
  111. package/src/components/CnAppRoot/CnAppRoot.vue +21 -4
  112. package/src/components/CnConversationThread/CnConversationThread.md +17 -0
  113. package/src/components/CnConversationThread/CnConversationThread.vue +113 -8
  114. package/src/components/CnConversationThread/CnConversationWidget.vue +60 -1
  115. package/src/components/CnDashboardPage/CnDashboardPage.md +4 -1
  116. package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -6
  117. package/src/components/CnDataTable/CnDataTable.md +1 -0
  118. package/src/components/CnDataTable/CnDataTable.vue +49 -0
  119. package/src/components/CnDetailPage/CnDetailPage.md +2 -1
  120. package/src/components/CnDetailPage/CnDetailPage.vue +40 -2
  121. package/src/components/CnFormDialog/CnFormDialog.md +1 -1
  122. package/src/components/CnFormDialog/CnFormDialog.vue +127 -0
  123. package/src/components/CnIndexPage/CnIndexPage.md +14 -2
  124. package/src/components/CnIndexPage/CnIndexPage.vue +192 -11
  125. package/src/components/CnIndexPage/useSelfFetchList.js +6 -1
  126. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +10 -1
  127. package/src/components/CnObjectRow/CnObjectRow.vue +95 -1
  128. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +134 -2
  129. package/src/components/CnSearchSelect/CnSearchSelect.vue +283 -0
  130. package/src/components/CnSearchSelect/index.js +12 -0
  131. package/src/components/CnSetupWizard/CnSetupWizard.vue +56 -6
  132. package/src/components/CnTabsWidget/CnTabsWidget.vue +8 -1
  133. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +7 -2
  134. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +18 -2
  135. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  136. package/src/components/index.js +1 -0
  137. package/src/composables/indexSources.js +7 -7
  138. package/src/css/dialog.css +5 -3
  139. package/src/css/index-page.css +19 -0
  140. package/src/css/look-board-index.css +48 -0
  141. package/src/index.js +1 -0
  142. package/src/mixins/pageViews.js +8 -1
  143. package/src/schemas/app-manifest-v2.schema.json +75 -6
  144. package/src/utils/nextcloudDirectory.js +17 -0
  145. package/src/utils/sortOrder.js +58 -0
  146. package/src/utils/validateManifestV2.compiled.js +3 -3
  147. package/src/utils/visibleWhen.js +13 -2
@@ -368,6 +368,20 @@ export default {
368
368
  default: 0,
369
369
  },
370
370
 
371
+ /**
372
+ * Rows per page: the board's pager under the table ("4 of 7", page
373
+ * buttons, Next), forwarded to CnDataTable. It pages the rows the
374
+ * widget holds; with a `source`, `source.limit` is how many are read
375
+ * (OpenRegister's default page otherwise). 0 (the default) shows no
376
+ * pager.
377
+ *
378
+ * @spec openspec/changes/object-table-pager/specs/object-table/spec.md#requirement-an-object-table-can-page-its-rows
379
+ */
380
+ pageSize: {
381
+ type: Number,
382
+ default: 0,
383
+ },
384
+
371
385
  /**
372
386
  * vue-router route object for the "View all" footer link. Forwarded
373
387
  * to CnDataTable (footer shows when the rows are a `limit`-ed subset).
@@ -611,7 +625,9 @@ export default {
611
625
  params[`_order[${field}]`] = dir === 'desc' ? 'desc' : 'asc'
612
626
  }
613
627
  if (typeof src.limit === 'number' && src.limit > 0) {
614
- params._limit = src.limit + 1
628
+ // Under a pager `limit` is how many rows the pages hold, not a
629
+ // "View all" cap that needs to see one more.
630
+ params._limit = this.pageSize > 0 ? src.limit : src.limit + 1
615
631
  }
616
632
  // `source.extend` (#91 Wave 3): forwarded as OR's repeated
617
633
  // `_extend[]` param so virtual/declarative fields (e.g.
@@ -736,7 +752,7 @@ export default {
736
752
  inner.register = this.source.register
737
753
  inner.schemaId = this.source.schema
738
754
  inner.fetchParams = this.resolvedFetchParams
739
- if (typeof this.source.limit === 'number' && this.source.limit > 0) {
755
+ if (typeof this.source.limit === 'number' && this.source.limit > 0 && !(this.pageSize > 0)) {
740
756
  inner.limit = this.source.limit
741
757
  }
742
758
  }
@@ -5,7 +5,7 @@
5
5
  defaultWidth="wizard"
6
6
  :eyebrow="boardEyebrow"
7
7
  :subtitle="subtitle"
8
- :name="dialogTitle"
8
+ :name="shownTitle"
9
9
  size="large"
10
10
  :open="dialogOpen"
11
11
  :noClose="loading || !cancellable"
@@ -265,6 +265,12 @@ export default {
265
265
  * Empty uses the dialog title.
266
266
  */
267
267
  eyebrowContext: { type: String, default: '' },
268
+ /**
269
+ * Board look: head the dialog with the current step's label, the
270
+ * dialog title moving to the eyebrow's context. The result phase and
271
+ * the Nextcloud look keep `dialogTitle`.
272
+ */
273
+ stepHeading: { type: Boolean, default: false },
268
274
  /**
269
275
  * The eyebrow sentence, with `{context}`, `{step}` and `{total}`
270
276
  * placeholders, so it can be translated.
@@ -356,6 +362,19 @@ export default {
356
362
  .replace('{total}', String(this.steps.length))
357
363
  },
358
364
 
365
+ /**
366
+ * The dialog's heading: the current step's label with `stepHeading`
367
+ * in the board look, otherwise `dialogTitle`.
368
+ *
369
+ * @return {string} The heading.
370
+ */
371
+ shownTitle() {
372
+ if (this.stepHeading && this.isBoardLook && this.result === null && this.currentStep && this.currentStep.label) {
373
+ return this.currentStep.label
374
+ }
375
+ return this.dialogTitle
376
+ },
377
+
359
378
  /** @return {object} The check icon, the default finishing icon. */
360
379
  Check() {
361
380
  return Check
@@ -130,6 +130,7 @@ export { default as CnContactsTab } from '../integrations/builtin/contacts/CnCon
130
130
  export { CnContactPicker } from './CnContactPicker/index.js'
131
131
  export { CnContactCreate } from './CnContactCreate/index.js'
132
132
  export { CnResourceSelect } from './CnResourceSelect/index.js'
133
+ export { CnSearchSelect } from './CnSearchSelect/index.js'
133
134
  export { CnPropertySourceField } from './CnPropertySourceField/index.js'
134
135
  export { CnDurationField } from './CnDurationField/index.js'
135
136
  export { CnFavouriteToggle } from './CnFavouriteToggle/index.js'
@@ -14,7 +14,7 @@ import { formatRelativeDate } from '../utils/boardDate.js'
14
14
  import { fetchReferenceObjects } from '../utils/boardReferenceColumns.js'
15
15
  import { fillTemplate } from '../utils/fillTemplate.js'
16
16
  import { FLOW_RUN_ERROR_STATUSES, flowRunStatuses } from '../utils/flowRunStatus.js'
17
- import { fetchGroupMembers, fetchPresence, fetchStatuses } from '../utils/nextcloudDirectory.js'
17
+ import { fetchGroupMembers, fetchPresence, fetchStatuses, fetchUsers } from '../utils/nextcloudDirectory.js'
18
18
  import { readPath } from '../utils/readPath.js'
19
19
  import { useFlowStore } from './useFlowStore.js'
20
20
  import { organisationRecord, useOrganisationStore } from './useOrganisationStore.js'
@@ -1054,7 +1054,8 @@ function searchSource(config = {}, context = {}) {
1054
1054
  * DqAfdelingen). Pair it with `folderSidebar: { source: 'groups',
1055
1055
  * filterField: 'group' }`, which lists the groups as the folder tree; the
1056
1056
  * selected group arrives as `group`. `sourceConfig.group` is the group shown
1057
- * before one is picked.
1057
+ * before one is picked; without one, All lists every user the reader may
1058
+ * list (all users for an admin, their groups' users for a sub-admin).
1058
1059
  *
1059
1060
  * Today is the dav app's out-of-office period when one runs now ("Absent
1060
1061
  * until 14 Oct"), else the user status (present, away) and its message. The
@@ -1088,14 +1089,13 @@ function usersSource() {
1088
1089
  load: async (config) => {
1089
1090
  const group = String((config && (config.group || config.defaultGroup)) || '')
1090
1091
  const mine = ++seq
1091
- if (group === '') {
1092
- state.rows = []
1093
- return
1094
- }
1095
1092
  state.loading = true
1096
1093
  try {
1097
1094
  const { default: axios } = await import('@nextcloud/axios')
1098
- const [members, statuses] = await Promise.all([fetchGroupMembers(axios, group), fetchStatuses(axios)])
1095
+ const [members, statuses] = await Promise.all([
1096
+ group === '' ? fetchUsers(axios, { limit: (config && config.limit) || 500 }) : fetchGroupMembers(axios, group),
1097
+ fetchStatuses(axios),
1098
+ ])
1099
1099
  const functionField = (config && config.functionField) || 'headline'
1100
1100
  const rolesField = (config && config.rolesField) || 'role'
1101
1101
  const rows = await Promise.all(members.map(async (user) => {
@@ -9,14 +9,16 @@
9
9
  * Theme hooks (all optional, defaults below):
10
10
  * --cn-dialog-danger fill of a destructive primary
11
11
  * --cn-dialog-eyebrow-color eyebrow colour
12
- * --cn-dialog-eyebrow-transform uppercase (default) or none
12
+ * --cn-dialog-eyebrow-transform none (default, sentence case) or uppercase
13
+ * --cn-dialog-eyebrow-tracking letter spacing; 0 by default, 0.06em suits capitals
13
14
  */
14
15
 
15
16
  .cn-dialog-board {
16
17
  --cn-dialog-width: 640px;
17
18
  --cn-dialog-danger: var(--color-error);
18
19
  --cn-dialog-eyebrow-color: var(--color-primary-element-text, var(--color-main-text));
19
- --cn-dialog-eyebrow-transform: uppercase;
20
+ --cn-dialog-eyebrow-transform: none;
21
+ --cn-dialog-eyebrow-tracking: 0;
20
22
  }
21
23
 
22
24
  /* Dimmed page behind the dialog: text colour at 45%. */
@@ -73,7 +75,7 @@
73
75
  margin-bottom: 4px;
74
76
  font-size: 13px;
75
77
  font-weight: 700;
76
- letter-spacing: 0.06em;
78
+ letter-spacing: var(--cn-dialog-eyebrow-tracking, 0.06em);
77
79
  text-transform: var(--cn-dialog-eyebrow-transform, uppercase);
78
80
  color: var(--cn-dialog-eyebrow-color, var(--color-main-text));
79
81
  }
@@ -190,6 +190,25 @@
190
190
  font-size: 14px;
191
191
  }
192
192
 
193
+ /* The selected folder's heading above the list. */
194
+ .cn-index-page__scope-heading {
195
+ display: flex;
196
+ flex-direction: column;
197
+ gap: 4px;
198
+ margin-bottom: 12px;
199
+ }
200
+
201
+ .cn-index-page__scope-title {
202
+ margin: 0;
203
+ font-size: 1.25em;
204
+ font-weight: 700;
205
+ }
206
+
207
+ .cn-index-page__scope-subtitle {
208
+ font-size: 14px;
209
+ color: var(--color-text-maxcontrast);
210
+ }
211
+
193
212
  /* Opt-in folder navigation pane (left of the list). */
194
213
  .cn-index-page__folder-pane {
195
214
  flex: 0 0 240px;
@@ -372,6 +372,17 @@
372
372
  cursor: not-allowed;
373
373
  }
374
374
 
375
+ /* `searchPlacement: "toolbar"`: the search field on row 1 takes the room
376
+ before Filter and the view switch, which keep the end of the row. */
377
+ .cn-look-board .cn-actions-bar--board > .cn-actions-bar__row--views > .cn-actions-bar__search--toolbar {
378
+ flex: 1 1 220px;
379
+ min-width: 180px;
380
+ }
381
+
382
+ .cn-look-board .cn-actions-bar--board > .cn-actions-bar__row--views > .cn-actions-bar__search--toolbar ~ .cn-actions-bar__view-controls {
383
+ margin-inline-start: 0;
384
+ }
385
+
375
386
  /* The search field: 40px, radius 8, not a pill. */
376
387
  .cn-look-board .cn-actions-bar--board .cn-actions-bar__search {
377
388
  flex: 1 1 260px;
@@ -1108,3 +1119,40 @@
1108
1119
  .cn-look-board:has(.cn-index-page) > .app-sidebar__toggle {
1109
1120
  display: none;
1110
1121
  }
1122
+
1123
+ /* The folder pane as a card (DqAfdelingen): 280px, a hairline border, 12px
1124
+ corners, with each folder's count in a grey pill. */
1125
+ .cn-look-board .cn-index-page__folder-pane {
1126
+ flex: 0 0 280px;
1127
+ align-self: flex-start;
1128
+ margin-inline-end: 20px;
1129
+ padding: 14px;
1130
+ border: 1px solid var(--cn-board-hairline);
1131
+ border-radius: var(--cn-board-card-radius, 12px);
1132
+ background: var(--color-main-background);
1133
+ }
1134
+
1135
+ .cn-look-board .cn-index-page__folder-pane .cn-folder-tree__count {
1136
+ display: inline-flex;
1137
+ align-items: center;
1138
+ justify-content: center;
1139
+ min-width: 20px;
1140
+ height: 20px;
1141
+ padding: 0 6px;
1142
+ box-sizing: border-box;
1143
+ border-radius: 10px;
1144
+ background: var(--color-background-dark);
1145
+ color: var(--color-main-text);
1146
+ font-size: 12px;
1147
+ font-weight: 600;
1148
+ }
1149
+
1150
+ /* The selected folder's heading above the list: a 20px title over a soft line. */
1151
+ .cn-look-board .cn-index-page__scope-heading {
1152
+ margin-bottom: 16px;
1153
+ }
1154
+
1155
+ .cn-look-board .cn-index-page__scope-title {
1156
+ font-size: 20px;
1157
+ font-weight: 700;
1158
+ }
package/src/index.js CHANGED
@@ -279,6 +279,7 @@ export {
279
279
  CnSaveViewDialog,
280
280
  CnSchemaFormDialog,
281
281
  CnSearchPage,
282
+ CnSearchSelect,
282
283
  CnSectionsWidget,
283
284
  CnSettingsCard,
284
285
  CnSettingsField,
@@ -270,12 +270,14 @@ export const pageViews = {
270
270
 
271
271
  /**
272
272
  * Whether a user arranges the views for themselves (`userLayout`).
273
+ * Not while Buildiq edits the page (`editingBody`): Buildiq is the
274
+ * layout editor, and it shows and changes the manifest layout.
273
275
  *
274
276
  * @return {boolean}
275
277
  * @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
276
278
  */
277
279
  viewUserLayoutOn() {
278
- return this.userLayout === true && this.hasViews
280
+ return this.userLayout === true && this.hasViews && this.editingBody !== true
279
281
  },
280
282
 
281
283
  /**
@@ -640,6 +642,11 @@ export const pageViews = {
640
642
  this.viewUserLayouts = { ...this.viewUserLayouts, [view.id]: arranged }
641
643
  this.viewUserLayoutsDirty = { ...this.viewUserLayoutsDirty, [view.id]: true }
642
644
  this.$emit('view-layout-change', { view: view.id, layout: arranged })
645
+ // Outside an in-page edit session (the dashboard's edit mode, the
646
+ // detail page's Arrange) no Done will come to save it.
647
+ if (this.isEditing !== true && this.arrangingView !== true) {
648
+ this.saveViewUserLayouts()
649
+ }
643
650
  return
644
651
  }
645
652
  for (const u of updated) {
@@ -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.125.0",
6
+ "version": "2.127.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -115,10 +115,11 @@
115
115
  }
116
116
  },
117
117
  "optionsSource": { "type": "string", "description": "choice: where to read the option list from at runtime, instead of declaring it here. Names a key in the app's own GET /api/setup/status document (dots walk into nested objects, e.g. `profiles`). Entries accept `id` for `value` and `name` for `label`, and a numeric `objectCount` becomes the stat a card shows. Use it when the SERVER owns what is on offer, so the manifest cannot drift from it." },
118
- "display": { "type": "string", "enum": ["select", "cards"], "default": "select", "description": "choice: how the options are rendered. `select` is a dropdown; `cards` is a grid of CnChoiceCards, which shows each option's description and stats. Pick cards when the choice needs explaining." },
118
+ "display": { "type": "string", "enum": ["select", "cards", "rows"], "default": "select", "description": "choice: how the options are rendered. `select` is a dropdown; `cards` is a grid of CnChoiceCards, which shows each option's description and stats; `rows` is CnChoiceCards' rows layout, one radio row per option with its description below the label. Pick cards or rows when the choice needs explaining." },
119
119
  "dependsOn": { "type": "string", "description": "choice: the configKey of a previous choice step this one cascades from. Disabled until the parent is set; its option list is taken from optionsByParent[parentValue] (e.g. organisation-type depends on country)." },
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
+ "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." },
122
123
  "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" } },
123
124
  "multiple": { "type": "boolean", "default": false, "description": "choice: allow multi-select." },
124
125
  "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." },
@@ -541,6 +542,18 @@
541
542
  }
542
543
  },
543
544
  "$defs": {
545
+ "fieldOverride": {
546
+ "type": "object",
547
+ "additionalProperties": true,
548
+ "description": "One field's overrides over its schema-derived descriptor (label, order, hidden, readOnly, widget, enumLabels and more stay free-form). A `widget` that is not one of the form's own widget names mounts the registered component of that name (the v2 registry, then customComponents). 2.126.0 documents the search look of a single select.",
549
+ "properties": {
550
+ "widget": { "type": "string", "minLength": 1 },
551
+ "look": { "type": "string", "enum": ["search"], "description": "`search` draws a single select as CnSearchSelect: a magnifier instead of the chevron, and rows with initials or an icon, the name, a secondary line and a check on the chosen one (DqOverdragen \"Nieuw team\")." },
552
+ "optionSecondary": { "type": "string", "description": "look 'search': the row's secondary line, a template filled from each option (\"{members} leden · {focus}\")." },
553
+ "optionAvatar": { "type": "boolean", "description": "look 'search': draw each option's initials." },
554
+ "optionIcon": { "type": "string", "minLength": 1, "description": "look 'search': one icon for every row." }
555
+ }
556
+ },
544
557
  "breadcrumbCrumb": {
545
558
  "type": "object",
546
559
  "additionalProperties": false,
@@ -1985,6 +1998,7 @@
1985
1998
  "items": { "$ref": "#/$defs/action" },
1986
1999
  "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."
1987
2000
  },
2001
+ "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)." },
1988
2002
  "rowClass": {
1989
2003
  "type": "array",
1990
2004
  "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.",
@@ -2162,6 +2176,39 @@
2162
2176
  }
2163
2177
  }
2164
2178
  },
2179
+ {
2180
+ "description": "record-list built-in widget (CnRecordListWidget): the action under the list, `props.content.footerAction` (2.126.0). Other content keys stay free-form.",
2181
+ "if": {
2182
+ "properties": { "widgetKey": { "const": "record-list" } },
2183
+ "required": ["widgetKey"]
2184
+ },
2185
+ "then": {
2186
+ "properties": {
2187
+ "props": {
2188
+ "type": "object",
2189
+ "properties": {
2190
+ "content": {
2191
+ "type": "object",
2192
+ "properties": {
2193
+ "footerAction": {
2194
+ "type": "object",
2195
+ "additionalProperties": true,
2196
+ "required": ["label"],
2197
+ "description": "One action under the list, also under the empty text (DqBezwaar \"Beroep registreren\"). Its other keys are an action's (`type`, `route`, `form`, ...). While `disabledWhen` holds it is disabled and says `disabledReason` (\"Kan zodra de beslissing is verzonden\"). Label and reason go through the host's translate.",
2198
+ "properties": {
2199
+ "label": { "type": "string", "minLength": 1 },
2200
+ "icon": { "type": "string", "minLength": 1 },
2201
+ "disabledWhen": { "$ref": "#/$defs/visibleWhen" },
2202
+ "disabledReason": { "type": "string" }
2203
+ }
2204
+ }
2205
+ }
2206
+ }
2207
+ }
2208
+ }
2209
+ }
2210
+ }
2211
+ },
2165
2212
  {
2166
2213
  "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.",
2167
2214
  "if": {
@@ -2864,7 +2911,7 @@
2864
2911
  },
2865
2912
  "fieldOverrides": {
2866
2913
  "type": "object",
2867
- "additionalProperties": true,
2914
+ "additionalProperties": { "$ref": "#/$defs/fieldOverride" },
2868
2915
  "description": "open-form: per-field overrides passed to fieldsFromSchema, keyed by field. Recognised keys include label, order, hidden, readOnly and widget."
2869
2916
  },
2870
2917
  "advanced": {
@@ -3201,9 +3248,9 @@
3201
3248
  },
3202
3249
  "op": {
3203
3250
  "type": "string",
3204
- "enum": ["eq", "neq", "gt", "gte", "lt", "lte", "empty", "notEmpty", "in"],
3251
+ "enum": ["eq", "neq", "gt", "gte", "lt", "lte", "empty", "notEmpty", "in", "anyOf"],
3205
3252
  "default": "eq",
3206
- "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). `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
+ "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."
3207
3254
  },
3208
3255
  "value": {
3209
3256
  "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`."
@@ -4006,6 +4053,9 @@
4006
4053
  "additionalProperties": true,
4007
4054
  "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`.",
4008
4055
  "properties": {
4056
+ "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)." },
4057
+ "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)." },
4058
+ "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)." },
4009
4059
  "folders": {
4010
4060
  "type": "array",
4011
4061
  "description": "Explicit folder list for source:'custom', and the place a folder declares its own list layout. Every other folder key stays free-form.",
@@ -4168,6 +4218,11 @@
4168
4218
  ]
4169
4219
  }
4170
4220
  },
4221
+ "sideColumnFrom": {
4222
+ "type": "string",
4223
+ "minLength": 1,
4224
+ "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`."
4225
+ },
4171
4226
  "headerActions": {
4172
4227
  "type": "array",
4173
4228
  "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.",
@@ -4301,7 +4356,7 @@
4301
4356
  },
4302
4357
  "fieldOverrides": {
4303
4358
  "type": "object",
4304
- "additionalProperties": true,
4359
+ "additionalProperties": { "$ref": "#/$defs/fieldOverride" },
4305
4360
  "description": "Per-field overrides merged over the schema-derived field descriptors in the page's built-in Add/Edit form dialog (e.g. `{ \"description\": { \"widget\": \"textarea\" } }`). Which widget a field renders as belongs to the surface, not to the schema. Maps to CnIndexPage / CnDetailPage `fieldOverrides`."
4306
4361
  },
4307
4362
  "createDefaults": {
@@ -4667,10 +4722,24 @@
4667
4722
  "variant": { "type": "string", "enum": ["primary", "secondary"], "default": "secondary" },
4668
4723
  "icon": { "type": "string", "minLength": 1, "description": "MDI icon name." },
4669
4724
  "format": { "type": "string", "enum": ["csv", "excel"], "default": "csv", "description": "For `export`." },
4725
+ "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)." },
4670
4726
  "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)." }
4671
4727
  }
4672
4728
  }
4673
4729
  },
4730
+ "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)." },
4731
+ "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)." },
4732
+ "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"] } } } } ] },
4733
+ "listConfig": {
4734
+ "type": "object",
4735
+ "additionalProperties": true,
4736
+ "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).",
4737
+ "properties": {
4738
+ "subtitleTemplate": { "type": "string", "minLength": 1, "description": "The meta line: `{field}` tokens filled from the row; empty parts and their separators drop." },
4739
+ "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\" }`)."},
4740
+ "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."}}} } ] }
4741
+ }
4742
+ },
4674
4743
  "rowTitle": {
4675
4744
  "type": "string",
4676
4745
  "enum": ["link", "plain"],
@@ -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.
@@ -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
+ }