@conduction/nextcloud-vue 2.56.0-beta.21 → 2.56.0-beta.23

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 (66) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +29 -0
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  4. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +18 -0
  5. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  6. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +9 -6
  7. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  8. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +16 -1
  9. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  10. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +5 -4
  11. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  12. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue2.js +25 -0
  13. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue2.js.map +1 -1
  14. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue3.js +3 -1
  15. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue3.js.map +1 -1
  16. package/dist/esm/components/CnGraphCanvas/CnFlowNode.vue2.js +36 -1
  17. package/dist/esm/components/CnGraphCanvas/CnFlowNode.vue2.js.map +1 -1
  18. package/dist/esm/components/CnGraphCanvas/CnFlowNode.vue3.js.map +1 -1
  19. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue2.js +37 -0
  20. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue2.js.map +1 -1
  21. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue3.js +4 -2
  22. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue3.js.map +1 -1
  23. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +16 -1
  24. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  25. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +6 -6
  26. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  27. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +53 -5
  28. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
  29. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +1 -1
  30. package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
  31. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +133 -0
  32. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
  33. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js +22 -14
  34. package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
  35. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +15 -1
  36. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  37. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +3 -2
  38. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  39. package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue2.js +15 -1
  40. package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue2.js.map +1 -1
  41. package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue3.js +2 -2
  42. package/dist/esm/components/CnWidgetGrid/CnWidgetGrid.vue3.js.map +1 -1
  43. package/dist/esm/index.js +1 -0
  44. package/dist/esm/index.js.map +1 -1
  45. package/dist/esm/utils/notificationRuleLabel.js +132 -0
  46. package/dist/esm/utils/notificationRuleLabel.js.map +1 -0
  47. package/dist/nextcloud-vue.cjs.js +578 -47
  48. package/dist/nextcloud-vue.cjs.js.map +2 -2
  49. package/package.json +1 -1
  50. package/scripts/openregister-reliance.js +206 -0
  51. package/src/components/CnAppRoot/CnAppRoot.md +1 -0
  52. package/src/components/CnAppRoot/CnAppRoot.vue +29 -0
  53. package/src/components/CnDetailPage/CnDetailPage.vue +24 -3
  54. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +22 -4
  55. package/src/components/CnFlowDetail/CnFlowDetail.vue +27 -0
  56. package/src/components/CnGraphCanvas/CnFlowNode.vue +36 -1
  57. package/src/components/CnGraphCanvas/CnGraphCanvas.vue +40 -1
  58. package/src/components/CnIndexPage/CnIndexPage.vue +17 -2
  59. package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +54 -6
  60. package/src/components/CnPageRenderer/CnPageRenderer.vue +147 -6
  61. package/src/components/CnTabsWidget/CnTabsWidget.vue +17 -2
  62. package/src/components/CnWidgetGrid/CnWidgetGrid.vue +19 -3
  63. package/src/index.js +2 -0
  64. package/src/utils/notificationRuleLabel.js +129 -0
  65. package/webpack/index.d.ts +16 -0
  66. package/webpack/index.js +70 -0
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/nextcloud-vue.cjs.js.map
3
- size: 35684577 bytes
4
- sha256: 40220f92c6defbd478644408d9d23a88047c47798fb52eefee28e6d70e0fbc9a
3
+ size: 35731122 bytes
4
+ sha256: 2f00e1767fdb1c72a109ba42b159ca43fb36fbd6a93a33087f4c8529da77b80b
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.56.0-beta.21",
3
+ "version": "2.56.0-beta.23",
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": {
@@ -0,0 +1,206 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+
6
+ /**
7
+ * Scores every component under src/components on how much it relies on
8
+ * OpenRegister and writes the result to docs/components/_generated/reliance.json
9
+ * for the documentation site.
10
+ *
11
+ * Two numbers per component:
12
+ * - direct: lines inside the component's own directory that reference
13
+ * OpenRegister (store imports, API paths, composables, the register and
14
+ * schema selector).
15
+ * - transitive: files outside the directory, reached by following the actual
16
+ * import statements, that carry such a reference. A wrapper with no
17
+ * reference of its own still counts the children it cannot render without,
18
+ * while importing the widget registry alone does not drag in every widget.
19
+ *
20
+ * The tier follows the sum: none (0), light (1 to 3), medium (4 to 10) and
21
+ * heavy (11 and more). A component with tier none does not touch OpenRegister
22
+ * itself nor through anything it imports.
23
+ *
24
+ * Usage: node scripts/openregister-reliance.js
25
+ */
26
+
27
+ const fs = require('fs')
28
+ const path = require('path')
29
+
30
+ /** Patterns that mark a direct OpenRegister reliance; each hit counts once per line. */
31
+ const DIRECT_MARKERS = [
32
+ /\/apps\/openregister/,
33
+ /\bopenregister\b/i,
34
+ /useObjectStore|useRegisterStore|useSchemaStore|createObjectStore|createCrudStore/,
35
+ /from '[^']*\/store(\/index\.js)?'/,
36
+ /from '[^']*\/store\/[^']+'/,
37
+ /objectService|ObjectService/,
38
+ /CnRegisterSchemaSelect/,
39
+ ]
40
+
41
+ const SOURCE_FILE = /\.(vue|js|ts|mjs)$/
42
+ const TEST_FILE = /\.(spec|test)\./
43
+
44
+ /**
45
+ * @param {string} dir directory to walk
46
+ * @return {string[]} source files below it, tests excluded
47
+ */
48
+ function walk(dir) {
49
+ const files = []
50
+ for (const entry of fs.readdirSync(dir)) {
51
+ const full = path.join(dir, entry)
52
+ if (fs.statSync(full).isDirectory()) {
53
+ if (entry !== 'node_modules' && entry !== '__tests__') {
54
+ files.push(...walk(full))
55
+ }
56
+ } else if (SOURCE_FILE.test(entry) && !TEST_FILE.test(entry)) {
57
+ files.push(full)
58
+ }
59
+ }
60
+ return files
61
+ }
62
+
63
+ /**
64
+ * @param {string} source file contents
65
+ * @return {number} lines with a direct marker, comments excluded
66
+ */
67
+ function countDirect(source) {
68
+ let count = 0
69
+ for (const line of source.split('\n')) {
70
+ const trimmed = line.trimStart()
71
+ if (trimmed.startsWith('*') || trimmed.startsWith('//') || trimmed.startsWith('/*')) {
72
+ continue
73
+ }
74
+ if (DIRECT_MARKERS.some((marker) => marker.test(line))) {
75
+ count++
76
+ }
77
+ }
78
+ return count
79
+ }
80
+
81
+ /**
82
+ * Resolves a relative import to a file on disk the way the bundler does.
83
+ *
84
+ * @param {string} fromFile importing file
85
+ * @param {string} specifier the relative import path
86
+ * @return {string|null}
87
+ */
88
+ function resolveImport(fromFile, specifier) {
89
+ const base = path.resolve(path.dirname(fromFile), specifier)
90
+ const candidates = [base, base + '.js', base + '.mjs', base + '.ts', base + '.vue', path.join(base, 'index.js'), path.join(base, 'index.mjs'), path.join(base, 'index.ts')]
91
+ return candidates.find((candidate) => fs.existsSync(candidate) && fs.statSync(candidate).isFile()) ?? null
92
+ }
93
+
94
+ /**
95
+ * @param {string} source file contents
96
+ * @return {string[]} relative import specifiers, static and dynamic
97
+ */
98
+ function relativeImports(source) {
99
+ return [...source.matchAll(/(?:from\s+|import\s*\(\s*|import\s+)'(\.{1,2}\/[^']+)'/g)].map((match) => match[1])
100
+ }
101
+
102
+ /**
103
+ * @param {number} score direct lines plus transitive files
104
+ * @return {'none'|'light'|'medium'|'heavy'}
105
+ */
106
+ function tierOf(score) {
107
+ if (score === 0) {
108
+ return 'none'
109
+ }
110
+ if (score <= 3) {
111
+ return 'light'
112
+ }
113
+ if (score <= 10) {
114
+ return 'medium'
115
+ }
116
+ return 'heavy'
117
+ }
118
+
119
+ /**
120
+ * @param {string} componentName e.g. CnDeleteDialog
121
+ * @return {string} the docs page slug, e.g. cn-delete-dialog
122
+ */
123
+ function pageSlug(componentName) {
124
+ return componentName.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase()
125
+ }
126
+
127
+ /**
128
+ * @param {string} srcDir the src directory holding components/
129
+ * @param {string|null} docsComponentsDir where the component pages live, to link only existing pages
130
+ * @return {Record<string, {direct: number, transitive: number, tier: string, via: string[], page: string|null}>}
131
+ */
132
+ function computeReliance(srcDir, docsComponentsDir = null) {
133
+ const componentsDir = path.join(srcDir, 'components')
134
+ const files = new Map()
135
+ for (const file of walk(srcDir)) {
136
+ const source = fs.readFileSync(file, 'utf8')
137
+ files.set(file, {
138
+ direct: countDirect(source),
139
+ imports: relativeImports(source).map((specifier) => resolveImport(file, specifier)).filter(Boolean),
140
+ })
141
+ }
142
+
143
+ const reachable = (startFiles) => {
144
+ const own = new Set(startFiles)
145
+ const seen = new Set()
146
+ const queue = [...startFiles]
147
+ while (queue.length > 0) {
148
+ const file = queue.pop()
149
+ for (const next of (files.get(file) ?? { imports: [] }).imports) {
150
+ if (!own.has(next) && !seen.has(next)) {
151
+ seen.add(next)
152
+ queue.push(next)
153
+ }
154
+ }
155
+ }
156
+ return [...seen]
157
+ }
158
+
159
+ const result = {}
160
+ for (const entry of fs.readdirSync(componentsDir).sort()) {
161
+ const dir = path.join(componentsDir, entry)
162
+ if (!fs.statSync(dir).isDirectory() || !/^Cn[A-Z]/.test(entry)) {
163
+ continue
164
+ }
165
+ const ownFiles = walk(dir)
166
+ const direct = ownFiles.reduce((sum, file) => sum + (files.get(file)?.direct ?? 0), 0)
167
+ const relyingFiles = reachable(ownFiles).filter((file) => (files.get(file)?.direct ?? 0) > 0)
168
+ const via = new Set(relyingFiles
169
+ .filter((file) => file.startsWith(componentsDir + path.sep))
170
+ .map((file) => file.slice(componentsDir.length + 1).split(path.sep)[0])
171
+ .filter((name) => name !== entry))
172
+ const slug = pageSlug(entry)
173
+ const hasPage = docsComponentsDir !== null && fs.existsSync(path.join(docsComponentsDir, slug + '.md'))
174
+ result[entry] = {
175
+ direct,
176
+ transitive: relyingFiles.length,
177
+ tier: tierOf(direct + relyingFiles.length),
178
+ via: [...via].sort(),
179
+ page: hasPage ? slug : null,
180
+ }
181
+ }
182
+ return result
183
+ }
184
+
185
+ /**
186
+ * @param {string} repoRoot the repository root
187
+ * @return {{outFile: string, components: object}}
188
+ */
189
+ function writeReliance(repoRoot) {
190
+ const components = computeReliance(path.join(repoRoot, 'src'), path.join(repoRoot, 'docs/components'))
191
+ const outFile = path.join(repoRoot, 'docs/components/_generated/reliance.json')
192
+ fs.mkdirSync(path.dirname(outFile), { recursive: true })
193
+ fs.writeFileSync(outFile, JSON.stringify({ generatedBy: 'scripts/openregister-reliance.js', components }, null, '\t') + '\n')
194
+ return { outFile, components }
195
+ }
196
+
197
+ module.exports = { computeReliance, countDirect, tierOf, pageSlug, writeReliance }
198
+
199
+ if (require.main === module) {
200
+ const { outFile, components } = writeReliance(path.resolve(__dirname, '..'))
201
+ const tiers = {}
202
+ for (const item of Object.values(components)) {
203
+ tiers[item.tier] = (tiers[item.tier] ?? 0) + 1
204
+ }
205
+ console.log(`wrote ${path.relative(process.cwd(), outFile)}: ${Object.keys(components).length} components`, tiers)
206
+ }
@@ -78,6 +78,7 @@ With loading state, custom components, translate, permissions, and custom page t
78
78
  | `cellWidgets` | Object | No | `{}` | Cell-widget registry — map of widget-id → Vue component, rendered for a column that declares `pages[].config.columns[].widget`. The component receives `{ value, row, property, formatted, ...widgetProps }`. The library ships one built-in id, `"badge"` (renders `CnStatusBadge`); consumer entries cover everything else (status pills with custom colour maps, inline toggles, link cells, …). Provided to descendant `CnDataTable` / `CnCellRenderer` as `cnCellWidgets`. See `docs/migrating-to-manifest.md` → "Column widgets". |
79
79
  | `kbSearchProviders` | Object | No | `{}` | Pluggable knowledge-base search providers (#91 Wave 3) — map of provider-key → `{ search(query, opts), externalOpen? }`, merged OVER the library built-in `default` (endpoint) provider and provided to descendant `CnKbSearchWidget` as `cnKbSearchProviders`. A `kb-search` widget selects its provider via `content.provider`; an app talking to a bespoke KB backend (the xwiki proxy) registers its client here — the library ships only the seam. |
80
80
  | `translate` | Function | No | `key => key` | Translate function from the consuming app, typically `(key) => t(appId, key)`. Provided to descendants as `cnTranslate` |
81
+ | `notificationLabels` | Object | No | `{}` | Labels for the app's notification rules in the user-settings notification pane, keyed `<schema>.<key>` or `<key>`; values are translated strings or `{ nl, en }` maps. A rule without a label reads as its key split into words ("Case assigned"), never as the raw key. Provided to descendants as `cnNotificationLabels` |
81
82
  | `permissions` | Array | No | `[]` | Permission strings for the current user; forwarded to `CnAppNav` for menu filtering |
82
83
  | `pageTypes` | Object | No | `null` | Page-type registry map (`type → component`). Falls back to the library's `defaultPageTypes`. Extend with `{ ...defaultPageTypes, report: MyReportPage }` |
83
84
  | `userSettingsTitle` | String | No | `''` | Title shown at the top of the hosted `NcAppSettingsDialog`. Empty (the default) resolves to `translate('User settings')`. Override per app to brand the modal (e.g. `'Decidiq preferences'`). |
@@ -996,6 +996,18 @@ export default {
996
996
  * wiring.
997
997
  */
998
998
  cnAppId: this.appId,
999
+ /**
1000
+ * The app's labels for its notification rules, read by the
1001
+ * notification-preferences pane in the user-settings modal. A
1002
+ * getter so a late prop update is still seen by a pane that
1003
+ * mounts after it.
1004
+ *
1005
+ * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md
1006
+ */
1007
+ get cnNotificationLabels() {
1008
+ return self.notificationLabels || {}
1009
+ },
1010
+
999
1011
  /**
1000
1012
  * Target repo slug for the in-product feature-request deep
1001
1013
  * link (e.g. `ConductionNL/pipelinq`). Read from the
@@ -1411,6 +1423,23 @@ export default {
1411
1423
  default: (key) => key,
1412
1424
  },
1413
1425
 
1426
+ /**
1427
+ * Labels for the app's notification rules, shown in the
1428
+ * user-settings notification pane instead of the rule keys. Keyed
1429
+ * `<schema>.<key>` (or just `<key>`); values are already translated
1430
+ * strings, e.g. `{ 'case.caseAssigned': t('dossiq', 'A case is
1431
+ * assigned to me') }`, or per-locale maps `{ nl, en }`. A rule's own
1432
+ * `label` from OpenRegister wins; a rule with no label anywhere reads
1433
+ * as its key split into words, never as the raw key.
1434
+ *
1435
+ * @type {{[key: string]: (string|object)}}
1436
+ * @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md
1437
+ */
1438
+ notificationLabels: {
1439
+ type: Object,
1440
+ default: () => ({}),
1441
+ },
1442
+
1414
1443
  /**
1415
1444
  * List of permission strings the current user holds. Forwarded
1416
1445
  * to CnAppNav's permission filter.
@@ -454,7 +454,8 @@
454
454
  :cnRegistry="cnRegistry"
455
455
  :availableWidgets="widgets"
456
456
  @geoSaved="onGeoSaved"
457
- @openIntegration="onAutoBodyOpenIntegration" />
457
+ @openIntegration="onAutoBodyOpenIntegration"
458
+ @selectObject="onRelatedObjectSelect" />
458
459
  </slot>
459
460
  </div>
460
461
  </div>
@@ -764,7 +765,8 @@
764
765
  :cnRegistry="cnRegistry"
765
766
  :availableWidgets="bodyGridWidgets"
766
767
  @geoSaved="onGeoSaved"
767
- @openIntegration="onAutoBodyOpenIntegration" />
768
+ @openIntegration="onAutoBodyOpenIntegration"
769
+ @selectObject="onRelatedObjectSelect" />
768
770
  </slot>
769
771
  </div>
770
772
  </template>
@@ -975,7 +977,8 @@
975
977
  :cnRegistry="cnRegistry"
976
978
  :availableWidgets="widgets"
977
979
  @geoSaved="onGeoSaved"
978
- @openIntegration="onAutoBodyOpenIntegration" />
980
+ @openIntegration="onAutoBodyOpenIntegration"
981
+ @selectObject="onRelatedObjectSelect" />
979
982
  </slot>
980
983
  </div>
981
984
  </aside>
@@ -2334,6 +2337,7 @@ export default {
2334
2337
  'layout-change',
2335
2338
  'open-integration',
2336
2339
  'refresh',
2340
+ 'related-object-click',
2337
2341
  'related-row-click',
2338
2342
  'relation-linked',
2339
2343
  'request-feature',
@@ -4315,6 +4319,23 @@ export default {
4315
4319
  this.$emit('open-integration', integrationId)
4316
4320
  },
4317
4321
 
4322
+ /**
4323
+ * Re-emit a click on an object in the Related widget. The page does not
4324
+ * know the routes; the host (CnPageRenderer on a manifest page) opens
4325
+ * the object's own detail page.
4326
+ *
4327
+ * @param {object} raw The clicked object; `@self` names its register and schema.
4328
+ */
4329
+ onRelatedObjectSelect(raw) {
4330
+ /**
4331
+ * @event related-object-click An object in the Related widget was
4332
+ * clicked. Payload is the raw object; `@self` carries its register,
4333
+ * schema and id.
4334
+ * @type {object}
4335
+ */
4336
+ this.$emit('related-object-click', raw)
4337
+ },
4338
+
4318
4339
  /**
4319
4340
  * Re-fetch the object after the geo widget persists `@self.geo` (so the
4320
4341
  * rest of the page — metadata, other widgets — reflects the new value),
@@ -114,7 +114,8 @@
114
114
  :includeGroups="content.groups || []"
115
115
  :hideSingleTabTitle="content.hideSingleTabTitle !== false"
116
116
  :showTotalCount="content.showTotalCount !== false"
117
- @openIntegration="onOpenIntegration" />
117
+ @openIntegration="onOpenIntegration"
118
+ @selectObject="onSelectObject" />
118
119
 
119
120
  <!-- `type: 'object-geo'` — view/edit the object's `@self.geo` on a map. -->
120
121
  <CnObjectGeoWidget
@@ -183,7 +184,8 @@
183
184
  <component
184
185
  :is="renderer"
185
186
  ref="renderer"
186
- v-bind="rendererProps" />
187
+ v-bind="rendererProps"
188
+ @selectObject="onSelectObject" />
187
189
  </CnWidgetWrapper>
188
190
 
189
191
  <!-- Registry "card" widgets (stat / gauge / delta) render bare tile
@@ -218,7 +220,8 @@
218
220
  :is="renderer"
219
221
  v-else-if="renderer"
220
222
  ref="renderer"
221
- v-bind="rendererProps" />
223
+ v-bind="rendererProps"
224
+ @selectObject="onSelectObject" />
222
225
 
223
226
  <!-- No renderer resolves: render nothing, which is what CnDetailPage has
224
227
  always done here. Surfacing an "unknown widget" box instead would be
@@ -501,7 +504,7 @@ export default {
501
504
  },
502
505
  },
503
506
 
504
- emits: ['geo-saved', 'open-integration'],
507
+ emits: ['geo-saved', 'open-integration', 'select-object'],
505
508
 
506
509
  setup() {
507
510
  // Every integration component this host renders goes through the
@@ -1006,6 +1009,21 @@ export default {
1006
1009
  this.$emit('open-integration', integrationId)
1007
1010
  },
1008
1011
 
1012
+ /**
1013
+ * Pass a related-object click up; the page decides where it opens. Heard
1014
+ * from the Related widget itself and from a registry widget that nests a
1015
+ * host of its own (a Tabs widget with a Related tab).
1016
+ *
1017
+ * @param {object} raw The clicked object as the related widget holds it.
1018
+ */
1019
+ onSelectObject(raw) {
1020
+ /**
1021
+ * @event select-object A related object was clicked in the related widget. Payload is the raw object; its `@self` names the register and schema.
1022
+ * @type {object}
1023
+ */
1024
+ this.$emit('select-object', raw)
1025
+ },
1026
+
1009
1027
  /**
1010
1028
  * Delegate to the rendered list widget's own create affordance, so the
1011
1029
  * card's Add action and the widget's footer button open the same form.
@@ -200,6 +200,8 @@
200
200
  :maxZoom="maxZoom"
201
201
  @zoomChange="zoom = $event"
202
202
  @nodeSelect="onNodeSelect"
203
+ @nodeActivate="onNodeActivate"
204
+ @nodeMenu="onNodeMenu"
203
205
  @edgeSelect="onEdgeSelect"
204
206
  @edgeLabelClick="onEdgeLabelClick"
205
207
  @edgeLabelContext="onEdgeLabelContext"
@@ -1713,6 +1715,31 @@ export default {
1713
1715
  this.openNodeMenu({ item: id, event: mouse })
1714
1716
  },
1715
1717
 
1718
+ /**
1719
+ * Enter or Space on a step: open its editor, as a double-click does.
1720
+ *
1721
+ * @param {string} id The step id.
1722
+ * @return {void}
1723
+ */
1724
+ onNodeActivate(id) {
1725
+ this.store.selectedNodeId = id
1726
+ this.store.editingNodeId = id
1727
+ },
1728
+
1729
+ /**
1730
+ * Shift+F10 or the menu key on a step: its menu, at the step.
1731
+ *
1732
+ * @param {object} request The node's request.
1733
+ * @param {string} request.id The step id.
1734
+ * @param {number} request.clientX Where to place the menu.
1735
+ * @param {number} request.clientY Where to place the menu.
1736
+ * @return {void}
1737
+ */
1738
+ onNodeMenu({ id, clientX, clientY }) {
1739
+ this.store.selectedNodeId = id
1740
+ this.openNodeMenu({ item: id, event: { clientX, clientY } })
1741
+ },
1742
+
1716
1743
  /**
1717
1744
  * Place a palette node where it was dropped.
1718
1745
  *
@@ -224,6 +224,19 @@ export default {
224
224
  * edges, its undo stack, its persistence — so nothing is removed here.
225
225
  */
226
226
  'remove',
227
+
228
+ /**
229
+ * @event activate Enter or Space on the focused node: open this step.
230
+ * Carries the node id. The pointer equivalent is a double-click.
231
+ */
232
+ 'activate',
233
+
234
+ /**
235
+ * @event menu Shift+F10 or the menu key on the focused node: show this
236
+ * step's actions. Carries `{ id, clientX, clientY }`, a point at the
237
+ * node, so a host positions the menu as it would for a click.
238
+ */
239
+ 'menu',
227
240
  ],
228
241
 
229
242
  setup() {
@@ -478,7 +491,8 @@ export default {
478
491
  * The keyboard contract.
479
492
  *
480
493
  * Arrow keys move; `c` arms an exit and, on a second node, completes the
481
- * connection; `Escape` cancels. Repeated `c` on the SAME node steps
494
+ * connection; `Escape` cancels. Enter or Space opens the step, Shift+F10
495
+ * or the menu key opens its actions. Repeated `c` on the SAME node steps
482
496
  * through its exits rather than re-arming the first — the behaviour a
483
497
  * mouse gets for free by pointing.
484
498
  *
@@ -527,6 +541,27 @@ export default {
527
541
  return
528
542
  }
529
543
 
544
+ // Opening the step, and its menu, from the keyboard. Only for a key
545
+ // pressed ON the node: a port inside it is a control of its own.
546
+ if (event.target === event.currentTarget) {
547
+ if (event.key === 'Enter' || event.key === ' ') {
548
+ event.preventDefault()
549
+ this.$emit('activate', this.id)
550
+ return
551
+ }
552
+
553
+ if ((event.key === 'F10' && event.shiftKey === true) || event.key === 'ContextMenu') {
554
+ event.preventDefault()
555
+ const rect = event.currentTarget.getBoundingClientRect()
556
+ this.$emit('menu', {
557
+ id: this.id,
558
+ clientX: rect.left + rect.width / 2,
559
+ clientY: rect.bottom,
560
+ })
561
+ return
562
+ }
563
+ }
564
+
530
565
  // Delete and Backspace both, because which one removes a thing is a
531
566
  // platform habit rather than a preference: Backspace is the delete
532
567
  // key on a Mac keyboard, and a user who learned one does not think
@@ -37,7 +37,9 @@
37
37
  v-bind="nodeProps"
38
38
  :deletable="!readOnly"
39
39
  @connect="onConnect"
40
- @remove="onNodeRemove">
40
+ @remove="onNodeRemove"
41
+ @activate="onNodeActivate"
42
+ @menu="onNodeMenu">
41
43
  <template #default="slotProps">
42
44
  <!-- @slot node The body of a step, rendered inside the
43
45
  focusable node wrapper. Receives `{ node }` with the
@@ -266,6 +268,8 @@ export default {
266
268
  'canvas-drop',
267
269
 
268
270
  'node-remove',
271
+ 'node-activate',
272
+ 'node-menu',
269
273
 
270
274
  /**
271
275
  * The canvas zoom changed, by any route — a control here, the host's own
@@ -415,6 +419,41 @@ export default {
415
419
  return port === 'in' ? null : port
416
420
  },
417
421
 
422
+ /**
423
+ * Forward Enter or Space on a node: the keyboard double-click.
424
+ *
425
+ * @param {string} id The node id.
426
+ * @return {void}
427
+ */
428
+ onNodeActivate(id) {
429
+ if (this.readOnly === true) {
430
+ return
431
+ }
432
+
433
+ /**
434
+ * @event node-activate Enter or Space on a node. Payload is the id.
435
+ */
436
+ this.$emit('node-activate', id)
437
+ },
438
+
439
+ /**
440
+ * Forward Shift+F10 or the menu key on a node: the keyboard click menu.
441
+ *
442
+ * @param {object} request `{ id, clientX, clientY }`, a point at the node.
443
+ * @return {void}
444
+ */
445
+ onNodeMenu(request) {
446
+ if (this.readOnly === true) {
447
+ return
448
+ }
449
+
450
+ /**
451
+ * @event node-menu Shift+F10 or the menu key on a node. Payload is
452
+ * `{ id, clientX, clientY }`.
453
+ */
454
+ this.$emit('node-menu', request)
455
+ },
456
+
418
457
  /**
419
458
  * Forward a node click.
420
459
  *
@@ -731,7 +731,7 @@
731
731
  :currentPageSize="effectivePagination.limit || 20"
732
732
  class="cn-index-page__pagination"
733
733
  @pageChanged="onPageEvent"
734
- @pageSizeChanged="$emit('page-size-changed', $event)" />
734
+ @pageSizeChanged="onPageSizeEvent" />
735
735
  </div>
736
736
 
737
737
  <!-- The open record, beside the list on a wide screen and instead
@@ -1057,7 +1057,7 @@ function namedFilterToQuery(values) {
1057
1057
  * @event {object} row-aux-click — Table row or card middle-clicked. Payload: (row, event) — the row object and the native auxclick event
1058
1058
  * @event {{ key: string, order: string }} sort — Column sort changed
1059
1059
  * @event {number} page-changed — Pagination page changed
1060
- * @event {number} page-size-changed — Pagination page size changed
1060
+ * @event {number} page-size-changed — Pagination page size changed. In self-fetch mode the list refetches page 1 at the new size first.
1061
1061
  * @event {string[]} select — Selection changed. Payload: array of selected IDs
1062
1062
  * @event {object} action — Row action triggered. Payload: { action, row }
1063
1063
  * @event {{ action: string, id: string, selectedIds: Array, count: number }} bulk-action — A declarative bulk action was triggered from the selection strip. The SELECTION travels with it: an action that has to go and find out what was selected is one re-render away from acting on a different set than the user saw highlighted.
@@ -5482,6 +5482,21 @@ export default {
5482
5482
  this.$emit('page-changed', page)
5483
5483
  },
5484
5484
 
5485
+ /**
5486
+ * Page-size pick from CnPagination. In self-fetch mode the list refetches
5487
+ * page 1 at the new size — the event alone left the select changing and
5488
+ * the table not, since a manifest page has no host listening.
5489
+ *
5490
+ * @param {number} size Requested page size.
5491
+ * @return {void}
5492
+ */
5493
+ onPageSizeEvent(size) {
5494
+ if (this.isSelfFetchMode && typeof this.list.onPageSizeChange === 'function') {
5495
+ this.list.onPageSizeChange(size)
5496
+ }
5497
+ this.$emit('page-size-changed', size)
5498
+ },
5499
+
5485
5500
  /**
5486
5501
  * @param {{key: string, values: Array}} payload Facet-filter change from the sidebar.
5487
5502
  * @return {void}