@conduction/nextcloud-vue 2.71.0 → 2.73.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 (55) hide show
  1. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue2.js +25 -0
  2. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue2.js.map +1 -1
  3. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue3.js +3 -1
  4. package/dist/esm/components/CnFlowDetail/CnFlowDetail.vue3.js.map +1 -1
  5. package/dist/esm/components/CnFlowDetail/CnRunDetailSidebar.vue2.js +2 -0
  6. package/dist/esm/components/CnFlowDetail/CnRunDetailSidebar.vue2.js.map +1 -1
  7. package/dist/esm/components/CnFlowDetail/CnRunDetailSidebar.vue3.js +7 -0
  8. package/dist/esm/components/CnFlowDetail/CnRunDetailSidebar.vue3.js.map +1 -1
  9. package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue.js +10 -0
  10. package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue.js.map +1 -0
  11. package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue2.js +150 -0
  12. package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue2.js.map +1 -0
  13. package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue3.js +54 -0
  14. package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue3.js.map +1 -0
  15. package/dist/esm/components/CnGraphCanvas/CnFlowNode.vue2.js +36 -1
  16. package/dist/esm/components/CnGraphCanvas/CnFlowNode.vue2.js.map +1 -1
  17. package/dist/esm/components/CnGraphCanvas/CnFlowNode.vue3.js.map +1 -1
  18. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue2.js +37 -0
  19. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue2.js.map +1 -1
  20. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue3.js +4 -2
  21. package/dist/esm/components/CnGraphCanvas/CnGraphCanvas.vue3.js.map +1 -1
  22. package/dist/esm/dialogs/CnFlowNodeEditModal.vue2.js +7 -10
  23. package/dist/esm/dialogs/CnFlowNodeEditModal.vue2.js.map +1 -1
  24. package/dist/esm/dialogs/CnFlowNodeEditModal.vue3.js +23 -13
  25. package/dist/esm/dialogs/CnFlowNodeEditModal.vue3.js.map +1 -1
  26. package/dist/esm/dialogs/CnRunNodeDialog.vue2.js +8 -6
  27. package/dist/esm/dialogs/CnRunNodeDialog.vue2.js.map +1 -1
  28. package/dist/esm/dialogs/CnRunNodeDialog.vue3.js.map +1 -1
  29. package/dist/esm/index.js +1 -0
  30. package/dist/esm/index.js.map +1 -1
  31. package/dist/esm/l10n/en.json.js +15 -1
  32. package/dist/esm/l10n/en.json.js.map +1 -1
  33. package/dist/esm/l10n/nl.json.js +15 -1
  34. package/dist/esm/l10n/nl.json.js.map +1 -1
  35. package/dist/esm/nextcloud-vue.css +64 -0
  36. package/dist/esm/utils/selectOptions.js +57 -0
  37. package/dist/esm/utils/selectOptions.js.map +1 -0
  38. package/dist/nextcloud-vue.cjs.js +6050 -5649
  39. package/dist/nextcloud-vue.cjs.js.map +2 -2
  40. package/dist/nextcloud-vue.css +64 -0
  41. package/l10n/en.json +15 -1
  42. package/l10n/nl.json +15 -1
  43. package/package.json +1 -1
  44. package/scripts/openregister-reliance.js +206 -0
  45. package/src/components/CnFlowDetail/CnFlowDetail.vue +27 -0
  46. package/src/components/CnFlowDetail/CnRunDetailSidebar.vue +6 -0
  47. package/src/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue +250 -0
  48. package/src/components/CnFlowStepOutcomes/index.js +8 -0
  49. package/src/components/CnGraphCanvas/CnFlowNode.vue +36 -1
  50. package/src/components/CnGraphCanvas/CnGraphCanvas.vue +40 -1
  51. package/src/components/index.js +1 -0
  52. package/src/dialogs/CnFlowNodeEditModal.vue +41 -20
  53. package/src/dialogs/CnRunNodeDialog.vue +8 -6
  54. package/src/index.js +1 -0
  55. package/src/utils/selectOptions.js +54 -0
