@conduction/nextcloud-vue 2.99.0 → 2.100.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 (79) hide show
  1. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +6 -1
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +2 -1
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  6. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +25 -8
  7. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  8. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +5 -3
  9. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  10. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  11. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +11 -8
  12. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  13. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +13 -4
  14. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  15. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  16. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +54 -3
  17. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  18. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +16 -2
  19. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  20. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue.js +1 -1
  21. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js +42 -7
  22. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js.map +1 -1
  23. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js +121 -57
  24. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js +10 -0
  26. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js +2 -1
  28. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +136 -7
  30. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  31. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  32. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  33. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +68 -7
  34. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  35. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +256 -240
  36. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  37. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js +1 -1
  38. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js +38 -9
  39. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js.map +1 -1
  40. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js.map +1 -1
  41. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  42. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +16 -2
  43. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  44. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +40 -25
  45. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  46. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
  47. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +7 -7
  48. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  49. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  50. package/dist/esm/l10n/en.json.js +7 -1
  51. package/dist/esm/l10n/en.json.js.map +1 -1
  52. package/dist/esm/l10n/nl.json.js +7 -1
  53. package/dist/esm/l10n/nl.json.js.map +1 -1
  54. package/dist/esm/library-style.js +1 -1
  55. package/dist/esm/nextcloud-vue.css +348 -161
  56. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  57. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  58. package/dist/nextcloud-vue.cjs.js +1482 -998
  59. package/dist/nextcloud-vue.cjs.js.map +2 -2
  60. package/dist/nextcloud-vue.css +348 -161
  61. package/l10n/en.json +7 -1
  62. package/l10n/nl.json +7 -1
  63. package/package.json +1 -1
  64. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +7 -1
  65. package/src/components/CnDetailPage/CnDetailPage.vue +28 -9
  66. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +13 -4
  67. package/src/components/CnFactsWidget/CnFactsWidget.vue +65 -3
  68. package/src/components/CnFlowsPage/CnFlowBoard.vue +77 -2
  69. package/src/components/CnFlowsPage/CnFlowEditorPage.vue +11 -0
  70. package/src/components/CnIndexPage/CnIndexPage.vue +136 -7
  71. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +114 -7
  72. package/src/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue +38 -9
  73. package/src/components/CnSetupWizard/CnSetupWizard.vue +37 -3
  74. package/src/components/CnWizardDialog/CnWizardDialog.vue +7 -7
  75. package/src/css/detail-page.css +15 -0
  76. package/src/css/dialog.css +48 -0
  77. package/src/css/look-board.css +83 -1
  78. package/src/schemas/app-manifest-v2.schema.json +25 -1
  79. package/src/utils/validateManifestV2.compiled.js +3 -3
package/l10n/en.json CHANGED
@@ -1694,7 +1694,13 @@
1694
1694
  "{fields}: {reason}": "{fields}: {reason}",
1695
1695
  "Current {name}": "Current {name}",
1696
1696
  "Upload {name}": "Upload {name}",
1697
- "The image could not be uploaded: {reason}": "The image could not be uploaded: {reason}"
1697
+ "The image could not be uploaded: {reason}": "The image could not be uploaded: {reason}",
1698
+ "The saved flow runs once with the record you pick as its subject.": "The saved flow runs once with the record you pick as its subject.",
1699
+ "Record": "Record",
1700
+ "Run test": "Run test",
1701
+ "Test with a record": "Test with a record",
1702
+ "admin": "admin",
1703
+ "Wizard": "Wizard"
1698
1704
  },
