@conduction/nextcloud-vue 2.66.0 → 2.68.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 (95) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +19 -0
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +10 -3
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +2 -2
  6. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  8. package/dist/esm/components/CnContextMenu/CnContextMenu.vue2.js +37 -26
  9. package/dist/esm/components/CnContextMenu/CnContextMenu.vue2.js.map +1 -1
  10. package/dist/esm/components/CnContextMenu/CnContextMenu.vue3.js +3 -3
  11. package/dist/esm/components/CnContextMenu/CnContextMenu.vue3.js.map +1 -1
  12. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +4 -4
  13. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  15. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +87 -18
  16. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  17. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +6 -1
  18. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  19. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +89 -49
  20. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  21. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +337 -315
  22. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  23. package/dist/esm/components/CnIndexPage/defaultActions.js +6 -7
  24. package/dist/esm/components/CnIndexPage/defaultActions.js.map +1 -1
  25. package/dist/esm/components/CnRowActions/CnRowActions.vue2.js +33 -34
  26. package/dist/esm/components/CnRowActions/CnRowActions.vue2.js.map +1 -1
  27. package/dist/esm/components/CnRowActions/CnRowActions.vue3.js +3 -3
  28. package/dist/esm/components/CnRowActions/CnRowActions.vue3.js.map +1 -1
  29. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +13 -13
  30. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  31. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  32. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +3 -2
  33. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
  34. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
  35. package/dist/esm/composables/useContextMenu.js +4 -2
  36. package/dist/esm/composables/useContextMenu.js.map +1 -1
  37. package/dist/esm/composables/useDependencyCheck.js +44 -1
  38. package/dist/esm/composables/useDependencyCheck.js.map +1 -1
  39. package/dist/esm/composables/useListView.js +43 -1
  40. package/dist/esm/composables/useListView.js.map +1 -1
  41. package/dist/esm/composables/useSetupStatus.js +26 -3
  42. package/dist/esm/composables/useSetupStatus.js.map +1 -1
  43. package/dist/esm/l10n/en.json.js +4 -1
  44. package/dist/esm/l10n/en.json.js.map +1 -1
  45. package/dist/esm/l10n/nl.json.js +4 -1
  46. package/dist/esm/l10n/nl.json.js.map +1 -1
  47. package/dist/esm/nextcloud-vue.css +20 -2
  48. package/dist/esm/store/plugins/liveUpdates.js +19 -9
  49. package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
  50. package/dist/esm/store/useObjectStore.js +20 -9
  51. package/dist/esm/store/useObjectStore.js.map +1 -1
  52. package/dist/esm/utils/resolveRowActions.js +130 -0
  53. package/dist/esm/utils/resolveRowActions.js.map +1 -0
  54. package/dist/esm/utils/rowActionItem.js +104 -0
  55. package/dist/esm/utils/rowActionItem.js.map +1 -0
  56. package/dist/esm/utils/timelineEvents.js +44 -1
  57. package/dist/esm/utils/timelineEvents.js.map +1 -1
  58. package/dist/esm/utils/validateManifest.js +59 -3
  59. package/dist/esm/utils/validateManifest.js.map +1 -1
  60. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  61. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  62. package/dist/nextcloud-vue.cjs.js +1431 -802
  63. package/dist/nextcloud-vue.cjs.js.map +2 -2
  64. package/dist/nextcloud-vue.css +20 -2
  65. package/l10n/en.json +4 -1
  66. package/l10n/nl.json +4 -1
  67. package/package.json +1 -1
  68. package/scripts/.jsdoc-baselines.json +2 -2
  69. package/src/components/CnActionsBar/CnActionsBar.md +2 -1
  70. package/src/components/CnActionsBar/CnActionsBar.vue +25 -1
  71. package/src/components/CnAppNav/CnAppNav.vue +2 -2
  72. package/src/components/CnContextMenu/CnContextMenu.vue +40 -29
  73. package/src/components/CnDataTable/CnDataTable.md +1 -1
  74. package/src/components/CnDataTable/CnDataTable.vue +4 -4
  75. package/src/components/CnFormDialog/CnFormDialog.vue +92 -18
  76. package/src/components/CnIndexPage/CnIndexPage.md +23 -1
  77. package/src/components/CnIndexPage/CnIndexPage.vue +108 -52
  78. package/src/components/CnIndexPage/defaultActions.js +6 -7
  79. package/src/components/CnRowActions/CnRowActions.vue +36 -37
  80. package/src/components/CnSetupWizard/CnSetupWizard.vue +13 -13
  81. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +3 -2
  82. package/src/composables/useContextMenu.js +4 -2
  83. package/src/composables/useDependencyCheck.js +43 -0
  84. package/src/composables/useListView.js +43 -1
  85. package/src/composables/useSetupStatus.js +26 -3
  86. package/src/css/actions-bar.css +20 -2
  87. package/src/schemas/app-manifest-v2.schema.json +48 -10
  88. package/src/schemas/app-manifest.schema.json +1 -1
  89. package/src/store/plugins/liveUpdates.js +19 -9
  90. package/src/store/useObjectStore.js +19 -8
  91. package/src/utils/resolveRowActions.js +127 -0
  92. package/src/utils/rowActionItem.js +100 -0
  93. package/src/utils/timelineEvents.js +43 -0
  94. package/src/utils/validateManifest.js +60 -3
  95. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/nextcloud-vue.cjs.js.map