@@ -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
  *
@@ -191,6 +191,7 @@ export { CnDeltaWidgetForm } from './CnDeltaWidgetForm/index.js'
191
191
  export { CnGaugeWidget } from './CnGaugeWidget/index.js'
192
192
  export { CnGaugeWidgetForm } from './CnGaugeWidgetForm/index.js'
193
193
  export { CnFlowRunsWidget } from './CnFlowRunsWidget/index.js'
194
+ export { CnFlowStepOutcomes } from './CnFlowStepOutcomes/index.js'
194
195
  export { CnFlowRunsWidgetForm } from './CnFlowRunsWidgetForm/index.js'
195
196
  export { CnTasksWidget } from './CnTasksWidget/index.js'
196
197
  export { CnTasksWidgetForm } from './CnTasksWidgetForm/index.js'
@@ -40,16 +40,27 @@
40
40
  :placeholder="t('nextcloud-vue', 'GET')"
41
41
  @update:modelValue="setKey(key, $event)" />
42
42
 
43
- <!-- A `select` field with `optionsFrom` renders as a picker fed
44
- by the URL the OWNING APP declared — never a bare uuid
45
- text box. -->
46
- <NcSelect v-else-if="widgetFor(key) === 'select'"
47
- :modelValue="selectedOption(key)"
48
- :options="selectOptions[key] || []"
49
- :inputLabel="labelFor(key)"
50
- :loading="selectLoading[key] === true"
51
- :placeholder="t('nextcloud-vue', 'Pick one…')"
52
- @update:modelValue="setKey(key, $event ? $event.id : '')" />
43
+ <!-- A `select` field renders as a picker, never a bare text box.
44
+ Its choices come from the URL the OWNING APP declared
45
+ (`optionsFrom`) or from the form itself (`options`), for a
46
+ short fixed vocabulary with no endpoint of its own. -->
47
+ <template v-else-if="widgetFor(key) === 'select'">
48
+ <NcSelect :modelValue="selectedOption(key)"
49
+ class="cn-flow-node-edit__select"
50
+ :options="selectOptions[key] || []"
51
+ :inputLabel="labelFor(key)"
52
+ :loading="selectLoading[key] === true"
53
+ :placeholder="t('nextcloud-vue', 'Pick one…')"
54
+ @update:modelValue="setKey(key, $event ? $event.id : '')" />
55
+ <!-- NcSelect has no helper text of its own, so the help
56
+ follows the control, the next thing a screen reader
57
+ reads after it. -->
58
+ <p v-if="hintFor(key)"
59
+ class="cn-flow-node-edit__field-help"
60
+ :data-testid="`flow-node-help-${key}`">
61
+ {{ hintFor(key) }}
62
+ </p>
63
+ </template>
53
64
 
54
65
  <NcTextArea v-else-if="widgetFor(key) === 'textarea'"
55
66
  :modelValue="String(draft.config[key] ?? '')"
@@ -170,6 +181,7 @@ import {
170
181
  } from '@nextcloud/vue'
171
182
  import CnCronField from '../components/CnCronField/CnCronField.vue'
172
183
  import { useFlowStore } from '../composables/useFlowStore.js'
184
+ import { hasStaticOptions, normaliseSelectOptions } from '../utils/selectOptions.js'
173
185
 
174
186
  /** The verbs an HTTP-shaped `method` option can take. */