1699
1705
  "plurals": {
1700
1706
  "{count} selected": [
package/l10n/nl.json CHANGED
@@ -1909,7 +1909,13 @@
1909
1909
  "Day added.": "Dag toegevoegd.",
1910
1910
  "Only an administrator can change the working calendar.": "Alleen een beheerder kan de werkkalender wijzigen.",
1911
1911
  "The day could not be added. Try again.": "De dag kon niet worden toegevoegd. Probeer het opnieuw.",
1912
- "Loading …": "Laden …"
1912
+ "Loading …": "Laden …",
1913
+ "The saved flow runs once with the record you pick as its subject.": "De opgeslagen flow draait één keer met het record dat u kiest als onderwerp.",
1914
+ "Record": "Record",
1915
+ "Run test": "Test uitvoeren",
1916
+ "Test with a record": "Testen met een record",
1917
+ "admin": "beheerder",
1918
+ "Wizard": "Assistent"
1913
1919
  },
1914
1920
  "plurals": {
1915
1921
  "{count} selected": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.99.0",
3
+ "version": "2.100.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": {
@@ -218,6 +218,7 @@
218
218
  <CnSetupWizard
219
219
  v-if="showSetup && setupWizardOpen"
220
220
  :appId="appId"
221
+ :appName="appName"
221
222
  :steps="setupSteps"
222
223
  @complete="setupWizardOpen = false"
223
224
  @close="setupWizardOpen = false" />
@@ -231,7 +232,7 @@ import { loadState } from '@nextcloud/initial-state'
231
232
  import { translate as t } from '@nextcloud/l10n'
232
233
  import { generateUrl } from '@nextcloud/router'
233
234
  import { NcButton, NcLoadingIcon, NcSettingsSection } from '@nextcloud/vue'
234
- import { computed, provide } from 'vue'
235
+ import { computed, inject, provide } from 'vue'
235
236
  import AutoFix from 'vue-material-design-icons/AutoFix.vue'
236
237
  import BookOpenVariant from 'vue-material-design-icons/BookOpenVariant.vue'
237
238
  import HelpCircleOutline from 'vue-material-design-icons/HelpCircleOutline.vue'
@@ -772,6 +773,11 @@ export default {
772
773
  // Everything inside the shell, its own version card and the app's
773
774
  // sections alike, follows the shell's look without the app providing it.
774
775
  provide('cnLook', look)
776
+ // The setup wizard translates its step titles and bodies through
777
+ // `cnTranslate`. On an admin page no CnAppRoot provides it, so the
778
+ // shell hands down the app's own catalog, keyed by its app id.
779
+ const hostTranslate = inject('cnTranslate', null)
780
+ provide('cnTranslate', hostTranslate || ((text, vars) => t(props.appId, text, vars)))
775
781
  return { environmentName, isBoard, ownLookClass }
776
782
  },
777
783
 
@@ -414,7 +414,8 @@
414
414
  :key="`${entry.id}-link`"
415
415
  :href="entry.href"
416
416
  :target="entry.linkTarget || undefined"
417
- :data-testid="entry.testid">
417
+ :data-testid="entry.testid"
418
+ :data-cn-admin-tag="adminTagFor(group)">
418
419
  <template v-if="entry.iconName || entry.iconClass" #icon>
419
420
  <CnIcon v-if="entry.iconName" :name="entry.iconName" :size="20" />
420
421
  <span v-else :class="entry.iconClass" />
@@ -428,6 +429,7 @@
428
429
  :disabled="entry.disabled"
429
430
  :aria-pressed="entry.pressed === null ? null : String(entry.pressed)"
430
431
  :closeAfterClick="true"
432
+ :data-cn-admin-tag="adminTagFor(group)"
431
433
  @click="entry.run()">
432
434
  <template v-if="entry.iconName || entry.iconClass" #icon>
433
435
  <CnIcon v-if="entry.iconName" :name="entry.iconName" :size="20" />
@@ -2443,13 +2445,15 @@ export default {
2443
2445
 
2444
2446
  /**
2445
2447
  * What the Actions menu itself shows (manifest `config.actionsMenu`):
2446
- * `{ showRefresh?, showHelpLinks?, label? }`. `showRefresh: false`
2447
- * removes Refresh. `showHelpLinks: false` removes Request a feature,
2448
- * Report a bug and Documentation, for an app that offers them in its
2449
- * own help menu. `label` names the menu, for example "More". `null`
2450
- * (the default) changes nothing.
2448
+ * `{ showRefresh?, showHelpLinks?, showMarkUnread?, showOpenSidebar?, label? }`.
2449
+ * `showRefresh: false` removes Refresh. `showHelpLinks: false` removes
2450
+ * Request a feature, Report a bug and Documentation, for an app that
2451
+ * offers them in its own help menu. `showMarkUnread: false` and
2452
+ * `showOpenSidebar: false` remove those two built-in entries, for a
2453
+ * menu the board draws without them (DqZaakMeer). `label` names the
2454
+ * menu, for example "More". `null` (the default) changes nothing.
2451
2455
  *
2452
- * @type {{showRefresh?: boolean, showHelpLinks?: boolean, label?: string}|null}
2456
+ * @type {{showRefresh?: boolean, showHelpLinks?: boolean, showMarkUnread?: boolean, showOpenSidebar?: boolean, label?: string}|null}
2453
2457
  */
2454
2458
  actionsMenu: {
2455
2459
  type: Object,
@@ -4173,7 +4177,8 @@ export default {
4173
4177
  * @spec openspec/changes/record-unread-markers/tasks.md#task-2
4174
4178
  */
4175
4179
  showMarkUnread() {
4176
- return typeof this.resolvedSelf.unread === 'boolean'
4180
+ return this.actionsMenu?.showMarkUnread !== false
4181
+ && typeof this.resolvedSelf.unread === 'boolean'
4177
4182
  && this.register !== '' && this.schema !== '' && this.objectId !== ''
4178
4183
  },
4179
4184
 
@@ -4185,7 +4190,8 @@ export default {
4185
4190
  * @return {boolean}
4186
4191
  */
4187
4192
  showOpenSidebar() {
4188
- return this.isBoard && this.hasExternalSidebar && this.sidebarActive && this.objectSidebarState.open !== true
4193
+ return this.actionsMenu?.showOpenSidebar !== false
4194
+ && this.isBoard && this.hasExternalSidebar && this.sidebarActive && this.objectSidebarState.open !== true
4189
4195
  },
4190
4196
 
4191
4197
  /** @return {string} Label of the Open sidebar entry. */
@@ -4933,6 +4939,19 @@ export default {
4933
4939
  },
4934
4940
 
4935
4941
  methods: {
4942
+ /**
4943
+ * The tag after an admin-only entry in the board look ("beheerder",
4944
+ * DqZaakMeer), or null for any other entry or look. It is drawn by
4945
+ * detail-page.css from the attribute, since the menu renders its
4946
+ * entries' text as plain text.
4947
+ *
4948
+ * @param {{key: string}} group The menu group the entry is in.
4949
+ * @return {string|null} The tag, or null.
4950
+ */
4951
+ adminTagFor(group) {
4952
+ return this.isBoard && group && group.key === 'cn-admin' ? t('nextcloud-vue', 'admin') : null
4953
+ },
4954
+
4936
4955
  /**
4937
4956
  * Open the object sidebar through the shared channel.
4938
4957
  *
@@ -184,8 +184,14 @@
184
184
  <CnIcon :name="widget.icon" :size="20" />
185
185
  </template>
186
186
  <!-- A renderer that reports its own header (`header` event:
187
- `{ link, note }`, e.g. record-list's "3 in totaal"). -->
188
- <template v-if="rendererHeader.note || rendererHeader.action" #actions>
187
+ `{ link, note, action, pill }`, e.g. record-list's "3 in totaal",
188
+ a facts card's status pill). -->
189
+ <template v-if="rendererHeader.note || rendererHeader.action || rendererHeader.pill" #actions>
190
+ <CnStatusBadge
191
+ v-if="rendererHeader.pill"
192
+ :label="rendererHeader.pill.label"
193
+ :variant="rendererHeader.pill.variant"
194
+ data-testid="cn-detail-widget-header-pill" />
189
195
  <span
190
196
  v-if="rendererHeader.note"
191
197
  class="cn-detail-page__catalog-note"
@@ -298,6 +304,7 @@ import CnObjectDataWidget from '../CnObjectDataWidget/CnObjectDataWidget.vue'
298
304
  import CnObjectFilesWidget from '../CnObjectFilesWidget/CnObjectFilesWidget.vue'
299
305
  import CnObjectGeoWidget from '../CnObjectGeoWidget/CnObjectGeoWidget.vue'
300
306
  import CnRelatedObjectsWidget from '../CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue'
307
+ import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
301
308
  import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
302
309
  import { normalizeLook } from '../../composables/useLook.js'
303
310
  import { isAppInstalled } from '../../utils/appInstalled.js'
@@ -376,6 +383,7 @@ export default {
376
383
  CnObjectDataWidget,
377
384
  CnObjectGeoWidget,
378
385
  CnRelatedObjectsWidget,
386
+ CnStatusBadge,
379
387
  CnWidgetWrapper,
380
388
  NcActionButton,
381
389
  CnEmptyContent,
@@ -631,7 +639,7 @@ export default {
631
639
  * `header` event (screens-relation-pages): a link and a note,
632
640
  * with the list's total already filled in.
633
641
  */
634
- rendererHeader: { link: null, note: '', action: null },
642
+ rendererHeader: { link: null, note: '', action: null, pill: null },
635
643
  }
636
644
  },
637
645
 
@@ -1218,7 +1226,8 @@ export default {
1218
1226
  const link = header && header.link && typeof header.link === 'object' ? header.link : null
1219
1227
  const note = header && typeof header.note === 'string' ? header.note : ''
1220
1228
  const action = header && header.action && typeof header.action.run === 'function' && header.action.label ? header.action : null
1221
- this.rendererHeader = { link, note, action }
1229
+ const pill = header && header.pill && typeof header.pill.label === 'string' && header.pill.label !== '' ? header.pill : null
1230
+ this.rendererHeader = { link, note, action, pill }
1222
1231
  },
1223
1232
 
1224
1233
  /**
@@ -132,6 +132,13 @@
132
132
  </div>
133
133
  </template>
134
134
  </template>
135
+ <NcNoteCard
136
+ v-if="notice && record"
137
+ :type="notice.type"
138
+ class="cn-facts-widget__notice"
139
+ data-testid="cn-facts-notice">
140
+ {{ notice.text }}
141
+ </NcNoteCard>
135
142
  <component
136
143
  :is="link.to ? 'router-link' : 'a'"
137
144
  v-if="link && record"
@@ -157,7 +164,7 @@
157
164
  import { emit } from '@nextcloud/event-bus'
158
165
  import { formatFileSize } from '@nextcloud/files'
159
166
  import { translate as t } from '@nextcloud/l10n'
160
- import { NcLoadingIcon } from '@nextcloud/vue'
167
+ import { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
161
168
  import Check from 'vue-material-design-icons/Check.vue'
162
169
  import ContentCopy from 'vue-material-design-icons/ContentCopy.vue'
163
170
  import EyeOutline from 'vue-material-design-icons/EyeOutline.vue'
@@ -266,7 +273,7 @@ export const FACTS_REVEAL_CHANNEL = 'cn:facts:reveal'
266
273
  export default {
267
274
  name: 'CnFactsWidget',
268
275
 
269
- components: { Check, CnIcon, CnStatusBadge, ContentCopy, EyeOutline, NcLoadingIcon },
276
+ components: { Check, CnIcon, CnStatusBadge, ContentCopy, EyeOutline, NcLoadingIcon, NcNoteCard },
270
277
 
271
278
  inject: {
272
279
  cnTranslate: { default: () => (key) => key },
@@ -290,6 +297,8 @@ export default {
290
297
  * `resolve: { register, schema, labelField? }` on an entry or a part turns a value that is a reference (an id, or a list of ids) into the label of the referenced object (`labelField`, else `title`, then `name`): the case type or status of a task shows its name, not its uuid. The ids are fetched once per register and schema through the object store and shared by every facts widget on the page; the entry stays empty until its label is known, and when no label can be read. A resolved value works as a pill too: `map` is applied to the resolved label, and `variantMap` / `colorMap` look the tone up by the value of `resolve.toneField` on the referenced object when that is set (a status named by its `name`, coloured by its `state`), then by the resolved label, then by the stored value.
291
298
  * `format: 'user'` shows a Nextcloud user id as the user's display name, read once per id; the entry stays empty until the name is known.
292
299
  * - `action` — an action drawn as a button under the facts; `headerAction` — one drawn in the card header (reported through the `header` event).
300
+ * - `headerPill` — a status pill in the card header (reported through the `header` event): `{ path?, text?, map?, variant?, variantMap?, colorMap? }`, read from the record like a `pill` entry (DqBezwaar "Advies ontvangen"); nothing shows while the value is empty.
301
+ * - `notice` — `{ text, variant? }`, a note band inside the card under the facts (`info` by default; `success`, `warning` or `error`); the text is translated.
293
302
  * `format` is `text` (default), `date` (board short date), `date-long` (always with the year), `list` (an array joined by ", ", each item read at `itemPath`), `bytes` (a size, "1.5 MB") or `currency` (a money amount in the reader's language, "€ 1.234,50": the code in `currency`, else the record's `currencyField`, else the app's reporting currency, else EUR; `decimals` defaults to 2).
294
303
  * `map` turns a raw value into a label (translated). `parts` (on a `line`) lists paths, or `{ path, label?, format? }`, joined by `separator` (default " · "); empty parts are dropped.
295
304
  * - `source` — `{ register, schema, id }` or `{ url }`; omitted = the current object. An entry with `from: "object"` reads the current object even then (a client card that also shows the ticket's channel).
@@ -508,6 +517,47 @@ export default {
508
517
  return this.actionView(this.content?.headerAction)
509
518
  },
510
519
 
520
+ /**
521
+ * The card header's status pill, read from the record like a `pill`
522
+ * entry, or null when it has no text.
523
+ *
524
+ * @return {{label: string, variant: string}|null}
525
+ */
526
+ headerPill() {
527
+ const pill = this.content?.headerPill
528
+ if (!pill || typeof pill !== 'object' || !this.record) {
529
+ return null
530
+ }
531
+ const raw = typeof pill.path === 'string' && pill.path !== '' ? readPath(this.record, pill.path) : undefined
532
+ const mapped = pill.map && (typeof raw === 'string' || typeof raw === 'number') && Object.hasOwn(pill.map, raw) ? pill.map[raw] : undefined
533
+ const text = mapped ?? pill.text ?? raw
534
+ if (typeof text !== 'string' && typeof text !== 'number') {
535
+ return null
536
+ }
537
+ const label = this.tr(String(text))
538
+ if (label === '') {
539
+ return null
540
+ }
541
+ const colours = pill.variantMap || pill.colorMap
542
+ const hit = colours && (typeof raw === 'string' || typeof raw === 'number') && Object.hasOwn(colours, raw) ? colours[raw] : pill.variant
543
+ return { label, variant: isBadgeVariant(hit) ? hit : 'default' }
544
+ },
545
+
546
+ /**
547
+ * The note band under the facts: its text translated, its NcNoteCard
548
+ * type from `variant` (info by default), or null without text.
549
+ *
550
+ * @return {{text: string, type: string}|null}
551
+ */
552
+ notice() {
553
+ const notice = this.content?.notice
554
+ const text = notice && typeof notice.text === 'string' ? this.tr(notice.text) : ''
555
+ if (text === '') {
556
+ return null
557
+ }
558
+ return { text, type: ['success', 'warning', 'error'].includes(notice.variant) ? notice.variant : 'info' }
559
+ },
560
+
511
561
  /** @return {string} The reveal button's text. */
512
562
  showLabel() {
513
563
  return t('nextcloud-vue', 'Show')
@@ -544,6 +594,13 @@ export default {
544
594
  this.emitHeader()
545
595
  },
546
596
  },
597
+
598
+ headerPill: {
599
+ deep: true,
600
+ handler() {
601
+ this.emitHeader()
602
+ },
603
+ },
547
604
  },
548
605
 
549
606
  methods: {
@@ -803,12 +860,13 @@ export default {
803
860
  const view = this.headerAction
804
861
  /**
805
862
  * @event header Emitted with the card header's parts: the header action, if any.
806
- * @type {{link: null, note: string, action: (object|null)}} `action` is `{ label, icon, run }`.
863
+ * @type {{link: null, note: string, action: (object|null), pill: (object|null)}} `action` is `{ label, icon, run }`, `pill` is `{ label, variant }`.
807
864
  */
808
865
  this.$emit('header', {
809
866
  link: null,
810
867
  note: '',
811
868
  action: view ? { ...view, run: () => this.runAction(this.content.headerAction) } : null,
869
+ pill: this.headerPill,
812
870
  })
813
871
  },
814
872
 
@@ -1368,6 +1426,10 @@ export default {
1368
1426
  background: var(--color-primary-element-light-hover, var(--color-primary-element-light));
1369
1427
  }
1370
1428
 
1429
+ .cn-facts-widget__notice {
1430
+ margin: 4px 0 0;
1431
+ }
1432
+
1371
1433
  .cn-facts-widget__link {
1372
1434
  font-size: 14px;
1373
1435
  font-weight: 600;
@@ -39,6 +39,15 @@
39
39
  @click="$emit('run')">
40
40
  {{ t('nextcloud-vue', 'Run') }}
41
41
  </NcButton>
42
+ <NcButton v-if="testSubjectReady"
43
+ :disabled="store.running || !store.flow.id"
44
+ data-testid="flow-board-test"
45
+ @click="testOpen = true">
46
+ <template #icon>
47
+ <TestTube :size="16" />
48
+ </template>
49
+ {{ testLabel }}
50
+ </NcButton>
42
51
  <CnBuildiqEditButton />
43
52
  <NcButton variant="primary"
44
53
  :disabled="store.saving || !store.flow.name || store.graphLocked"
@@ -49,6 +58,32 @@
49
58
  </div>
50
59
  </header>
51
60
 
61
+ <CnDialog v-if="testOpen"
62
+ :name="testLabel"
63
+ data-testid="flow-board-test-dialog"
64
+ @closing="testOpen = false">
65
+ <p class="cn-flow-board__test-intro">
66
+ {{ t('nextcloud-vue', 'The saved flow runs once with the record you pick as its subject.') }}
67
+ </p>
68
+ <CnResourceSelect v-model="testRecord"
69
+ :register="testSubject.register"
70
+ :schema="testSubject.schema"
71
+ :labelField="testSubject.labelField || 'title'"
72
+ :inputLabel="t('nextcloud-vue', 'Record')"
73
+ data-testid="flow-board-test-record" />
74
+ <template #actions>
75
+ <NcButton data-testid="flow-board-test-cancel" @click="testOpen = false">
76
+ {{ t('nextcloud-vue', 'Cancel') }}
77
+ </NcButton>
78
+ <NcButton variant="primary"
79
+ :disabled="!testRecord || store.running"
80
+ data-testid="flow-board-test-run"
81
+ @click="runTest">
82
+ {{ t('nextcloud-vue', 'Run test') }}
83
+ </NcButton>
84
+ </template>
85
+ </CnDialog>
86
+
52
87
  <p class="cn-flow-board__path" data-testid="flow-board-path">
53
88
  <a :href="listHref" class="cn-flow-board__crumb" @click="onCrumbClick($event, detailRoute)">{{ listText }}</a>
54
89
  <span aria-hidden="true"> / </span>
@@ -186,11 +221,14 @@
186
221
  <script>
187
222
  import { translate as t } from '@nextcloud/l10n'
188
223
  import { NcButton, NcCheckboxRadioSwitch, NcNoteCard } from '@nextcloud/vue'
224
+ import TestTube from 'vue-material-design-icons/TestTube.vue'
189
225
  import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
226
+ import CnDialog from '../CnDialog/CnDialog.vue'
190
227
  import CnFlowDetail from '../CnFlowDetail/CnFlowDetail.vue'
191
228
  import CnFlowRunSummary from '../CnFlowDetail/CnFlowRunSummary.vue'
192
229
  import CnFlowSidebar from '../CnFlowDetail/CnFlowSidebar.vue'
193
230
  import CnFlowStepLog from '../CnFlowDetail/CnFlowStepLog.vue'
231
+ import CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'
194
232
  import { FLOW_RUN_ACTIVE_STATUSES, useFlowStore } from '../../composables/useFlowStore.js'
195
233
  import { formatRelativeDate } from '../../utils/boardDate.js'
196
234
  import { flowOutcomeLevels, flowRunDurationMs, flowRunLogLines, flowRunName, formatFlowDuration } from '../../utils/flowRunLog.js'
@@ -204,7 +242,7 @@ import { CnStatusBadge } from '../CnStatusBadge/index.js'
204
242
  export default {
205
243
  name: 'CnFlowBoard',
206
244
 
207
- components: { CnBuildiqEditButton, CnFlowDetail, CnFlowRunSummary, CnFlowSidebar, CnFlowStepLog, CnStatusBadge, NcButton, NcCheckboxRadioSwitch, NcNoteCard },
245
+ components: { CnBuildiqEditButton, CnDialog, CnFlowDetail, CnResourceSelect, TestTube, CnFlowRunSummary, CnFlowSidebar, CnFlowStepLog, CnStatusBadge, NcButton, NcCheckboxRadioSwitch, NcNoteCard },
208
246
 
209
247
  props: {
210
248
  /** The flow uuid, or `new`. */
@@ -225,6 +263,12 @@ export default {
225
263
  notice: { type: String, default: '' },
226
264
  /** The pill beside the title ("Case type flow"). */
227
265
  pill: { type: String, default: '' },
266
+ /**
267
+ * The records a test run can take as its subject: `{ register, schema,
268
+ * labelField?, label? }`. Draws a header button ("Testen met een zaak",
269
+ * DqFlow) that runs the saved flow once with a picked record.
270
+ */
271
+ testSubject: { type: Object, default: null },
228
272
  },
229
273
 
230
274
  emits: ['save', 'run', 'retried'],
@@ -234,10 +278,20 @@ export default {
234
278
  },
235
279
 
236
280
  data() {
237
- return { runTab: 'step' }
281
+ return { runTab: 'step', testOpen: false, testRecord: null }
238
282
  },
239
283
 
240
284
  computed: {
285
+ /** @return {boolean} Whether a test subject is declared well enough to pick one. */
286
+ testSubjectReady() {
287
+ return Boolean(this.testSubject && this.testSubject.register && this.testSubject.schema)
288
+ },
289
+
290
+ /** @return {string} The test button's label: the page's own, else the library's. */
291
+ testLabel() {
292
+ return (this.testSubject && this.testSubject.label) || t('nextcloud-vue', 'Test with a record')
293
+ },
294
+
241
295
  /** @return {boolean} Whether the palette card is drawn. */
242
296
  showPalette() {
243
297
  return this.palette === 'column' && !this.store.inRunView
@@ -454,6 +508,22 @@ export default {
454
508
  },
455
509
 
456
510
  methods: {
511
+ /**
512
+ * Run the saved flow once with the picked record as its subject, then
513
+ * close the dialog; the run shows on the canvas as any run does.
514
+ *
515
+ * @return {Promise<void>}
516
+ */
517
+ async runTest() {
518
+ const uuid = Array.isArray(this.testRecord) ? this.testRecord[0] : this.testRecord
519
+ if (!uuid) {
520
+ return
521
+ }
522
+ await this.store.run({ uuid, register: this.testSubject.register, schema: this.testSubject.schema })
523
+ this.testOpen = false
524
+ this.testRecord = null
525
+ },
526
+
457
527
  t,
458
528
 
459
529
  /**
@@ -581,6 +651,11 @@ export default {
581
651
  gap: 8px;
582
652
  }
583
653
 
654
+ .cn-flow-board__test-intro {
655
+ margin: 0 0 12px;
656
+ color: var(--color-text-maxcontrast);
657
+ }
658
+
584
659
  .cn-flow-board__path {
585
660
  margin: 0;
586
661
  color: var(--color-text-maxcontrast);
@@ -11,6 +11,7 @@
11
11
  :paletteFootnote="paletteFootnote ? cnTranslate(paletteFootnote) : ''"
12
12
  :notice="notice ? cnTranslate(notice) : ''"
13
13
  :pill="pill ? cnTranslate(pill) : ''"
14
+ :testSubject="testSubject ? { ...testSubject, label: testSubject.label ? cnTranslate(testSubject.label) : '' } : null"
14
15
  @save="onSave"
15
16
  @run="onRun" />
16
17
  <CnFlowDetail v-else
@@ -122,6 +123,16 @@ export default {
122
123
  default: '',
123
124
  },
124
125
 
126
+ /**
127
+ * Board look: the records a test run can take as its subject,
128
+ * `{ register, schema, labelField?, label? }`. Manifest key
129
+ * `config.testSubject`; `label` is translated with the app's catalogue.
130
+ */
131
+ testSubject: {
132
+ type: Object,
133
+ default: null,
134
+ },
135
+
125
136
  /**
126
137
  * Board look: an info notice under the path. Manifest key
127
138
  * `config.notice`, translated with the app's catalogue.