3
- size: 35355767 bytes
4
- sha256: e048d3627c1eba03f90228c9f300e9341baf3086530119fe40b4af2106f5b7b2
3
+ size: 35437389 bytes
4
+ sha256: a49cebe981b616ba6555ce135455260b6b111ebcea9afc9c34da210cbc38f32b
@@ -1940,6 +1940,23 @@ a.cn-kpi-card--stacked:focus-visible {
1940
1940
  color: var(--color-text-maxcontrast);
1941
1941
  }
1942
1942
 
1943
+ /* Beside the inline search (`showCountWithSearch`): the search field shrinks
1944
+ first and the counter stays on one line. */
1945
+ .cn-actions-bar__count--beside-search {
1946
+ flex-shrink: 0;
1947
+ white-space: nowrap;
1948
+ }
1949
+
1950
+ /* Taken out of the flex row so it adds no gap, but kept in the accessibility
1951
+ tree so the live region exists before a total appears. */
1952
+ .cn-actions-bar__count--empty {
1953
+ position: absolute;
1954
+ width: 1px;
1955
+ height: 1px;
1956
+ overflow: hidden;
1957
+ clip-path: inset(50%);
1958
+ }
1959
+
1943
1960
  /* Inline search field */
1944
1961
  .cn-actions-bar__search {
1945
1962
  position: relative;
@@ -2199,8 +2216,9 @@ a.cn-kpi-card--stacked:focus-visible {
2199
2216
  display: contents;
2200
2217
  }
2201
2218
 
2202
- /* #after-search filter buttons + the view toggle ride beside the
2203
- search field (DOM order — search first via its own order). */
2219
+ /* #after-search filter buttons, the beside-search counter and the view
2220
+ toggle ride beside the search field (DOM order — search first via its
2221
+ own order). */
2204
2222
  .cn-actions-bar__info > * {
2205
2223
  order: 1;
2206
2224
  }
package/l10n/en.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "translations": {
3
+ "Advanced": "Advanced",
3
4
  "The item could not be created.": "The item could not be created.",
4
5
  "Start over": "Start over",
5
6
  "Continue where you left off": "Continue where you left off",
@@ -1100,7 +1101,9 @@
1100
1101
  "Show the date of today above the heading": "Show the date of today above the heading",
1101
1102
  "Plain header without a coloured background": "Plain header without a coloured background",
1102
1103
  "Loading…": "Loading…",
1103
- "A register and schema are required": "A register and schema are required"
1104
+ "A register and schema are required": "A register and schema are required",
1105
+ "Change the search or try again.": "Change the search or try again.",
1106
+ "Try again later.": "Try again later."
1104
1107
  },
1105
1108
  "plurals": {
1106
1109
  "%n year": ["%n year", "%n years"],
package/l10n/nl.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "translations": {
3
+ "Advanced": "Geavanceerd",
3
4
  "The item could not be created.": "Het item kon niet worden aangemaakt.",
4
5
  "Start over": "Opnieuw beginnen",
5
6
  "Continue where you left off": "Ga verder waar je was",
@@ -1144,7 +1145,9 @@
1144
1145
  "Show the date of today above the heading": "Toon de datum van vandaag boven de kop",
1145
1146
  "Plain header without a coloured background": "Eenvoudige kop zonder gekleurde achtergrond",
1146
1147
  "Loading…": "Laden…",
1147
- "A register and schema are required": "Een register en schema zijn verplicht"
1148
+ "A register and schema are required": "Een register en schema zijn verplicht",
1149
+ "Change the search or try again.": "Pas de zoekopdracht aan of probeer het opnieuw.",
1150
+ "Try again later.": "Probeer het later opnieuw."
1148
1151
  },
1149
1152
  "plurals": {
1150
1153
  "%n year": ["%n jaar", "%n jaar"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.66.0",
3
+ "version": "2.68.0",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
6
6
  "bugs": {
@@ -111,7 +111,7 @@
111
111
  "CnIconPicker": 1,
112
112
  "CnImageWidget": 1,
113
113
  "CnImageWidgetForm": 1,
114
- "CnIndexPage": 0.74,
114
+ "CnIndexPage": 0.79,
115
115
  "CnIndexSidebar": 0.69,
116
116
  "CnInfoWidget": 1,
117
117
  "CnIntegrationCard": 1,
@@ -202,7 +202,7 @@
202
202
  "CnRichSubmitDialog": 1,
203
203
  "CnRoadmapItem": 1,
204
204
  "CnRoadmapTab": 1,
205
- "CnRowActions": 0.8,
205
+ "CnRowActions": 1,
206
206
  "CnSavedViewsControl": 1,
207
207
  "CnSaveViewDialog": 1,
208
208
  "CnSchemaFormDialog": 0.79,
@@ -152,9 +152,10 @@ Controlling the inline action button count — `inlineActionCount` sets how many
152
152
  | `showMap` | `Boolean` | `false` | Whether to render the third "Map" view-toggle segment. Off by default so existing two-segment consumers are unchanged |
153
153
  | `mapLabel` | `String` | `''` | Label for the map view-toggle option (defaults to "Map"). Only shown when `showMap` |
154
154
  | `mapIcon` | `String` | `''` | MDI icon name for the map option (defaults to the built-in map-marker icon). Resolved via `CnIcon` |
155
- | `showSearch` | `Boolean` | `false` | Whether to show the inline search field on the left of the bar |
155
+ | `showSearch` | `Boolean` | `false` | Whether to show the inline search field on the left of the bar. The field takes the place of the "Showing X of Y" counter unless `showCountWithSearch` is set |
156
156
  | `searchValue` | `String` | `''` | Current value of the inline search field (controlled) |
157
157
  | `searchPlaceholder` | `String` | `''` | Placeholder / accessible label for the inline search field |
158
+ | `showCountWithSearch` | `Boolean` | `false` | Keep the "Showing X of Y" counter visible beside the inline search field, after the search and any `#after-search` controls. No effect without `showSearch` |
158
159
  | `headerActions` | `Array` | `[]` | Manifest-declared page-level actions rendered inside the overflow dropdown between the built-in Refresh and the `#action-items` slot. Each entry is `{ id, label, icon?, disabled? }` — the bar emits `@header-action({ action: id, id })` on click and the parent (e.g. `CnIndexPage`) dispatches the resolved handler. The `icon` field accepts EITHER an MDI Vue component name (e.g. `'History'`) — rendered via `CnIcon` — OR a Nextcloud core CSS icon class (e.g. `'icon-history'`) — rendered as a `<span>` carrying that class. An entry with `href` (URL) or `to` (vue-router location) renders as a real link, `linkTarget` as its `target`; it still emits `@header-action`, so the parent must not navigate for it again. |
159
160
 
160
161
  ## Manifest header actions example
@@ -12,11 +12,16 @@
12
12
  :aria-label="searchPlaceholder || t('nextcloud-vue', 'Search')"
13
13
  @input="onSearchInput">
14
14
  </div>
15
- <span v-else-if="pagination && pagination.total > 0" class="cn-actions-bar__count">
15
+ <span v-else-if="hasTotal" class="cn-actions-bar__count">
16
16
  {{ countText }}
17
17
  </span>
18
18
  <!-- @slot after-search Refinement controls rendered beside the search field on the LEFT side of the bar (e.g. a filter menu button). Convention: the left side groups the VISUAL controls — search, filters, view toggle — while the right cluster holds the ACT controls (add, overflow); the standalone sort select is a display control too but keeps its legacy right-side placement. -->
19
19
  <slot name="after-search" />
20
+ <!-- Counter beside the search (`showCountWithSearch`): after the search and its #after-search controls. The live region stays mounted while there is no total, so results coming back after none are announced too. -->
21
+ <span v-if="showSearch && showCountWithSearch"
22
+ class="cn-actions-bar__count cn-actions-bar__count--beside-search"
23
+ :class="{ 'cn-actions-bar__count--empty': !hasTotal }"
24
+ aria-live="polite">{{ hasTotal ? countText : '' }}</span>
20
25
 
21
26
  <!-- View mode toggle (Cards / Table / List) — segmented control with
22
27
  a sliding thumb that animates between the N segments. Lives in
@@ -644,6 +649,16 @@ export default {
644
649
  default: '',
645
650
  },
646
651
 
652
+ /**
653
+ * Keep the "Showing X of Y" counter visible beside the inline search field,
654
+ * after any `#after-search` controls. By default the search field takes the
655
+ * counter's place. No effect without `showSearch`.
656
+ */
657
+ showCountWithSearch: {
658
+ type: Boolean,
659
+ default: false,
660
+ },
661
+
647
662
  /** Whether the refresh action is currently in progress */
648
663
  refreshing: {
649
664
  type: Boolean,
@@ -811,6 +826,15 @@ export default {
811
826
  }))
812
827
  },
813
828
 
829
+ /**
830
+ * Whether there is a non-empty total for the "Showing X of Y" counter.
831
+ *
832
+ * @return {boolean}
833
+ */
834
+ hasTotal() {
835
+ return Boolean(this.pagination && this.pagination.total > 0)
836
+ },
837
+
814
838
  countText() {
815
839
  if (!this.pagination) {
816
840
  return ''
@@ -1113,7 +1113,7 @@ export default {
1113
1113
 
1114
1114
  /**
1115
1115
  * Label for the foldout's gear button. Manifest override:
1116
- * `nav.settingsLabel`; defaults to "Settings".
1116
+ * `nav.settingsLabel`; defaults to "Advanced".
1117
1117
  *
1118
1118
  * @return {string}
1119
1119
  */
@@ -1122,7 +1122,7 @@ export default {
1122
1122
  if (typeof custom === 'string' && custom.length > 0) {
1123
1123
  return this.effectiveTranslate(custom)
1124
1124
  }
1125
- return t('nextcloud-vue', 'Settings')
1125
+ return t('nextcloud-vue', 'Advanced')
1126
1126
  },
1127
1127
 
1128
1128
  /**
@@ -12,7 +12,7 @@
12
12
  @close="onClose"
13
13
  @closed="onClosed">
14
14
  <!-- Dynamic actions from array prop -->
15
- <template v-for="{ action, link } in renderedActions" :key="action.label">
15
+ <template v-for="{ action, link } in renderedActions" :key="actionKey(action)">
16
16
  <!-- A navigate-only action is a real link, so it can be middle-clicked, opened in a new tab or copied. -->
17
17
  <NcActionLink
18
18
  v-if="link"
@@ -20,7 +20,7 @@
20
20
  :target="link.target"
21
21
  :title="resolveTitle(action)"
22
22
  :class="{ 'cn-row-action--destructive': action.destructive }"
23
- :data-testid="`cn-action-item-${slugifyLabel(action.label)}`"
23
+ :data-testid="actionTestId(action)"
24
24
  closeAfterClick
25
25
  @click="onLinkAction(action, link, $event)">
26
26
  <template v-if="action.icon" #icon>
@@ -34,7 +34,7 @@
34
34
  :title="resolveTitle(action)"
35
35
  :disabled="resolveDisabled(action)"
36
36
  :class="{ 'cn-row-action--destructive': action.destructive }"
37
- :data-testid="`cn-action-item-${slugifyLabel(action.label)}`"
37
+ :data-testid="actionTestId(action)"
38
38
  closeAfterClick
39
39
  @click="onAction(action)">
40
40
  <template v-if="action.icon" #icon>
@@ -95,6 +95,7 @@ import { NcActionButton, NcActionLink, NcActions } from '@nextcloud/vue'
95
95
  import { CTX_MENU_DATA_ATTR, CTX_MENU_POPPER_ATTR } from '../../composables/useContextMenu.js'
96
96
  import { followItemActionLink, resolveItemActionLink } from '../../utils/actionLink.js'
97
97
  import { isModifiedClick } from '../../utils/linkNavigation.js'
98
+ import { isRowActionVisible, rowActionKey, rowActionPayload, rowActionTestId, slugifyActionLabel } from '../../utils/rowActionItem.js'
98
99
  import { CnIcon } from '../CnIcon/index.js'
99
100
 
100
101
  /**
@@ -109,12 +110,13 @@ import { CnIcon } from '../CnIcon/index.js'
109
110
  * cursor positioning). The composable handles the DOM attributes; this component
110
111
  * handles the NcActions template boilerplate.
111
112
  *
112
- * Dynamic actions (CnIndexPage pattern)
113
+ * Dynamic actions (CnIndexPage pattern).
114
+ * CnIndexPage passes the same per-row list its row actions menu renders, so the two menus always match.
113
115
  * ```vue
114
116
  * <CnContextMenu
115
117
  * v-model:open="contextMenuOpen"
116
- * :actions="mergedActions"
117
- * :target-item="contextMenuRow"
118
+ * :actions="rowActionsFor(contextMenuShownRow)"
119
+ * :target-item="contextMenuShownRow"
118
120
  * @action="$emit('action', $event)"
119
121
  * @close="closeContextMenu" />
120
122
  * ```
@@ -161,13 +163,15 @@ export default {
161
163
  * `visible` (boolean | (targetItem) => boolean) hides the entry when falsy
162
164
  * (default: shown). `title` (string | (targetItem) => string) renders as
163
165
  * a native tooltip — useful for explaining why an entry is disabled.
166
+ * A locally decidable `visibleWhen` that is false for the target hides the entry too, as in CnRowActions.
167
+ * `id` is passed in the `action` payload; `builtin` marks a CnIndexPage built-in, whose testid is `cn-action-item-<id>` in every locale.
164
168
  * `href` (URL) or `to` (vue-router location), each a value or a
165
169
  * `(targetItem) => …` function, render the entry as a real link, with
166
170
  * `linkTarget` as its `target`; a link emits `action` but does not call
167
171
  * `handler`. When the entire array is empty (or all entries are filtered
168
172
  * out), only the default slot content is rendered.
169
173
  *
170
- * @type {Array<{label: string, icon: object | string, handler: (targetItem: object) => void, disabled: boolean | ((targetItem: object) => boolean), visible: boolean | ((targetItem: object) => boolean), title: string | ((targetItem: object) => string), destructive: boolean, href: string | ((targetItem: object) => string), to: string | object | ((targetItem: object) => string | object), linkTarget: string}>}
174
+ * @type {Array<{label: string, id: string, builtin: boolean, icon: object | string, handler: (targetItem: object) => void, disabled: boolean | ((targetItem: object) => boolean), visible: boolean | ((targetItem: object) => boolean), visibleWhen: object, title: string | ((targetItem: object) => string), destructive: boolean, href: string | ((targetItem: object) => string), to: string | object | ((targetItem: object) => string | object), linkTarget: string}>}
171
175
  */
172
176
  actions: {
173
177
  type: Array,
@@ -209,21 +213,13 @@ export default {
209
213
 
210
214
  computed: {
211
215
  /**
212
- * Filter actions by their `visible` predicate. Entries without
213
- * `visible` are always shown (backwards compatible).
216
+ * Filter actions by their `visibleWhen` and `visible` gates, with the same rule CnRowActions uses.
217
+ * Entries without either always show.
214
218
  *
215
219
  * @return {Array} Visible actions for the current targetItem.
216
220
  */
217
221
  visibleActions() {
218
- return this.actions.filter((action) => {
219
- if (action.visible === undefined) {
220
- return true
221
- }
222
- if (typeof action.visible === 'function') {
223
- return !!action.visible(this.targetItem)
224
- }
225
- return !!action.visible
226
- })
222
+ return this.actions.filter((action) => isRowActionVisible(action, this.targetItem))
227
223
  },
228
224
 
229
225
  /**
@@ -466,18 +462,33 @@ export default {
466
462
 
467
463
  /**
468
464
  * Slugify an action label for use in stable `data-testid` selectors.
469
- * Lowercase, kebab-case, strip non-alphanumeric. Used solely by the
470
- * `:data-testid` binding on NcActionButton — does not affect runtime
471
- * behaviour or rendered text.
465
+ * Lowercase, kebab-case, strip non-alphanumeric.
472
466
  *
473
467
  * @param {string} label The action's display label.
474
468
  * @return {string} kebab-case slug suitable for a testid suffix.
475
469
  */
476
470
  slugifyLabel(label) {
477
- return String(label || '')
478
- .toLowerCase()
479
- .replace(/[^a-z0-9]+/g, '-')
480
- .replace(/^-+|-+$/g, '')
471
+ return slugifyActionLabel(label)
472
+ },
473
+
474
+ /**
475
+ * The entry's `data-testid`: `cn-action-item-<id>` for a built-in, the label slug otherwise.
476
+ *
477
+ * @param {object} action The action descriptor.
478
+ * @return {string} The testid.
479
+ */
480
+ actionTestId(action) {
481
+ return rowActionTestId(action)
482
+ },
483
+
484
+ /**
485
+ * The entry's render key: `builtin:<id>` for a built-in, the label otherwise.
486
+ *
487
+ * @param {object} action The action descriptor.
488
+ * @return {string} The key.
489
+ */
490
+ actionKey(action) {
491
+ return rowActionKey(action)
481
492
  },
482
493
 
483
494
  onAction(action) {
@@ -485,10 +496,10 @@ export default {
485
496
  action.handler(this.targetItem)
486
497
  }
487
498
  /**
488
- * @event action User picked an entry from the menu. The action's own `handler(targetItem)` (when present) ran synchronously before this event fires; the event lets parents observe / log the choice.
489
- * @type {{ action: string, row: object|null }}
499
+ * @event action User picked an entry from the menu. The action's own `handler(targetItem)` (when present) ran synchronously before this event fires; the event lets parents observe / log the choice. Payload: `action` (the label), the target as `row`, the action's `id` when it has one, and `builtin: true` for a CnIndexPage built-in.
500
+ * @type {{ action: string, row: object|null, id?: string, builtin?: boolean }}
490
501
  */
491
- this.$emit('action', { action: action.label, row: this.targetItem })
502
+ this.$emit('action', rowActionPayload(action, this.targetItem))
492
503
  },
493
504
 
494
505
  /**
@@ -506,7 +517,7 @@ export default {
506
517
  return
507
518
  }
508
519
  followItemActionLink(event, link, this.$router)
509
- this.$emit('action', { action: action.label, row: this.targetItem })
520
+ this.$emit('action', rowActionPayload(action, this.targetItem))
510
521
  },
511
522
 
512
523
  onClose() {
@@ -186,7 +186,7 @@ export default {
186
186
  | `includeColumns` | Array | `null` | Column keys to include (whitelist) in schema mode |
187
187
  | `sortKey` | String | `null` | Currently active sort column key (controlled) |
188
188
  | `sortOrder` | String | `'asc'` | Current sort direction: `'asc'` or `'desc'` |
189
- | `sortKeys` | Array | `[]` | Ordered multi-column ("shift+click") sort key list, `[{ key, order }, …]` (0–3 entries); takes precedence over `sortKey`/`sortOrder` when non-empty. Priority badges show only when two or more rendered, sortable columns are sorted; a key whose column is not rendered or not sortable (such as a `_uuid` tie-break) still sorts, but is not counted or numbered |
189
+ | `sortKeys` | Array | `[]` | Ordered multi-column ("shift+click") sort key list, `[{ key, order }, …]` (0–3 entries); takes precedence over `sortKey`/`sortOrder` when non-empty. Priority badges show only when two or more rendered, sortable columns are sorted; a key whose column is not rendered or not sortable (such as a `_uuid` tie-break) still sorts, but is not counted or numbered, and `aria-sort` goes to the first sort key whose column is rendered and sortable |
190
190
  | `selectedIds` | Array | `[]` | Array of selected row IDs (controlled) |
191
191
  | `rowClass` | Function | `null` | `(row) => string` — CSS class(es) applied to each `<tr>` |
192
192
  | `rowClickToView` | Boolean | `false` | Emit `row-click` on a row-body click even while `selectable` (selection then via the checkbox column only) |
@@ -371,7 +371,7 @@ import '../../css/table.css'
371
371
  *
372
372
  * Sorting: a plain click on a sortable header is single-sort (cycle asc → desc → cleared), unchanged from before.
373
373
  * Shift+click (or Shift+Enter on a focused header) appends the column as a secondary/tertiary sort key, capped at 3, with numbered priority badges (1, 2, 3) once two or more rendered, sortable columns are sorted.
374
- * A sort key whose column is not rendered, or not sortable, still sorts, but is not counted or numbered.
374
+ * A sort key whose column is not rendered, or not sortable, still sorts, but is not counted or numbered, and `aria-sort` goes to the first sort key whose column is rendered and sortable.
375
375
  * Pass `sortKeys: [{key, order}, ...]` for multi-sort (falls back to the legacy `sortKey`/`sortOrder` props when empty).
376
376
  * The `sort` event payload is extended, not replaced: `{key, order}` still mirrors the primary key exactly as before; a new `keys` field carries the full ordered list.
377
377
  * See `src/utils/multiColumnSort.js` for the state machine.
@@ -1722,8 +1722,8 @@ export default {
1722
1722
  },
1723
1723
 
1724
1724
  /**
1725
- * `aria-sort` value for a column header: `'ascending'`/`'descending'` for the PRIMARY (index 0) active sort key only, per WCAG guidance that `aria-sort` describes single-column sort state.
1726
- * `null` omits the attribute entirely (unsorted, not sortable, or not the primary key).
1725
+ * `aria-sort` value for a column header: `'ascending'`/`'descending'` for the first rendered sort key only, per WCAG guidance that `aria-sort` describes single-column sort state.
1726
+ * `null` omits the attribute entirely (unsorted, not sortable, or not the first rendered sort key).
1727
1727
  *
1728
1728
  * @param {object} col Column definition.
1729
1729
  * @return {string|null}
@@ -1732,7 +1732,7 @@ export default {
1732
1732
  if (!col.sortable) {
1733
1733
  return null
1734
1734
  }
1735
- const primary = this.effectiveSortKeys[0]
1735
+ const primary = this.renderedSortKeys[0]
1736
1736
  if (!primary || primary.key !== col.key) {
1737
1737
  return null
1738
1738
  }
@@ -462,6 +462,11 @@
462
462
  :register="nestedCreate.register"
463
463
  :item="null"
464
464
  :initialData="nestedCreate.initialData"
465
+ :excludeFields="nestedCreate.form.excludeFields"
466
+ :includeFields="nestedCreate.form.includeFields"
467
+ :fieldOverrides="nestedCreate.form.fieldOverrides"
468
+ :size="nestedCreate.form.size"
469
+ :columns="nestedCreate.form.columns"
465
470
  :recoverDraft="false"
466
471
  data-testid="cn-form-dialog-nested-create"
467
472
  @confirm="onNestedCreateConfirm"
@@ -2645,7 +2650,12 @@ export default {
2645
2650
  * the new object. When the referenced schema cannot be loaded, the
2646
2651
  * term alone is saved as before, so the field never dead-ends.
2647
2652
  *
2653
+ * The form is the one the schema's index page shows for its own Add
2654
+ * button: that page's `createOverride` saves it and its form settings
2655
+ * shape it, with the typed term in the name.
2656
+ *
2648
2657
  * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
2658
+ * @spec openspec/changes/nested-create-uses-page-config/specs/schema-utilities/spec.md
2649
2659
  * @param {object} field The reference field descriptor.
2650
2660
  * @param {string} term The typed search term.
2651
2661
  * @return {Promise<object|null>} The created object, or null.
@@ -2662,21 +2672,37 @@ export default {
2662
2672
  store.registerObjectType(slug, schemaRef, register)
2663
2673
  }
2664
2674
  const labelField = this.referenceLabelField(field)
2675
+ const loadSchema = async () => {
2676
+ try {
2677
+ return await store.fetchSchema(slug)
2678
+ } catch {
2679
+ return null
2680
+ }
2681
+ }
2665
2682
  // The app may own how this schema is created (pipelinq's client and
2666
2683
  // contact need a contact-first endpoint; a plain save 400s). A
2667
2684
  // create override keeps this form and saves through the app's
2668
2685
  // handler; a create modal (only when no override exists) opens the
2669
2686
  // app's own dialog instead of the generic form.
2670
- const appCreate = this.appCreateFor(register, schemaRef)
2687
+ //
2688
+ // A stored schema usually names its reference by id (`$ref: 28`)
2689
+ // while the manifest page names it by slug (`schema: 'client'`), so
2690
+ // when the reference itself matches no page, the referenced schema
2691
+ // is loaded and its slug, id and uuid are tried too.
2692
+ let schema
2693
+ let appCreate = this.appCreateFor(register, [schemaRef])
2694
+ if (!appCreate.page) {
2695
+ schema = await loadSchema()
2696
+ if (schema) {
2697
+ appCreate = this.appCreateFor(register, [schemaRef, schema.slug, schema.id, schema.uuid])
2698
+ }
2699
+ }
2671
2700
  if (!appCreate.override && appCreate.modal) {
2672
2701
  return this.openAppCreateModal(appCreate.modal, store, slug, labelField, term)
2673
2702
  }
2674
2703
  const override = appCreate.override
2675
- let schema = null
2676
- try {
2677
- schema = await store.fetchSchema(slug)
2678
- } catch {
2679
- schema = null
2704
+ if (schema === undefined) {
2705
+ schema = await loadSchema()
2680
2706
  }
2681
2707
  if (!schema || !schema.properties) {
2682
2708
  const payload = { [labelField]: term }
@@ -2685,36 +2711,84 @@ export default {
2685
2711
  : store.saveObject(slug, payload)
2686
2712
  }
2687
2713
  const initialData = {}
2688
- if (term && schema.properties[labelField]) {
2689
- initialData[labelField] = term
2714
+ const prefillKey = this.nestedPrefillKey(schema, labelField)
2715
+ if (term && prefillKey) {
2716
+ initialData[prefillKey] = term
2690
2717
  }
2718
+ const form = appCreate.form || this.nestedFormConfig(null)
2691
2719
  return new Promise((resolve) => {
2692
- this.nestedCreate = { field, schema, register, slug, schemaRef, initialData, override, resolve }
2720
+ this.nestedCreate = { field, schema, register, slug, schemaRef, initialData, override, form, resolve }
2693
2721
  })
2694
2722
  },
2695
2723
 
2724
+ /**
2725
+ * The property of the referenced schema the typed term belongs in: the
2726
+ * reference's label field when the schema has it, else its name, title
2727
+ * or label. Visibility is decided by the form (a page `fieldOverrides`
2728
+ * entry can make a read-only name editable), not here.
2729
+ *
2730
+ * @spec openspec/changes/nested-create-uses-page-config/specs/schema-utilities/spec.md
2731
+ * @param {object} schema The referenced schema.
2732
+ * @param {string} labelField The reference's label field.
2733
+ * @return {string|null} The property key, or null when none fits.
2734
+ */
2735
+ nestedPrefillKey(schema, labelField) {
2736
+ const properties = (schema && schema.properties) || {}
2737
+ return [labelField, 'name', 'title', 'label'].find((key) => properties[key]) || null
2738
+ },
2739
+
2740
+ /**
2741
+ * The create-form settings of the index page that lists a schema, in
2742
+ * the shape the nested CnFormDialog binds: the same `excludeFields`,
2743
+ * `includeFields`, `fieldOverrides`, `formSize` and `formColumns`
2744
+ * CnIndexPage hands its own Add form, so creating from a picker shows
2745
+ * the form the page shows. Without a page, the form's defaults.
2746
+ *
2747
+ * @spec openspec/changes/nested-create-uses-page-config/specs/schema-utilities/spec.md
2748
+ * @param {object|null} config The page's `config`, or null.
2749
+ * @return {{excludeFields: string[], includeFields: (string[]|null), fieldOverrides: object, size: string, columns: number}} The form settings.
2750
+ */
2751
+ nestedFormConfig(config) {
2752
+ const c = config || {}
2753
+ const isMap = (v) => v !== null && typeof v === 'object' && !Array.isArray(v)
2754
+ return {
2755
+ excludeFields: Array.isArray(c.excludeFields) ? c.excludeFields : [],
2756
+ includeFields: Array.isArray(c.includeFields) ? c.includeFields : null,
2757
+ fieldOverrides: isMap(c.fieldOverrides) ? c.fieldOverrides : {},
2758
+ size: typeof c.formSize === 'string' && c.formSize !== '' ? c.formSize : 'normal',
2759
+ columns: c.formColumns === 2 ? 2 : 1,
2760
+ }
2761
+ },
2762
+
2696
2763
  /**
2697
2764
  * How the app creates objects of a schema, read off the manifest page
2698
2765
  * that lists them: the index page whose `config.schema` (and, when it
2699
2766
  * names one, `config.register`) matches. Its `config.createOverride` is
2700
2767
  * resolved to the registered handler; its `config.createModal` is the
2701
- * registry key of a `kind: 'modal'` dialog.
2768
+ * registry key of a `kind: 'modal'` dialog; its form settings
2769
+ * (`fieldOverrides` and friends) shape the nested form.
2702
2770
  *
2703
- * @spec openspec/changes/review-round-two/specs/schema-utilities/spec.md
2771
+ * The schema is matched on any of the given references, so a caller
2772
+ * holding the referenced schema can pass its slug, id and uuid: the
2773
+ * manifest names a schema by slug, a stored `$ref` often by id.
2774
+ *
2775
+ * @spec openspec/changes/nested-create-uses-page-config/specs/schema-utilities/spec.md
2704
2776
  * @param {string} register The reference's register.
2705
- * @param {string} schemaRef The reference's schema slug or id.
2706
- * @return {{override: (((formData: object, ctx: object) => Promise<object>)|null), modal: (string|null)}} What the app declared.
2777
+ * @param {string|Array<string|number>} schemaRefs The schema's slug, id or uuid, one or several.
2778
+ * @return {{page: (object|null), override: (((formData: object, ctx: object) => Promise<object>)|null), modal: (string|null), form: (object|null)}} What the app declared.
2707
2779
  */
2708
- appCreateFor(register, schemaRef) {
2709
- const none = { override: null, modal: null }
2780
+ appCreateFor(register, schemaRefs) {
2781
+ const none = { page: null, override: null, modal: null, form: null }
2710
2782
  const pages = this.cnManifest && Array.isArray(this.cnManifest.pages) ? this.cnManifest.pages : []
2711
- const wanted = String(schemaRef).toLowerCase()
2783
+ const wanted = (Array.isArray(schemaRefs) ? schemaRefs : [schemaRefs])
2784
+ .filter((ref) => ref !== undefined && ref !== null && ref !== '')
2785
+ .map((ref) => String(ref).toLowerCase())
2712
2786
  const page = pages.find((p) => {
2713
2787
  const config = p && p.config
2714
2788
  if (!config || config.schema === undefined || config.schema === null) {
2715
2789
  return false
2716
2790
  }
2717
- if (String(config.schema).toLowerCase() !== wanted) {
2791
+ if (!wanted.includes(String(config.schema).toLowerCase())) {
2718
2792
  return false
2719
2793
  }
2720
2794
  return !config.register || String(config.register) === String(register)
@@ -2732,7 +2806,7 @@ export default {
2732
2806
  const modal = modalEntry && modalEntry.kind === 'modal' && typeof this.cnOpenModal === 'function'
2733
2807
  ? config.createModal
2734
2808
  : null
2735
- return { override: override || null, modal }
2809
+ return { page, override: override || null, modal, form: this.nestedFormConfig(config) }
2736
2810
  },
2737
2811
 
2738
2812
  /**
@@ -386,7 +386,8 @@ export default {
386
386
  | `fieldOverrides` | Object | `{}` | Per-field config overrides passed to `CnFormDialog` |
387
387
  | `customComponents` | Object | `null` | Custom-component / handler registry. When set, takes precedence over the injected `cnCustomComponents` from CnAppRoot. Used to resolve `actions[].handler` registry names declared in the manifest (manifest-actions-dispatch). Named handlers resolve out of the v2 `registry` (a `kind: "handler"` entry) FIRST and fall back to this map. |
388
388
  | `showViewToggle` | Boolean | `true` | Whether to show the Cards/Table view toggle |
389
- | `inlineSearch` | Boolean | `false` | Show an inline search field in the actions bar (in addition to / instead of the sidebar search). Fed from the manifest as `pages[].config.inlineSearch`. |
389
+ | `inlineSearch` | Boolean | `false` | Show an inline search field in the actions bar (in addition to / instead of the sidebar search). Fed from the manifest as `pages[].config.inlineSearch`. The field takes the place of the "Showing X of Y" counter unless `showCountWithSearch` is set. |
390
+ | `showCountWithSearch` | Boolean | `false` | Keep the "Showing X of Y" counter visible beside the inline search field, after the search and any `#after-search` controls (only relevant with `inlineSearch`, which otherwise takes the counter's place). Fed from the manifest as `pages[].config.showCountWithSearch`. |
390
391
  | `searchPlaceholder` | String | `''` | Placeholder for the inline search field (manifest `config.searchPlaceholder`). |
391
392
  | `cardsLabel` | String | `''` | Label for the cards view-toggle option (manifest `config.cardsLabel`, e.g. "Tiles"). |
392
393
  | `tableLabel` | String | `''` | Label for the table view-toggle option (manifest `config.tableLabel`, e.g. "List"). |
@@ -865,6 +866,27 @@ actions, and the page's declaration stands as before. Wire the field on one
865
866
  list first, then check `rowActionsNotDeclared` to see what the server is
866
867
  offering that you have not declared yet.
867
868
 
869
+ The built-in View, Edit, Copy and Delete carry the ids `view`, `edit`, `copy` and `delete`, and match this block by id only, never by their label.
870
+ The right-click menu reads the same per-row list as the actions menu, so it drops the same refused actions.
871
+
872
+ ## Placing built-in row actions
873
+
874
+ A `"builtin:view"`, `"builtin:edit"`, `"builtin:copy"` or `"builtin:delete"` string in `actions` puts that built-in at its position; enabled built-ins the array does not place are appended in the default order, and the `show*Action` toggles still decide whether each renders.
875
+
876
+ ```json
877
+ "actionToggles": { "showViewAction": false },
878
+ "actions": [
879
+ "builtin:edit",
880
+ "builtin:copy",
881
+ { "id": "file-list", "label": "File list", "handler": "openPublicationFiles" },
882
+ "builtin:delete"
883
+ ]
884
+ ```
885
+
886
+ This renders Edit, Copy, File list, Delete, and makes Edit the keyboard primary action.
887
+ The keyboard primary action is the first entry the menu shows and enables, so a hidden or disabled entry is skipped.
888
+ See the CnIndexPage docs page for the full rules.
889
+
868
890
  ## State indicators on a row
869
891
 
870
892
  Declare `rowIndicators` and a row carries the flags a handler triages on. Each