175
187
  /**
@@ -287,7 +299,7 @@ export default {
287
299
 
288
300
  /**
289
301
  * The node's per-field declarations from the catalogue's `configForm`
290
- * ({key, label, type, help, required, optionsFrom}), keyed by config
302
+ * ({key, label, type, help, required, optionsFrom, options}), keyed by config
291
303
  * key. `configKeys` is the degraded form. Either way the node's OWNER
292
304
  * declares the vocabulary — this dialog never invents fields.
293
305
  *
@@ -398,7 +410,9 @@ export default {
398
410
  created() {
399
411
  // Select fields need their options; everything else is local.
400
412
  for (const [key, spec] of Object.entries(this.fieldSpecs)) {
401
- if (spec?.type === 'select' && spec?.optionsFrom) {
413
+ if (hasStaticOptions(spec)) {
414
+ this.selectOptions = { ...this.selectOptions, [key]: normaliseSelectOptions(spec.options) }
415
+ } else if (spec?.type === 'select' && spec?.optionsFrom) {
402
416
  this.loadSelectOptions(key, spec)
403
417
  }
404
418
  }
@@ -634,7 +648,7 @@ export default {
634
648
  */
635
649
  widgetFor(key) {
636
650
  const spec = this.fieldSpecs[key]
637
- if (spec?.type === 'select' && spec?.optionsFrom) {
651
+ if (spec?.type === 'select' && (spec?.optionsFrom || hasStaticOptions(spec))) {
638
652
  return 'select'
639
653
  }
640
654
  if (spec?.type === 'boolean') {
@@ -730,13 +744,7 @@ export default {
730
744
  ? generateUrl(url)
731
745
  : url)
732
746
  const rows = Array.isArray(response.data) ? response.data : (response.data?.results || [])
733
- const options = rows.map((row) => {
734
- const id = row.id ?? row.value ?? row['@self']?.uuid ?? row.uuid
735
- return {
736
- id,
737
- label: row.label || row.name || row.title || String(id),
738
- }
739
- }).filter((o) => o.id !== undefined && o.id !== null && o.id !== '')
747
+ const options = normaliseSelectOptions(rows)
740
748
  this.selectOptions = { ...this.selectOptions, [key]: options }
741
749
  } catch (error) {
742
750
  // A picker that could not load degrades to showing the stored
@@ -930,6 +938,19 @@ export default {
930
938
  color: var(--color-text-maxcontrast);
931
939
  }
932
940
 
941
+ /* Full width, like the text fields around it: at its natural width the
942
+ picker cut every option label in half. */
943
+ .cn-flow-node-edit__select {
944
+ width: 100%;
945
+ }
946
+
947
+ .cn-flow-node-edit__field-help {
948
+ margin: 4px 0 0;
949
+ padding-inline-start: 8px;
950
+ font-size: 0.85em;
951
+ color: var(--color-text-maxcontrast);
952
+ }
953
+
933
954
  .cn-flow-node-edit__advanced summary {
934
955
  cursor: pointer;
935
956
  color: var(--color-text-maxcontrast);
@@ -88,6 +88,7 @@
88
88
  import { translate as t } from '@nextcloud/l10n'
89
89
  import { NcButton, NcCheckboxRadioSwitch, NcDialog, NcLoadingIcon, NcNoteCard, NcSelect, NcTextArea, NcTextField } from '@nextcloud/vue'
90
90
  import { prefixUrl } from '../utils/headers.js'
91
+ import { hasStaticOptions, normaliseSelectOptions } from '../utils/selectOptions.js'
91
92
 
92
93
  /**
93
94
  * The dialog `run-node` opens to collect a directly-invoked flow node's own
@@ -128,7 +129,9 @@ export default {
128
129
  title: { type: String, default: '' },
129
130
  /**
130
131
  * The node's declared config form — `IFlowNodeConfigForm::configForm()`'s
131
- * shape, unchanged: `[{key, label, type, help?, required?, optionsFrom?}]`.
132
+ * shape, unchanged: `[{key, label, type, help?, required?, optionsFrom?, options?}]`.
133
+ * A `select` takes its choices from `optionsFrom` (a url) or `options`
134
+ * (strings, or `{value, label}` pairs declared in the form itself).
132
135
  *
133
136
  * @type {Array<object>}
134
137
  */
@@ -177,7 +180,9 @@ export default {
177
180
  }
178
181
  this.values = seed
179
182
  for (const field of (fields || [])) {
180
- if (field.type === 'select' && field.optionsFrom) {
183
+ if (hasStaticOptions(field)) {
184
+ this.options = { ...this.options, [field.key]: normaliseSelectOptions(field.options, (s) => this.tr(s)) }
185
+ } else if (field.type === 'select' && field.optionsFrom) {
181
186
  this.loadOptions(field)
182
187
  }
183
188
  }
@@ -274,10 +279,7 @@ export default {
274
279
  const url = prefixUrl(raw)
275
280
  const response = await axios.get(url)
276
281
  const rows = Array.isArray(response.data) ? response.data : (response.data?.results || [])
277
- const opts = rows.map((row) => {
278
- const id = row.id ?? row.value ?? row['@self']?.uuid ?? row.uuid
279
- return { id, label: row.label || row.name || row.title || String(id) }
280
- }).filter((o) => o.id !== undefined && o.id !== null && o.id !== '')
282
+ const opts = normaliseSelectOptions(rows)
281
283
  this.options = { ...this.options, [field.key]: opts }
282
284
  } catch (error) {
283
285
  // A picker that fails to load degrades to an empty list rather
package/src/index.js CHANGED
@@ -129,6 +129,7 @@ export {
129
129
  CnFlowRunsWidgetForm,
130
130
  CnFlowSidebar,
131
131
  CnFlowsPage,
132
+ CnFlowStepOutcomes,
132
133
  CnFolderSidebar,
133
134
  CnFolderTree,
134
135
  CnFormBuilder,
@@ -0,0 +1,54 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * The choices a node's `select` field offers, in the `{id, label}` shape
6
+ * NcSelect takes.
7
+ *
8
+ * A field gets its choices one of two ways. `optionsFrom` names a url the
9
+ * owning app answers. `options` lists them in the form itself, for a short
10
+ * fixed vocabulary that has no endpoint of its own. Both dialogs that render a
11
+ * node's configForm read the same shapes through this module, so the two
12
+ * cannot come to accept different ones.
13
+ */
14
+
15
+ /**
16
+ * Normalise rows into `{id, label}` options.
17
+ *
18
+ * Accepts plain strings and numbers, `{id, label}`, `{value, label}`, and
19
+ * OpenRegister objects (uuid from `@self`, label from name or title). A row
20
+ * without an id is dropped rather than shown as a blank option.
21
+ *
22
+ * @param {Array<unknown>} rows The raw rows.
23
+ * @param {(label: string) => string} [translate] Applied to each declared label.
24
+ * @return {Array<{id: string|number, label: string}>} The options.
25
+ */
26
+ export function normaliseSelectOptions(rows, translate = (s) => s) {
27
+ if (!Array.isArray(rows)) {
28
+ return []
29
+ }
30
+
31
+ return rows.map((row) => {
32
+ if (typeof row === 'string' || typeof row === 'number') {
33
+ return { id: row, label: String(row) }
34
+ }
35
+ if (!row || typeof row !== 'object') {
36
+ return { id: undefined, label: '' }
37
+ }
38
+
39
+ const id = row.id ?? row.value ?? row['@self']?.uuid ?? row.uuid
40
+ const declared = row.label || row.name || row.title
41
+
42
+ return { id, label: declared ? String(translate(declared)) : String(id) }
43
+ }).filter((option) => option.id !== undefined && option.id !== null && option.id !== '')
44
+ }
45
+
46
+ /**
47
+ * Whether a field declares its choices in the form itself.
48
+ *
49
+ * @param {object} field The field declaration.
50
+ * @return {boolean} True for a `select` with an `options` list.
51
+ */
52
+ export function hasStaticOptions(field) {
53
+ return field?.type === 'select' && Array.isArray(field?.options)
54
+ }