@conduction/nextcloud-vue 2.76.0 → 2.78.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 (173) hide show
  1. package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js +23 -0
  2. package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +18 -3
  5. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +49 -3
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +2 -1
  9. package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
  11. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +79 -1
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  13. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +229 -203
  14. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  15. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js +9 -0
  16. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js.map +1 -0
  17. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js +50 -0
  18. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js.map +1 -0
  19. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js +13 -0
  20. package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js.map +1 -0
  21. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +37 -0
  22. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  23. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +2 -2
  24. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -4
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +48 -6
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +90 -51
  31. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  32. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue.js +1 -1
  33. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js +45 -2
  34. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js.map +1 -1
  35. package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue3.js.map +1 -1
  36. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +16 -28
  37. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  38. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +12 -24
  39. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  40. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js +7 -2
  41. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js.map +1 -1
  42. package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue3.js.map +1 -1
  43. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +114 -1
  44. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  45. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +115 -62
  46. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  47. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +89 -7
  48. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
  49. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js +62 -30
  50. package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
  51. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +7 -1
  52. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  53. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  54. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +4 -1
  55. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  56. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  57. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +104 -13
  58. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  59. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +13 -9
  60. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  61. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +199 -7
  62. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +155 -119
  64. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  65. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +6 -0
  66. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  67. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -1
  68. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  69. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +36 -0
  70. package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
  71. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +35 -23
  72. package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
  73. package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js +6 -2
  74. package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js.map +1 -1
  75. package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue3.js.map +1 -1
  76. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
  77. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  78. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  79. package/dist/esm/composables/useFormDraft.js +11 -1
  80. package/dist/esm/composables/useFormDraft.js.map +1 -1
  81. package/dist/esm/composables/useListView.js +4 -0
  82. package/dist/esm/composables/useListView.js.map +1 -1
  83. package/dist/esm/composables/useObjectPresence.js +15 -4
  84. package/dist/esm/composables/useObjectPresence.js.map +1 -1
  85. package/dist/esm/index.js +2 -1
  86. package/dist/esm/index.js.map +1 -1
  87. package/dist/esm/l10n/en.json.js +32 -1
  88. package/dist/esm/l10n/en.json.js.map +1 -1
  89. package/dist/esm/l10n/nl.json.js +32 -1
  90. package/dist/esm/l10n/nl.json.js.map +1 -1
  91. package/dist/esm/nextcloud-vue.css +500 -75
  92. package/dist/esm/store/plugins/interactions.js +20 -10
  93. package/dist/esm/store/plugins/interactions.js.map +1 -1
  94. package/dist/esm/store/useObjectStore.js +17 -0
  95. package/dist/esm/store/useObjectStore.js.map +1 -1
  96. package/dist/esm/utils/badgeVariants.js +32 -0
  97. package/dist/esm/utils/badgeVariants.js.map +1 -0
  98. package/dist/esm/utils/builtInFormatters.js +15 -7
  99. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  100. package/dist/esm/utils/groupAutocomplete.js +63 -1
  101. package/dist/esm/utils/groupAutocomplete.js.map +1 -1
  102. package/dist/esm/utils/lensAvailability.js +90 -0
  103. package/dist/esm/utils/lensAvailability.js.map +1 -0
  104. package/dist/esm/utils/personalLenses.js +13 -6
  105. package/dist/esm/utils/personalLenses.js.map +1 -1
  106. package/dist/esm/utils/recordInteractions.js +31 -13
  107. package/dist/esm/utils/recordInteractions.js.map +1 -1
  108. package/dist/esm/utils/resolveFilterTokens.js +46 -3
  109. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  110. package/dist/esm/utils/schema.js +1 -1
  111. package/dist/esm/utils/schema.js.map +1 -1
  112. package/dist/esm/utils/sentinelTokens.js +4 -4
  113. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  114. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  115. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  116. package/dist/esm/utils/widgetVisibility.js +46 -1
  117. package/dist/esm/utils/widgetVisibility.js.map +1 -1
  118. package/dist/nextcloud-vue.cjs.js +4389 -2890
  119. package/dist/nextcloud-vue.cjs.js.map +2 -2
  120. package/dist/nextcloud-vue.css +500 -75
  121. package/l10n/en.json +32 -1
  122. package/l10n/nl.json +32 -1
  123. package/package.json +1 -1
  124. package/src/components/CnAiCompanion/CnAiCompanion.vue +23 -0
  125. package/src/components/CnAppNav/CnAppNav.vue +44 -5
  126. package/src/components/CnBoardView/CnBoardView.vue +2 -1
  127. package/src/components/CnCellRenderer/CnCellRenderer.md +1 -0
  128. package/src/components/CnCellRenderer/CnCellRenderer.vue +106 -9
  129. package/src/components/CnCellRenderer/CnGroupNameCell.vue +61 -0
  130. package/src/components/CnChartWidget/CnChartWidget.vue +39 -2
  131. package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -4
  132. package/src/components/CnDataTable/CnDataTable.md +9 -0
  133. package/src/components/CnDataTable/CnDataTable.vue +87 -22
  134. package/src/components/CnDateRangePicker/CnDateRangePicker.vue +44 -1
  135. package/src/components/CnDetailPage/CnDetailPage.vue +10 -27
  136. package/src/components/CnFavouriteToggle/CnFavouriteToggle.vue +7 -2
  137. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +204 -5
  138. package/src/components/CnFollowToggle/CnFollowToggle.vue +118 -13
  139. package/src/components/CnFormDialog/CnFormDialog.vue +7 -1
  140. package/src/components/CnFormPage/CnFormPage.vue +4 -1
  141. package/src/components/CnIndexPage/CnIndexPage.md +36 -0
  142. package/src/components/CnIndexPage/CnIndexPage.vue +112 -19
  143. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +226 -2
  144. package/src/components/CnObjectSidebar/CnFilesTab.vue +9 -0
  145. package/src/components/CnStatWidget/CnStatWidget.vue +52 -2
  146. package/src/components/CnStatusBadge/CnStatusBadge.vue +6 -2
  147. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
  148. package/src/composables/__tests__/useObjectPresence.spec.js +24 -0
  149. package/src/composables/useFormDraft.js +11 -1
  150. package/src/composables/useListView.js +4 -0
  151. package/src/composables/useObjectPresence.js +15 -4
  152. package/src/css/badge.css +31 -0
  153. package/src/css/kpi-card.css +51 -0
  154. package/src/css/look-board-detail.css +40 -2
  155. package/src/css/look-board-index.css +36 -0
  156. package/src/css/look-board.css +8 -0
  157. package/src/css/table.css +82 -17
  158. package/src/index.js +2 -1
  159. package/src/schemas/app-manifest-v2.schema.json +15 -11
  160. package/src/store/plugins/interactions.js +20 -10
  161. package/src/store/useObjectStore.js +17 -0
  162. package/src/utils/badgeVariants.js +29 -0
  163. package/src/utils/builtInFormatters.js +15 -7
  164. package/src/utils/groupAutocomplete.js +75 -0
  165. package/src/utils/index.js +1 -1
  166. package/src/utils/lensAvailability.js +86 -0
  167. package/src/utils/personalLenses.js +13 -6
  168. package/src/utils/recordInteractions.js +31 -13
  169. package/src/utils/resolveFilterTokens.js +45 -2
  170. package/src/utils/schema.js +1 -1
  171. package/src/utils/sentinelTokens.js +4 -4
  172. package/src/utils/validateManifestV2.compiled.js +2 -2
  173. package/src/utils/widgetVisibility.js +45 -0
@@ -1 +1 @@
1
- {"version":3,"file":"CnFormPage.vue3.js","sources":["../../../../src/components/CnFormPage/CnFormPage.vue?vue&type=template&id=4fd51f4f&lang.js"],"sourcesContent":["<!--\n CnFormPage — Manifest-driven runtime form.\n\n Renders a flat field set + submit button declared in\n `pages[].config` for `type: \"form\"` pages. Closes the gap that\n forces every consumer's runtime-form route (public surveys, \"request\n a quote\" pages, ticket-create routes when no detail-page round-trip\n is needed) onto `type: \"custom\"`.\n\n manifest-form-logic adds three declarative capabilities on top of the\n flat field set (all additive — a manifest with no `steps` and no\n `field.visibleWhen` / `field.validation` renders byte-for-byte as\n before):\n\n - `steps` prop: ordered `{ id, title, description?, fields[] }`\n wizard groups (`fields[]` are KEY REFERENCES into the `fields`\n prop). An accessible step indicator + Back/Next/Submit footer\n renders when `steps` is non-empty; a step whose fields are ALL\n hidden by conditions is skipped in both navigation directions.\n - `field.visibleWhen`: the shared manifest visibility predicate.\n LOCAL-mode conditions (no `endpoint`/`source`) are evaluated\n SYNCHRONOUSLY against the live `formData`, in field declaration\n order (a hidden field reads as `undefined` for every later\n field's condition — cascading hides). `endpoint`/`source`\n conditions are resolved ONCE at mount (fail-safe: hidden on\n error) and never re-evaluated on keystrokes.\n - `field.validation`: `{ required, min, max, pattern, message }`,\n enforced via `validateFieldValue()` before Next/Submit advances.\n Errors render through `NcInputField`-family `error`/`helperText`\n props (via `cnRenderFormField`) or an adjacent `role=\"alert\"`\n element for widgets without native error props.\n\n DECISION (spec-fixed): a field hidden by its `visibleWhen` is\n EXCLUDED from validation AND from the dispatched payload — the\n payload always equals what the user saw and confirmed — but its\n draft value is RETAINED in component state, so toggling the\n condition back restores it.\n\n Submit dispatch picks one of two paths based on which prop is set:\n\n - `submitEndpoint` — `axios[method](url, effectivePayload)`. URL\n `:param` segments are resolved against `$route.params`.\n - `submitHandler` — looks the name up in the customComponents\n registry and calls the resolved value with\n `(effectivePayload, $route, $router)`.\n\n Field rendering is delegated to `cnRenderFormField` from\n `@conduction/nextcloud-vue/composables` so the same input set\n CnSettingsPage uses (boolean, number, string, password, enum, json)\n is available without duplication, plus the form-only `file` type\n (CnFileField, whose value is the picked file as a `data:` URL).\n `widget: \"textarea\"` overrides the default string input.\n\n Slots (mirrors CnSettingsPage):\n - `#header` — overrides CnPageHeader. Scope `{ title, description }`.\n - `#actions` — right-aligned actions area.\n - `#field-<key>` — replaces the input for a specific field.\n Scope `{ field, value, onInput, error }`.\n - `#submit` — replaces the submit button. Scope\n `{ submitting, dirty, submit }`.\n\n Events:\n - `@input` — `{ key, value }` on every field change.\n - `@step` — `{ from, to }` on step navigation.\n - `@submit` — the effective payload (visible fields only) after\n successful submit.\n - `@error` — error object after failed submit.\n\n Spec: REQ-MFPT-* (manifest-form-page-type), REQ-MFL-* (manifest-form-logic).\n-->\n<template>\n\t<div class=\"cn-form-page\" :data-mode=\"mode\" data-testid=\"cn-form-page\">\n\t\t<!--\n\t\t\t@slot header\n\t\t\t@description Replaces the default `CnPageHeader`. Receives `{ title, description }` as scoped props\n\t\t\tso a custom header can mirror the manifest-supplied labels.\n\t\t-->\n\t\t<slot\n\t\t\tname=\"header\"\n\t\t\t:title=\"title\"\n\t\t\t:description=\"description\">\n\t\t\t<CnPageHeader\n\t\t\t\tv-if=\"title\"\n\t\t\t\t:title=\"title\"\n\t\t\t\t:description=\"description\" />\n\t\t</slot>\n\n\t\t<div v-if=\"$slots.actions || $slots.actions\" class=\"cn-form-page__actions\">\n\t\t\t<!-- @slot actions Action buttons (back, cancel, …) rendered above the form. -->\n\t\t\t<slot name=\"actions\" />\n\t\t</div>\n\n\t\t<!-- Success banner -->\n\t\t<div v-if=\"submitted\" class=\"cn-form-page__success\">\n\t\t\t{{ resolveLabel(successMessage) }}\n\t\t</div>\n\n\t\t<!-- A draft this form was left in: offered, never applied. -->\n\t\t<NcNoteCard\n\t\t\tv-if=\"draftOffer !== null\"\n\t\t\ttype=\"info\"\n\t\t\tdata-testid=\"cn-form-page-draft-offer\">\n\t\t\t{{ t('nextcloud-vue', 'You have unsaved changes from an earlier visit.') }}\n\t\t\t<div class=\"cn-form-page__draft-actions\">\n\t\t\t\t<NcButton data-testid=\"cn-form-page-draft-restore\" @click=\"restoreDraft\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Restore') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton data-testid=\"cn-form-page-draft-discard\" @click=\"discardDraft\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Discard') }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</NcNoteCard>\n\n\t\t<!-- Form body -->\n\t\t<form\n\t\t\tv-if=\"!submitted || mode !== 'public'\"\n\t\t\tclass=\"cn-form-page__form\"\n\t\t\t@submit.prevent=\"submit\">\n\t\t\t<!-- Paste to fill: only where it may be used (see smartPasteAvailable). -->\n\t\t\t<div v-if=\"smartPasteAvailable\" class=\"cn-form-page__smart-paste\">\n\t\t\t\t<NcButton type=\"button\" data-testid=\"cn-form-page-smart-paste\" @click=\"smartPasteOpen = true\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Paste to fill') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"hasSuggestions\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\tdata-testid=\"cn-form-page-accept-all\"\n\t\t\t\t\t@click=\"acceptAllSuggestions\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Accept all') }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t\t<p\n\t\t\t\tclass=\"cn-form-page__smart-paste-notice\"\n\t\t\t\taria-live=\"polite\"\n\t\t\t\tdata-testid=\"cn-form-page-smart-paste-notice\">\n\t\t\t\t{{ smartPasteNotice }}\n\t\t\t</p>\n\n\t\t\t<!-- Board look: every error of a failed submit, linked to its control. -->\n\t\t\t<CnFormErrorSummary\n\t\t\t\tv-if=\"isBoardLook && summaryItems.length > 0\"\n\t\t\t\tref=\"errorSummary\"\n\t\t\t\t:errors=\"summaryItems\"\n\t\t\t\t@focusField=\"focusField\" />\n\n\t\t\t<!-- Board look: the same stepper as CnWizardDialog. -->\n\t\t\t<CnStepper\n\t\t\t\tv-if=\"hasSteps && isBoardLook\"\n\t\t\t\t:steps=\"boardSteps\"\n\t\t\t\t:currentIndex=\"currentStepIndex\"\n\t\t\t\tlook=\"board\"\n\t\t\t\t:ariaLabel=\"t('nextcloud-vue', 'Form steps')\" />\n\n\t\t\t<!-- Step indicator — only rendered when `steps` is non-empty. -->\n\t\t\t<nav\n\t\t\t\tv-if=\"hasSteps && !isBoardLook\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Form steps')\"\n\t\t\t\tclass=\"cn-form-page__steps-nav\">\n\t\t\t\t<ol class=\"cn-form-page__steps\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"(step, index) in steps\"\n\t\t\t\t\t\t:key=\"step.id\"\n\t\t\t\t\t\tclass=\"cn-form-page__step\"\n\t\t\t\t\t\t:class=\"{\n\t\t\t\t\t\t\t'cn-form-page__step--current': index === currentStepIndex,\n\t\t\t\t\t\t\t'cn-form-page__step--done': index < currentStepIndex,\n\t\t\t\t\t\t}\"\n\t\t\t\t\t\t:aria-current=\"index === currentStepIndex ? 'step' : null\">\n\t\t\t\t\t\t<span v-if=\"index < currentStepIndex\" class=\"cn-form-page__step-check\" aria-hidden=\"true\">✓</span>\n\t\t\t\t\t\t{{ resolveLabel(step.title) }}\n\t\t\t\t\t</li>\n\t\t\t\t</ol>\n\t\t\t</nav>\n\n\t\t\t<p v-if=\"hasSteps && currentStepDescription\" class=\"cn-form-page__step-description\">\n\t\t\t\t{{ resolveLabel(currentStepDescription) }}\n\t\t\t</p>\n\n\t\t\t<!-- A public board look form says what \"optional\" means. -->\n\t\t\t<p v-if=\"showsOptionalNotice\"\n\t\t\t\tclass=\"cn-form-page__optional-notice\"\n\t\t\t\tdata-testid=\"cn-form-page-optional-notice\">\n\t\t\t\t{{ optionalNoticeLabel }}\n\t\t\t</p>\n\n\t\t\t<div\n\t\t\t\tv-for=\"field in visibleCurrentStepFields\"\n\t\t\t\t:key=\"field.key\"\n\t\t\t\t:ref=\"`field-${field.key}`\"\n\t\t\t\tclass=\"cn-form-page__field\"\n\t\t\t\t:class=\"{\n\t\t\t\t\t'cn-form-field--half': isBoardLook && field.width === 'half',\n\t\t\t\t\t'cn-form-field--invalid': isBoardLook && !!fieldErrors[field.key],\n\t\t\t\t}\"\n\t\t\t\t:data-field-key=\"field.key\"\n\t\t\t\t:aria-describedby=\"fieldErrors[field.key] && !fieldHasNativeErrorSupport(field) ? `cn-form-page__field-error-${field.key}` : null\">\n\t\t\t\t<CnFormField\n\t\t\t\t\tv-if=\"showsBoardHead(field)\"\n\t\t\t\t\t:controlId=\"controlIdFor(field)\"\n\t\t\t\t\t:text=\"resolveLabel(field.label) || field.key\"\n\t\t\t\t\t:optional=\"!isFieldRequired(field)\"\n\t\t\t\t\t:optionalLabel=\"optionalLabel\"\n\t\t\t\t\t:error=\"fieldErrors[field.key] || ''\"\n\t\t\t\t\t:errorId=\"errorIdFor(field)\" />\n\t\t\t\t<!--\n\t\t\t\t\t@slot field-${field.key}\n\t\t\t\t\t@description Per-field override slot. Replaces the auto-rendered input for one specific field.\n\t\t\t\t\tScoped props: `{ field, value, onInput, error }` — `onInput(v)` updates the field via `updateField`.\n\t\t\t\t-->\n\t\t\t\t<slot\n\t\t\t\t\t:name=\"`field-${field.key}`\"\n\t\t\t\t\t:field=\"field\"\n\t\t\t\t\t:value=\"formData[field.key]\"\n\t\t\t\t\t:onInput=\"(v) => updateField(field.key, v)\"\n\t\t\t\t\t:error=\"fieldErrors[field.key] || null\">\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"resolveFieldRender(field).tag\"\n\t\t\t\t\t\tv-if=\"resolveFieldRender(field)\"\n\t\t\t\t\t\tv-cn-select-aria=\"selectAria(field)\"\n\t\t\t\t\t\tv-bind=\"fieldProps(field)\"\n\t\t\t\t\t\tv-on=\"resolveFieldRender(field).listeners\">\n\t\t\t\t\t\t<!-- NcCheckboxRadioSwitch puts its label in the slot -->\n\t\t\t\t\t\t<template\n\t\t\t\t\t\t\tv-if=\"resolveFieldRender(field).kind === 'boolean'\">\n\t\t\t\t\t\t\t{{ resolveFieldRender(field).labelText }}\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</component>\n\t\t\t\t</slot>\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"fieldErrors[field.key] && !fieldHasNativeErrorSupport(field) && !showsBoardHead(field)\"\n\t\t\t\t\t:id=\"`cn-form-page__field-error-${field.key}`\"\n\t\t\t\t\tclass=\"cn-form-page__field-error\"\n\t\t\t\t\trole=\"alert\">\n\t\t\t\t\t{{ fieldErrors[field.key] }}\n\t\t\t\t</p>\n\t\t\t\t<span v-if=\"suggestions[field.key]\" class=\"cn-form-page__suggested\" data-testid=\"cn-form-page-suggested\">\n\t\t\t\t\t<span class=\"cn-form-page__suggested-tag\">{{ t('nextcloud-vue', 'Suggested') }}</span>\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Accept suggestion for {field}', { field: resolveLabel(field.label) || field.key })\"\n\t\t\t\t\t\t@click=\"acceptSuggestion(field.key)\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Accept') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</span>\n\t\t\t\t<small\n\t\t\t\t\tv-if=\"field.help\"\n\t\t\t\t\t:id=\"helpIdFor(field)\"\n\t\t\t\t\tclass=\"cn-form-page__field-help\">\n\t\t\t\t\t{{ resolveLabel(field.help) }}\n\t\t\t\t</small>\n\t\t\t\t<small\n\t\t\t\t\tv-if=\"assignedFrom[field.key]\"\n\t\t\t\t\tclass=\"cn-form-page__field-assigned\"\n\t\t\t\t\taria-live=\"polite\"\n\t\t\t\t\tdata-testid=\"cn-form-page-assigned\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Filled in from {field}', { field: assignedFrom[field.key] }) }}\n\t\t\t\t</small>\n\t\t\t\t<small\n\t\t\t\t\tv-if=\"calculating[field.key]\"\n\t\t\t\t\tclass=\"cn-form-page__field-calculating\"\n\t\t\t\t\trole=\"status\"\n\t\t\t\t\tdata-testid=\"cn-form-page-calculating\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Calculating') }}\n\t\t\t\t</small>\n\t\t\t\t<small\n\t\t\t\t\tv-if=\"calcErrors[field.key]\"\n\t\t\t\t\tclass=\"cn-form-page__field-error\"\n\t\t\t\t\trole=\"alert\"\n\t\t\t\t\tdata-testid=\"cn-form-page-calc-error\">\n\t\t\t\t\t{{ calcErrors[field.key] }}\n\t\t\t\t</small>\n\t\t\t</div>\n\n\t\t\t<!-- Error -->\n\t\t\t<p v-if=\"lastError\" class=\"cn-form-page__error\">\n\t\t\t\t{{ lastError }}\n\t\t\t</p>\n\n\t\t\t<!-- Conditions the host reports as unmet; with blockSubmit they hold the submit. -->\n\t\t\t<NcNoteCard\n\t\t\t\tv-if=\"unmetConditions.length > 0\"\n\t\t\t\ttype=\"warning\"\n\t\t\t\tclass=\"cn-form-page__unmet\"\n\t\t\t\tdata-testid=\"cn-form-page-unmet\">\n\t\t\t\t<ul id=\"cn-form-page-unmet-list\">\n\t\t\t\t\t<li v-for=\"(condition, index) in unmetConditions\" :key=\"index\">\n\t\t\t\t\t\t{{ condition.message }}\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</NcNoteCard>\n\n\t\t\t<div class=\"cn-form-page__submit\" :class=\"{ 'cn-form-page__footer': isBoardLook }\">\n\t\t\t\t<!-- Board look: Cancel on the first step, at the left edge of the card. -->\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"isBoardLook && canCancel && (!hasSteps || isFirstStep)\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tdata-testid=\"cn-form-page-cancel\"\n\t\t\t\t\t@click=\"cancel\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"hasSteps && !isFirstStep\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tdata-testid=\"cn-form-page-back\"\n\t\t\t\t\t@click=\"back\">\n\t\t\t\t\t<template v-if=\"isBoardLook\" #icon>\n\t\t\t\t\t\t<ChevronLeft :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ isBoardLook ? t('nextcloud-vue', 'Previous') : t('nextcloud-vue', 'Back') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"hasSteps && !isLastStep\"\n\t\t\t\t\tclass=\"cn-form-page__primary\"\n\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t:alignment=\"isBoardLook ? 'center-reverse' : 'center'\"\n\t\t\t\t\tdata-testid=\"cn-form-page-next\"\n\t\t\t\t\t@click=\"next\">\n\t\t\t\t\t<template v-if=\"isBoardLook\" #icon>\n\t\t\t\t\t\t<ChevronRight :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ t('nextcloud-vue', 'Next') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- @slot submit Replaces the default submit button. -->\n\t\t\t\t<!-- @binding {boolean} submitting Whether a submit is in flight. -->\n\t\t\t\t<!-- @binding {boolean} dirty Whether the form has unsaved changes. -->\n\t\t\t\t<!-- @binding {Function} submit Call to submit the form programmatically. -->\n\t\t\t\t<slot\n\t\t\t\t\tv-if=\"!hasSteps || isLastStep\"\n\t\t\t\t\tname=\"submit\"\n\t\t\t\t\t:submitting=\"submitting\"\n\t\t\t\t\t:dirty=\"dirty\"\n\t\t\t\t\t:submit=\"submit\">\n\t\t\t\t\t<!--\n\t\t\t\t\t\t`type`, not `native-type` — @nextcloud/vue 9's NcButton declares\n\t\t\t\t\t\tits native HTML button-type prop as `type` (`ButtonType = 'submit'\n\t\t\t\t\t\t| 'reset' | 'button'`, default `'button'`) and has no `nativeType`\n\t\t\t\t\t\tprop at all. `native-type` therefore fell through as an inert\n\t\t\t\t\t\tattribute and every button silently rendered as the component's\n\t\t\t\t\t\tdefault `type=\"button\"` — including THIS one. A `type=\"button\"`\n\t\t\t\t\t\tbutton inside a `<form>` does NOT fire the form's native `submit`\n\t\t\t\t\t\tevent on click, so `<form @submit.prevent=\"submit\">` above never\n\t\t\t\t\t\tran: clicking \"Submit\" did nothing, silently, in a real browser.\n\t\t\t\t\t\tJest's own local NcButton stub (`tests/components/CnFormPage.spec.js`)\n\t\t\t\t\t\tnever bound `:type` either, so its plain `<button>` defaulted to\n\t\t\t\t\t\tthe OPPOSITE — the HTML spec's implicit `type=\"submit\"` for a\n\t\t\t\t\t\ttype-less button in a form — which is why every jest submit test\n\t\t\t\t\t\tpassed while the real component was broken.\n\t\t\t\t\t-->\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tclass=\"cn-form-page__primary\"\n\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t:disabled=\"submitting || submitBlocked\"\n\t\t\t\t\t\t:title=\"blockedReason || null\"\n\t\t\t\t\t\t:aria-describedby=\"submitBlocked ? 'cn-form-page-unmet-list' : null\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<NcLoadingIcon v-if=\"submitting\" :size=\"20\" />\n\t\t\t\t\t\t\t<Send v-else :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ resolveLabel(submitLabel) }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</slot>\n\t\t\t</div>\n\t\t\t<!-- Local draft indicator: Saving, then Saved; one polite announcement per change. -->\n\t\t\t<p\n\t\t\t\tv-if=\"recoverDraft\"\n\t\t\t\tclass=\"cn-form-page__draft-state\"\n\t\t\t\taria-live=\"polite\"\n\t\t\t\tdata-testid=\"cn-form-page-draft-state\">\n\t\t\t\t{{ draftIndicatorLabel }}\n\t\t\t</p>\n\t\t</form>\n\t\t<CnSmartPasteDialog\n\t\t\tv-if=\"smartPasteOpen\"\n\t\t\t:hint=\"smartPaste && smartPaste.hint ? resolveLabel(smartPaste.hint) : ''\"\n\t\t\t:busy=\"smartPasteBusy\"\n\t\t\t:error=\"smartPasteError\"\n\t\t\t@fill=\"onSmartPasteFill\"\n\t\t\t@close=\"smartPasteOpen = false\" />\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport ChevronLeft from 'vue-material-design-icons/ChevronLeft.vue'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport Send from 'vue-material-design-icons/Send.vue'\nimport CnSmartPasteDialog from '../../dialogs/CnSmartPasteDialog.vue'\nimport CnFormErrorSummary from '../CnFormErrorSummary/CnFormErrorSummary.vue'\nimport CnFormField from '../CnFormField/CnFormField.vue'\nimport CnStepper from '../CnStepper/CnStepper.vue'\nimport { cnRenderFormField } from '../../composables/cnFormFieldRenderer.js'\nimport { draftIndicatorText, draftKey, formDraftMixin, readDraft } from '../../composables/useFormDraft.js'\nimport { normalizeLook } from '../../composables/useLook.js'\nimport { cnSelectAria } from '../../directives/cnSelectAria.js'\nimport { loadCurrentUserProfile } from '../../utils/currentUserProfile.js'\nimport { computeAssignments, resolveFieldDefaults } from '../../utils/formAssign.js'\nimport { validateFieldValue } from '../../utils/formValidation.js'\nimport { serverErrorMessage } from '../../utils/serverErrorMessage.js'\nimport { evaluateVisibleWhen, evaluateVisibleWhenLocal } from '../../utils/visibleWhen.js'\nimport { CnPageHeader } from '../CnPageHeader/index.js'\n\nconst ALLOWED_METHODS = ['POST', 'PUT', 'PATCH']\n\n/**\n * Resolve `:param` segments in a URL string against `$route.params`.\n * Mirrors the `:id` substitution Vue Router performs on its own\n * routes; reused here because the manifest declares the URL as a\n * static string and we want the runtime to fill the slot.\n *\n * @param {string} url URL template, e.g. `/api/survey/:token`.\n * @param {object} params $route.params.\n * @return {string}\n */\nfunction resolveParams(url, params) {\n\tif (!url || !params) {\n\t\treturn url\n\t}\n\treturn String(url).replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (match, name) => {\n\t\tconst value = params[name]\n\t\treturn value === undefined || value === null ? match : encodeURIComponent(String(value))\n\t})\n}\n\n/**\n * @event submit Fired after a successful submit. Payload: the effective payload (visible fields only; hidden-by-condition field keys excluded).\n * @event error Fired when submit fails. Payload: the thrown error / rejected reason.\n * @event input Fired on every field-level update; payload is `{ key, value }`.\n * @event step Fired on step navigation (Next/Back); payload is `{ from, to }` (step indices).\n *\n * @slot header Replaces the default `CnPageHeader`. Scoped props: `{ title, description }`.\n * @slot actions Optional slot for action buttons rendered above the form. Hidden when empty.\n * @slot field-${field.key} Per-field override slot. Replaces the auto-rendered input for one specific field. Scoped props: `{ field, value, onInput, error }`.\n * @slot submit Replaces the default submit button. Scoped props: `{ submitting, dirty, submit }`.\n */\nexport default {\n\tname: 'CnFormPage',\n\n\tdirectives: { cnSelectAria },\n\n\tcomponents: {\n\t\tCnFormErrorSummary,\n\t\tCnFormField,\n\t\tChevronLeft,\n\t\tChevronRight,\n\t\tCnPageHeader,\n\t\tCnSmartPasteDialog,\n\t\tCnStepper,\n\t\tNcButton,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t\tSend,\n\t},\n\n\tmixins: [formDraftMixin()],\n\n\tinject: {\n\t\t/**\n\t\t * Custom-component registry from CnAppRoot. Used to resolve the\n\t\t * `submitHandler` name to a concrete function. Defaults to an\n\t\t * empty object when the page is mounted standalone.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcnCustomComponents: { default: () => ({}) },\n\n\t\t/**\n\t\t * The label lookup from CnAppRoot (the manifest's own translations, then\n\t\t * the host's translate). Used when no `translate` prop is given, so a\n\t\t * manifest form page speaks the user's language.\n\t\t *\n\t\t * @type {Function|null}\n\t\t */\n\t\tcnTranslate: { default: null },\n\n\t\t/** The app's look (a page with `config.look` re-provides its own). */\n\t\tcnLook: { default: 'nextcloud' },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Where Cancel goes (board look). With a route, Cancel navigates there and\n\t\t * emits `cancel`; with only a `cancel` listener it just emits. With neither\n\t\t * the board footer has no Cancel.\n\t\t */\n\t\tcancelRoute: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The word shown as \"(optional)\" after an optional field's label in the board look. */\n\t\toptionalLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'optional'),\n\t\t},\n\n\t\t/** The sentence a public board look form shows above its first field. */\n\t\toptionalNoticeLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'A field without (optional) must be filled in.'),\n\t\t},\n\n\t\t/** Show the optional-fields sentence on a public board look form. Set false to hide it. */\n\t\tshowOptionalNotice: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Keep what the user typed in the browser and offer it back when the\n\t\t * form reopens (local only; nothing reaches the server). Off by default:\n\t\t * a public form can run on a shared kiosk, so a host opts in.\n\t\t */\n\t\trecoverDraft: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** The app the draft belongs to; part of the draft's storage key. */\n\t\tdraftAppId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Who is typing; part of the draft's storage key so a shared browser profile never leaks a draft between users. */\n\t\tdraftUserId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Names this form in the draft's storage key (for example the form id), so two forms never share a draft. */\n\t\tdraftScope: {\n\t\t\ttype: String,\n\t\t\tdefault: 'form',\n\t\t},\n\n\t\t/**\n\t\t * Host function that calculates a field: `(fieldKey, answers) => Promise<value>`.\n\t\t * Called for a field declaring `calculate.inputs` when one of those answers\n\t\t * changes, after 400 ms of quiet; the field is read-only. A rejection keeps\n\t\t * the last value and says it could not be calculated. The library knows no\n\t\t * rule language: calculation belongs to the host.\n\t\t *\n\t\t * @type {((fieldKey: string, answers: object) => Promise<unknown>)|null}\n\t\t */\n\t\tcalculate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Conditions the host reports as unmet, shown above the submit button.\n\t\t *\n\t\t * @type {Array<{message: string}>}\n\t\t */\n\t\tunmetConditions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Disable submit while `unmetConditions` is not empty; the first message is the reason. */\n\t\tblockSubmit: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Form fields. Each MUST conform to the `formField` $def. */\n\t\tfields: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Multi-step wizard groups: `Array<{id, title, description?,\n\t\t * fields: string[]}>`. `fields[]` entries are KEY REFERENCES into\n\t\t * the `fields` prop (single source of truth — no field\n\t\t * duplication). Empty (the default) renders today's single-step\n\t\t * form unchanged: no step indicator, no Next/Back.\n\t\t */\n\t\tsteps: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Registered submit handler name. Resolves against the\n\t\t * `cnCustomComponents` registry (or `customComponents` prop).\n\t\t * Mutually exclusive with `submitEndpoint` at the validator\n\t\t * level; the component itself prefers `submitEndpoint` when both\n\t\t * are set so a stale manifest doesn't crash.\n\t\t */\n\t\tsubmitHandler: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * URL the form data is dispatched to. `:paramName` segments are\n\t\t * resolved against `$route.params` at submit time.\n\t\t */\n\t\tsubmitEndpoint: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** HTTP method for endpoint mode. POST | PUT | PATCH. */\n\t\tsubmitMethod: {\n\t\t\ttype: String,\n\t\t\tdefault: 'POST',\n\t\t\tvalidator: (v) => typeof v === 'string' && ALLOWED_METHODS.includes(v.toUpperCase()),\n\t\t},\n\n\t\t/**\n\t\t * Form mode. `public` shows the success banner and hides the\n\t\t * form on submit; `edit` and `create` keep the form mounted so\n\t\t * the consumer can route away.\n\t\t */\n\t\tmode: {\n\t\t\ttype: String,\n\t\t\tdefault: 'public',\n\t\t\tvalidator: (v) => ['edit', 'create', 'public'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * \"Fill from pasted text\": `{ enabled, fields, hint?, handler }`. The\n\t\t * handler is a registry entry (a function, or `{ fill, available }`)\n\t\t * that proposes values for the allowed `fields`. Never shown in\n\t\t * `public` mode, when the handler is missing, or when it reports\n\t\t * unavailable.\n\t\t *\n\t\t * @type {{enabled: boolean, fields: string[], hint?: string, handler: string}|null}\n\t\t */\n\t\tsmartPaste: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** i18n key for the submit button label. */\n\t\tsubmitLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Submit'),\n\t\t},\n\n\t\t/** i18n key for the success banner. */\n\t\tsuccessMessage: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Thank you!'),\n\t\t},\n\n\t\t/** Pre-filled form state. Consumed by `mode: \"edit\"`. */\n\t\tinitialValue: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Page title. Forwarded to CnPageHeader. */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Page description. Forwarded to CnPageHeader. */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional translation function. When provided, applied to\n\t\t * field labels, success messages, `validation.message`, etc.\n\t\t * Defaults to identity.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Optional explicit custom-component registry. When set, takes\n\t\t * precedence over the injected `cnCustomComponents`. Mirrors\n\t\t * the resolution order in CnPageRenderer / CnSettingsPage.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcustomComponents: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\t/**\n\t * Events:\n\t *\n\t * @event submit\n\t * @description Fired after a successful submit (handler returned, or endpoint POST/PUT/PATCH succeeded).\n\t * Payload is the effective payload — `formData` with hidden-by-condition field keys removed.\n\t *\n\t * @event error\n\t * @description Fired when submit fails (handler threw, or endpoint returned a non-2xx response).\n\t * Payload is the thrown error / rejected reason.\n\t *\n\t * @event input\n\t * @description Fired on every field-level update; payload is `{ key, value }`.\n\t *\n\t * @event step\n\t * @description Fired on Next/Back step navigation; payload is `{ from, to }` (step indices).\n\t */\n\temits: ['submit', 'error', 'input', 'step', 'cancel'],\n\n\tdata() {\n\t\treturn {\n\t\t\tformData: this.initialWithDefaults(),\n\t\t\t/** The values the form opened with (initial values plus defaults), to tell a changed form from a fresh one. */\n\t\t\tbaseline: JSON.stringify(this.initialWithDefaults()),\n\t\t\t/** Fields the user edited by hand: their `assign` rules stop until reset. */\n\t\t\thandEdited: {},\n\t\t\t/** Field key -> label of the answer a rule filled it in from. */\n\t\t\tassignedFrom: {},\n\t\t\t/** Field keys being calculated now. */\n\t\t\tcalculating: {},\n\t\t\t/** Field key -> sentence when the last calculation failed. */\n\t\t\tcalcErrors: {},\n\t\t\t/** The signed-in user's profile (`displayName`, `email`) for `@me.*` defaults. */\n\t\t\tmeProfile: {},\n\t\t\t/** Smart paste: the handler said it is available (resolved at mount). */\n\t\t\tsmartPasteAvailable: false,\n\t\t\tsmartPasteOpen: false,\n\t\t\tsmartPasteBusy: false,\n\t\t\tsmartPasteError: '',\n\t\t\tsmartPasteNotice: '',\n\t\t\t/** Keys of fields filled by smart paste and not yet accepted or edited. */\n\t\t\tsuggestions: {},\n\t\t\tsubmitting: false,\n\t\t\tsubmitted: false,\n\t\t\tlastError: null,\n\t\t\t/** Current wizard step index (unused when `steps` is empty). */\n\t\t\tcurrentStepIndex: 0,\n\t\t\t/** Keys of the fields the last failed validation flagged, in form order (the error summary). */\n\t\t\tsummaryKeys: [],\n\t\t\t/** Per-field validation error messages, keyed by field.key. */\n\t\t\tfieldErrors: {},\n\t\t\t/**\n\t\t\t * Cache of resolved `endpoint`/`source` visibleWhen outcomes,\n\t\t\t * keyed by field.key. Resolved ONCE in `mounted()` — see the\n\t\t\t * module docblock. `undefined` (not yet resolved) reads as\n\t\t\t * hidden, mirroring CnBannerWidget's fail-safe/pending posture.\n\t\t\t */\n\t\t\tremoteVisibility: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {Array<{id: string, label: string}>} The steps in the stepper's shape. */\n\t\tboardSteps() {\n\t\t\treturn this.steps.map((step) => ({ id: step.id, label: this.resolveLabel(step.title) }))\n\t\t},\n\n\t\t/** @return {boolean} Whether the page has somewhere to cancel to. */\n\t\tcanCancel() {\n\t\t\tconst listener = this.$.vnode && this.$.vnode.props && this.$.vnode.props.onCancel\n\t\t\treturn this.cancelRoute !== '' || !!listener\n\t\t},\n\n\t\t/** @return {boolean} Whether the board look is active. */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\t/** @return {Array<{key: string, message: string, controlId: string}>} The errors the summary lists. */\n\t\tsummaryItems() {\n\t\t\treturn this.summaryKeys\n\t\t\t\t.filter((key) => this.fieldErrors[key])\n\t\t\t\t.map((key) => ({ key, message: this.fieldErrors[key], controlId: this.controlIdFor({ key }) }))\n\t\t},\n\n\t\t/** @return {boolean} Whether the public form says what \"optional\" means. */\n\t\tshowsOptionalNotice() {\n\t\t\treturn this.isBoardLook\n\t\t\t\t&& this.mode === 'public'\n\t\t\t\t&& this.showOptionalNotice\n\t\t\t\t&& this.fields.some((f) => this.isFieldRequired(f))\n\t\t},\n\n\t\t/** Whether the host's unmet conditions hold the submit. */\n\t\tsubmitBlocked() {\n\t\t\treturn this.blockSubmit === true && this.unmetConditions.length > 0\n\t\t},\n\n\t\t/** The reason shown for a held submit: the first unmet message. */\n\t\tblockedReason() {\n\t\t\treturn this.submitBlocked ? String(this.unmetConditions[0].message || '') : ''\n\t\t},\n\n\t\t/** Whether any field has changed since mount. */\n\t\tdirty() {\n\t\t\treturn JSON.stringify(this.formData) !== this.baseline\n\t\t},\n\n\t\t/**\n\t\t * Effective custom-component registry. Explicit prop wins over\n\t\t * the injected value (mirrors CnPageRenderer's resolution).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\teffectiveCustomComponents() {\n\t\t\treturn this.customComponents ?? this.cnCustomComponents ?? {}\n\t\t},\n\n\t\t/** The translator for manifest labels: the `translate` prop, else the injected `cnTranslate`, else identity. */\n\t\tlabelTranslator() {\n\t\t\tif (typeof this.translate === 'function') {\n\t\t\t\treturn this.translate\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t},\n\n\t\t/** Whether any field still carries a smart-paste suggestion mark. */\n\t\thasSuggestions() {\n\t\t\treturn Object.keys(this.suggestions).length > 0\n\t\t},\n\n\t\t/** Whether `steps` declares at least one entry. */\n\t\thasSteps() {\n\t\t\treturn Array.isArray(this.steps) && this.steps.length > 0\n\t\t},\n\n\t\t/** `config.fields[].key` → field object lookup. */\n\t\tfieldsByKey() {\n\t\t\tconst map = {}\n\t\t\tthis.fields.forEach((f) => {\n\t\t\t\tif (f && typeof f.key === 'string') {\n\t\t\t\t\tmap[f.key] = f\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn map\n\t\t},\n\n\t\t/**\n\t\t * Single-pass, declaration-order visibility cascade over `fields`\n\t\t * (REQ-MFL-9). LOCAL-mode conditions evaluate synchronously\n\t\t * against the effective (visibility-filtered) data built up as we\n\t\t * go — a hidden field reads as `undefined` for every LATER\n\t\t * field's condition. `endpoint`/`source` conditions read from the\n\t\t * `remoteVisibility` cache resolved once at mount.\n\t\t *\n\t\t * @return {object} `{ [fieldKey]: boolean }`\n\t\t */\n\t\teffectiveVisibility() {\n\t\t\tconst result = {}\n\t\t\tconst effectiveData = {}\n\t\t\tthis.fields.forEach((field) => {\n\t\t\t\tif (!field || typeof field.key !== 'string') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst cond = field.visibleWhen\n\t\t\t\tlet visible = true\n\t\t\t\tif (cond && (cond.endpoint || cond.source)) {\n\t\t\t\t\tvisible = this.remoteVisibility[field.key] === true\n\t\t\t\t} else if (cond) {\n\t\t\t\t\tvisible = evaluateVisibleWhenLocal(cond, effectiveData)\n\t\t\t\t}\n\t\t\t\tresult[field.key] = visible\n\t\t\t\teffectiveData[field.key] = visible ? this.formData[field.key] : undefined\n\t\t\t})\n\t\t\treturn result\n\t\t},\n\n\t\t/** Fields for the current step (or the full flat list when stepless). */\n\t\tcurrentStepFields() {\n\t\t\tif (this.hasSteps) {\n\t\t\t\treturn this.stepFields(this.steps[this.currentStepIndex])\n\t\t\t}\n\t\t\treturn this.fields\n\t\t},\n\n\t\t/** `currentStepFields` filtered to those currently visible. */\n\t\tvisibleCurrentStepFields() {\n\t\t\treturn this.currentStepFields.filter((f) => f && this.isFieldVisible(f.key))\n\t\t},\n\n\t\t/** Current step's optional description (stepless ⇒ ''). */\n\t\tcurrentStepDescription() {\n\t\t\tif (!this.hasSteps) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst step = this.steps[this.currentStepIndex]\n\t\t\treturn step && step.description ? step.description : ''\n\t\t},\n\n\t\t/** Indices of steps that are NOT fully hidden by conditions. */\n\t\tvisibleStepIndices() {\n\t\t\tif (!this.hasSteps) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn this.steps.map((_, i) => i).filter((i) => !this.isStepHidden(this.steps[i]))\n\t\t},\n\n\t\t/** Whether the current step is the first non-fully-hidden step. */\n\t\tisFirstStep() {\n\t\t\tif (!this.hasSteps) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tconst list = this.visibleStepIndices\n\t\t\treturn list.length === 0 || this.currentStepIndex === list[0]\n\t\t},\n\n\t\t/** Whether the current step is the last non-fully-hidden step. */\n\t\tisLastStep() {\n\t\t\tif (!this.hasSteps) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tconst list = this.visibleStepIndices\n\t\t\treturn list.length === 0 || this.currentStepIndex === list[list.length - 1]\n\t\t},\n\n\t\t/**\n\t\t * The dispatched payload (REQ-MFL-10): `formData` with every\n\t\t * hidden-by-condition declared field key removed. Keys not\n\t\t * declared in `fields` (e.g. an `id` carried by `initialValue`\n\t\t * for `mode: \"edit\"`) pass through untouched.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\teffectivePayload() {\n\t\t\tconst payload = { ...this.formData }\n\t\t\tthis.fields.forEach((field) => {\n\t\t\t\tif (field && typeof field.key === 'string' && !this.isFieldVisible(field.key)) {\n\t\t\t\t\tdelete payload[field.key]\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn payload\n\t\t},\n\n\t\t/**\n\t\t * Where this form's local draft lives, or '' when recovery is off.\n\t\t *\n\t\t * @return {string} The storage key.\n\t\t */\n\t\tformDraftKey() {\n\t\t\tif (this.recoverDraft !== true) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn draftKey({ appId: this.draftAppId, schema: this.draftScope, objectId: 'new', userId: this.draftUserId })\n\t\t},\n\n\t\t/**\n\t\t * What the draft indicator announces: Saving, then Saved with a relative time.\n\t\t *\n\t\t * @return {string} The text, or '' before anything is typed.\n\t\t */\n\t\tdraftIndicatorLabel() {\n\t\t\treturn draftIndicatorText(this.draftState, this.draftSavedAt)\n\t\t},\n\t},\n\n\twatch: {\n\t\t// Keep the local draft in step with what is typed. Only a form that differs\n\t\t// from its initial values counts as typed-in, and nothing is written while\n\t\t// an earlier draft is being offered (the user has not chosen yet).\n\t\tformData: {\n\t\t\tdeep: true,\n\t\t\thandler(values) {\n\t\t\t\tif (!this.formDraftKey || this.draftOffer !== null || !this.dirty) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.scheduleDraftWrite(this.formDraftKey, values)\n\t\t\t},\n\t\t},\n\n\t\tinitialValue: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\t// A reset: the rules may fill in again.\n\t\t\t\tthis.formData = this.initialWithDefaults()\n\t\t\t\tthis.baseline = JSON.stringify(this.formData)\n\t\t\t\tthis.handEdited = {}\n\t\t\t\tthis.assignedFrom = {}\n\t\t\t\tthis.runAssignments(null)\n\t\t\t},\n\t\t},\n\t},\n\n\tcreated() {\n\t\t// Pending calculation timers, per field (not reactive).\n\t\tthis.calcTimers = {}\n\t\t// Offered, never applied: a form that fills itself is indistinguishable\n\t\t// from one the server prefilled.\n\t\tif (this.formDraftKey) {\n\t\t\tthis.draftOffer = readDraft(this.formDraftKey)\n\t\t}\n\t},\n\n\tmounted() {\n\t\tthis.resolveRemoteVisibility()\n\t\tthis.resolveSmartPaste()\n\t\tthis.runAssignments(null)\n\t\tthis.loadProfileDefaults()\n\t},\n\n\tbeforeUnmount() {\n\t\tObject.values(this.calcTimers).forEach((timer) => clearTimeout(timer))\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * The registered smart-paste handler as `{ fill, available }`, or null.\n\t\t * A bare function is a `fill` with no availability check.\n\t\t *\n\t\t * @return {{fill: Function, available: Function|null}|null} The handler.\n\t\t */\n\t\tsmartPasteHandler() {\n\t\t\tconst name = this.smartPaste && this.smartPaste.handler\n\t\t\tconst entry = name ? this.effectiveCustomComponents[name] : null\n\t\t\tif (typeof entry === 'function') {\n\t\t\t\treturn { fill: entry, available: null }\n\t\t\t}\n\t\t\tif (entry && typeof entry.fill === 'function') {\n\t\t\t\treturn { fill: entry.fill, available: typeof entry.available === 'function' ? entry.available : null }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Decide once, at mount, whether \"Paste to fill\" shows: enabled, not a\n\t\t * public form, a handler that resolves and does not report unavailable.\n\t\t * Anything else leaves the form exactly as before.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync resolveSmartPaste() {\n\t\t\tthis.smartPasteAvailable = false\n\t\t\tif (!this.smartPaste || this.smartPaste.enabled !== true || this.mode === 'public') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst handler = this.smartPasteHandler()\n\t\t\tif (!handler) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnFormPage] smartPaste handler \"${this.smartPaste.handler}\" is not registered; hiding \"Paste to fill\".`)\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tthis.smartPasteAvailable = handler.available ? (await handler.available()) === true : true\n\t\t\t} catch {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnFormPage] smartPaste availability check failed; hiding \"Paste to fill\".')\n\t\t\t\tthis.smartPasteAvailable = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The allowed fields as the handler sees them: key, label, type and\n\t\t * allowed options. Never a value.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, type: string, options: string[]|null}>} The descriptors.\n\t\t */\n\t\tsmartPasteFields() {\n\t\t\tconst allowed = new Set(Array.isArray(this.smartPaste?.fields) ? this.smartPaste.fields : [])\n\t\t\treturn this.fields\n\t\t\t\t.filter((f) => f && allowed.has(f.key))\n\t\t\t\t.map((f) => ({\n\t\t\t\t\tkey: f.key,\n\t\t\t\t\tlabel: this.resolveLabel(f.label) || f.key,\n\t\t\t\t\ttype: f.type || 'string',\n\t\t\t\t\toptions: f.type === 'enum' ? this.enumValues(f) : null,\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The allowed values of an enum field.\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {string[]} The values.\n\t\t */\n\t\tenumValues(field) {\n\t\t\tconst raw = Array.isArray(field.enum) ? field.enum : (Array.isArray(field.options) ? field.options : [])\n\t\t\treturn raw.map((o) => (o && typeof o === 'object' ? o.value : o)).filter((v) => v !== undefined && v !== null).map(String)\n\t\t},\n\n\t\t/**\n\t\t * Coerce a proposed value to the field's type.\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @param {unknown} value The proposed value.\n\t\t * @return {{ok: boolean, value?: unknown}} The coerced value, or not ok.\n\t\t */\n\t\tcoerceProposal(field, value) {\n\t\t\tswitch (field.type || 'string') {\n\t\t\t\tcase 'number': {\n\t\t\t\t\tconst n = typeof value === 'number' ? value : (typeof value === 'string' && value.trim() !== '' ? Number(value) : NaN)\n\t\t\t\t\treturn Number.isFinite(n) ? { ok: true, value: n } : { ok: false }\n\t\t\t\t}\n\t\t\t\tcase 'boolean':\n\t\t\t\t\tif (value === true || value === 'true') {\n\t\t\t\t\t\treturn { ok: true, value: true }\n\t\t\t\t\t}\n\t\t\t\t\tif (value === false || value === 'false') {\n\t\t\t\t\t\treturn { ok: true, value: false }\n\t\t\t\t\t}\n\t\t\t\t\treturn { ok: false }\n\t\t\t\tcase 'enum':\n\t\t\t\t\treturn this.enumValues(field).includes(String(value)) ? { ok: true, value: String(value) } : { ok: false }\n\t\t\t\tcase 'string':\n\t\t\t\t\treturn typeof value === 'string' || typeof value === 'number' ? { ok: true, value: String(value) } : { ok: false }\n\t\t\t\tdefault:\n\t\t\t\t\treturn { ok: false }\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a field holds nothing the user typed.\n\t\t *\n\t\t * @param {unknown} value The field value.\n\t\t * @return {boolean} True when empty.\n\t\t */\n\t\tisEmptyField(value) {\n\t\t\treturn value === undefined || value === null || value === '' || value === false || (Array.isArray(value) && value.length === 0)\n\t\t},\n\n\t\t/**\n\t\t * Run the handler on the pasted text and land its proposals: only\n\t\t * allowed, visible fields that are empty (or all of them when\n\t\t * \"Replace what I typed\" is ticked), only values that fit the type and\n\t\t * pass the field's validation. Each landed field is marked as a\n\t\t * suggestion. Nothing is submitted.\n\t\t *\n\t\t * @param {{text: string, replace: boolean}} request The dialog's request.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onSmartPasteFill({ text, replace }) {\n\t\t\tconst handler = this.smartPasteHandler()\n\t\t\tif (!handler) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.smartPasteBusy = true\n\t\t\tthis.smartPasteError = ''\n\t\t\tlet values\n\t\t\ttry {\n\t\t\t\tconst answer = await handler.fill(text, this.smartPasteFields())\n\t\t\t\tvalues = answer && typeof answer.values === 'object' && answer.values !== null ? answer.values : {}\n\t\t\t} catch (err) {\n\t\t\t\tthis.smartPasteError = (err && err.message) || t('nextcloud-vue', 'The text could not be read.')\n\t\t\t\tthis.smartPasteBusy = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst allowed = new Set(Array.isArray(this.smartPaste?.fields) ? this.smartPaste.fields : [])\n\t\t\tlet filled = 0\n\t\t\tlet skipped = 0\n\t\t\tconst marks = { ...this.suggestions }\n\t\t\tfor (const [key, proposed] of Object.entries(values)) {\n\t\t\t\tconst field = this.fields.find((f) => f && f.key === key)\n\t\t\t\tif (!field || !allowed.has(key) || !this.isFieldVisible(key)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (!replace && !this.isEmptyField(this.formData[key])) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst coerced = this.coerceProposal(field, proposed)\n\t\t\t\tif (!coerced.ok || validateFieldValue(field, coerced.value, this.resolveLabel) !== null) {\n\t\t\t\t\tskipped += 1\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tthis.updateField(key, coerced.value)\n\t\t\t\tmarks[key] = true\n\t\t\t\tfilled += 1\n\t\t\t}\n\t\t\tthis.suggestions = { ...this.suggestions, ...marks }\n\t\t\tthis.smartPasteNotice = skipped > 0\n\t\t\t\t? t('nextcloud-vue', '{filled} fields filled, {skipped} skipped', { filled, skipped })\n\t\t\t\t: t('nextcloud-vue', '{filled} fields filled', { filled })\n\t\t\tthis.smartPasteBusy = false\n\t\t\tthis.smartPasteOpen = false\n\t\t},\n\n\t\t/**\n\t\t * Accept one suggestion (clears its mark, keeps the value).\n\t\t *\n\t\t * @param {string} key The field key.\n\t\t */\n\t\tacceptSuggestion(key) {\n\t\t\tthis.suggestions = Object.fromEntries(Object.entries(this.suggestions).filter(([k]) => k !== key))\n\t\t},\n\n\t\t/** Accept every suggestion. */\n\t\tacceptAllSuggestions() {\n\t\t\tthis.suggestions = {}\n\t\t},\n\n\t\t/**\n\t\t * Take the offered local draft into the form.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\trestoreDraft() {\n\t\t\tif (this.draftOffer === null) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.formData = { ...this.formData, ...this.draftOffer.values }\n\t\t\tthis.draftOffer = null\n\t\t},\n\n\t\t/**\n\t\t * Decline the offered local draft and forget it.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tdiscardDraft() {\n\t\t\tthis.forgetDraft(this.formDraftKey)\n\t\t},\n\n\t\tcloneInitial() {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(JSON.stringify(this.initialValue || {}))\n\t\t\t} catch {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a field must be filled in (`validation.required`, or `required`).\n\t\t *\n\t\t * @param {object} field The formField shape.\n\t\t * @return {boolean} True for a required field.\n\t\t */\n\t\tisFieldRequired(field) {\n\t\t\treturn !!(field && ((field.validation && field.validation.required) || field.required === true))\n\t\t},\n\n\t\t/**\n\t\t * Whether the board look draws this field's head (label above the control,\n\t\t * error between). A boolean keeps its inline label, and a `#field-<key>`\n\t\t * override keeps its own markup.\n\t\t *\n\t\t * @param {object} field The formField shape.\n\t\t * @return {boolean} True when the head is drawn.\n\t\t */\n\t\tshowsBoardHead(field) {\n\t\t\tif (!this.isBoardLook || this.$slots[`field-${field.key}`]) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst render = this.resolveFieldRender(field)\n\t\t\treturn !!render && render.kind !== 'boolean'\n\t\t},\n\n\t\tcontrolIdFor(field) {\n\t\t\treturn `cn-form-page__field-${field.key}`\n\t\t},\n\n\t\terrorIdFor(field) {\n\t\t\treturn `cn-form-page__field-error-${field.key}`\n\t\t},\n\n\t\thelpIdFor(field) {\n\t\t\treturn `cn-form-page__field-help-${field.key}`\n\t\t},\n\n\t\t/**\n\t\t * The bindings of a field's control: the renderer's props, plus in the\n\t\t * board look the outside label, an id for the label's `for`, and the error\n\t\t * moved above the control. For a text control, in both looks, an invalid\n\t\t * value is `aria-invalid` and `aria-describedby` lists the error before\n\t\t * the hint; `aria-required` is set in the board look.\n\t\t *\n\t\t * @param {object} field The formField shape.\n\t\t * @return {object} The props to bind.\n\t\t */\n\t\tfieldProps(field) {\n\t\t\tconst render = this.resolveFieldRender(field)\n\t\t\tconst props = { ...render.props }\n\t\t\tconst head = this.showsBoardHead(field)\n\t\t\tconst textControl = ['string', 'number', 'password', 'fallback', 'string-textarea'].includes(render.kind)\n\t\t\tconst error = this.fieldErrors[field.key]\n\t\t\tif (head) {\n\t\t\t\tprops.id = this.controlIdFor(field)\n\t\t\t\tif (render.kind === 'enum') {\n\t\t\t\t\tprops.labelOutside = true\n\t\t\t\t\tdelete props.inputLabel\n\t\t\t\t} else if (textControl) {\n\t\t\t\t\tprops.labelOutside = true\n\t\t\t\t}\n\t\t\t\tdelete props.error\n\t\t\t\tdelete props.helperText\n\t\t\t}\n\t\t\tif (textControl) {\n\t\t\t\tconst ids = []\n\t\t\t\tif (error && (head || !this.fieldHasNativeErrorSupport(field))) {\n\t\t\t\t\tids.push(this.errorIdFor(field))\n\t\t\t\t}\n\t\t\t\tif (field.help) {\n\t\t\t\t\tids.push(this.helpIdFor(field))\n\t\t\t\t}\n\t\t\t\tif (ids.length > 0) {\n\t\t\t\t\tprops['aria-describedby'] = ids.join(' ')\n\t\t\t\t}\n\t\t\t\tif (error) {\n\t\t\t\t\tprops['aria-invalid'] = 'true'\n\t\t\t\t}\n\t\t\t\tif (head && this.isFieldRequired(field)) {\n\t\t\t\t\tprops['aria-required'] = 'true'\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn props\n\t\t},\n\n\t\t/**\n\t\t * The accessibility attributes of an enum field's NcSelect. NcSelect\n\t\t * forwards no attributes to its input, so these go through\n\t\t * `v-cn-select-aria` onto the combobox input instead of `fieldProps`.\n\t\t * The error element exists for an enum in both looks (NcSelect has no\n\t\t * native error text), so an invalid value lists it before the hint.\n\t\t *\n\t\t * @param {object} field The formField shape.\n\t\t * @return {object|null} The attributes, or null for any other control.\n\t\t */\n\t\tselectAria(field) {\n\t\t\tconst render = this.resolveFieldRender(field)\n\t\t\tif (!render || render.kind !== 'enum') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst ids = []\n\t\t\tif (this.fieldErrors[field.key]) {\n\t\t\t\tids.push(this.errorIdFor(field))\n\t\t\t}\n\t\t\tif (field.help) {\n\t\t\t\tids.push(this.helpIdFor(field))\n\t\t\t}\n\t\t\treturn {\n\t\t\t\t'aria-invalid': this.fieldErrors[field.key] ? 'true' : null,\n\t\t\t\t'aria-describedby': ids.length > 0 ? ids.join(' ') : null,\n\t\t\t\t'aria-required': this.showsBoardHead(field) && this.isFieldRequired(field) ? 'true' : null,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Cancel (board look footer): go to `cancelRoute` when set, and tell the\n\t\t * host.\n\t\t */\n\t\tcancel() {\n\t\t\tif (this.cancelRoute !== '' && this.$router && typeof this.$router.push === 'function') {\n\t\t\t\tthis.$router.push(this.cancelRoute)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event cancel Emitted when the user presses Cancel in the board look.\n\t\t\t */\n\t\t\tthis.$emit('cancel')\n\t\t},\n\n\t\tresolveLabel(key) {\n\t\t\tif (!key) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.labelTranslator(key)\n\t\t},\n\n\t\t/**\n\t\t * Resolve render bindings for a field by delegating to the\n\t\t * shared `cnRenderFormField` helper. Memoised inline so the\n\t\t * template can call it once per field per render without\n\t\t * re-allocating bindings on unrelated re-renders.\n\t\t *\n\t\t * @param {object} field The formField shape to render.\n\t\t * @return {object|null}\n\t\t */\n\t\tresolveFieldRender(field) {\n\t\t\tconst rendered = cnRenderFormField({\n\t\t\t\tfield,\n\t\t\t\tvalue: this.formData[field.key],\n\t\t\t\tonInput: (next) => this.updateField(field.key, next),\n\t\t\t\tt: this.labelTranslator,\n\t\t\t\terror: this.fieldErrors[field.key] || null,\n\t\t\t})\n\t\t\t// A calculated field is the host's to set: shown, not editable.\n\t\t\tif (rendered && field.calculate && Array.isArray(field.calculate.inputs)) {\n\t\t\t\trendered.props = { ...rendered.props, readonly: true, disabled: rendered.kind === 'enum' || rendered.kind === 'boolean' }\n\t\t\t}\n\t\t\treturn rendered\n\t\t},\n\n\t\t/**\n\t\t * Whether the resolved input for `field` surfaces its error via\n\t\t * native `NcInputField`-family props (so CnFormPage should NOT\n\t\t * also render the adjacent `role=\"alert\"` paragraph). A\n\t\t * `#field-<key>` slot override always reports `false` — the\n\t\t * consumer's custom markup gets the standard fallback alert too.\n\t\t *\n\t\t * @param {object} field The formField shape to check.\n\t\t * @return {boolean}\n\t\t */\n\t\tfieldHasNativeErrorSupport(field) {\n\t\t\tif (this.$slots[`field-${field.key}`] || this.$slots[`field-${field.key}`]) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst render = this.resolveFieldRender(field)\n\t\t\tif (!render) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (['string', 'number', 'password', 'fallback'].includes(render.kind)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (render.kind === 'string-textarea') {\n\t\t\t\treturn render.tag !== 'textarea'\n\t\t\t}\n\t\t\treturn false\n\t\t},\n\n\t\t/**\n\t\t * Resolve the field objects (in step order) for a given step\n\t\t * entry, dropping any key that no longer matches a declared field.\n\t\t *\n\t\t * @param {{id: string, fields: string[]}} step The step entry.\n\t\t * @return {Array<object>}\n\t\t */\n\t\tstepFields(step) {\n\t\t\tif (!step || !Array.isArray(step.fields)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn step.fields.map((key) => this.fieldsByKey[key]).filter(Boolean)\n\t\t},\n\n\t\t/**\n\t\t * Whether every field belonging to `step` is currently hidden by\n\t\t * its `visibleWhen` condition (REQ-MFL-6: such a step is skipped\n\t\t * by Next/Back in both directions).\n\t\t *\n\t\t * @param {object} step The step entry.\n\t\t * @return {boolean}\n\t\t */\n\t\tisStepHidden(step) {\n\t\t\tconst flds = this.stepFields(step)\n\t\t\treturn flds.length > 0 && flds.every((f) => !this.isFieldVisible(f.key))\n\t\t},\n\n\t\t/**\n\t\t * Whether `key`'s field is currently visible per the\n\t\t * `effectiveVisibility` cascade.\n\t\t *\n\t\t * @param {string} key The field key.\n\t\t * @return {boolean}\n\t\t */\n\t\tisFieldVisible(key) {\n\t\t\treturn this.effectiveVisibility[key] !== false\n\t\t},\n\n\t\t/**\n\t\t * Resolve `endpoint` / `source` visibleWhen conditions once at\n\t\t * mount into `remoteVisibility` (fail-safe: any error hides the\n\t\t * field). Never re-run on formData changes — see REQ-MFL-9.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync resolveRemoteVisibility() {\n\t\t\tconst remoteFields = this.fields.filter((f) => f && f.visibleWhen && (f.visibleWhen.endpoint || f.visibleWhen.source))\n\t\t\tawait Promise.all(remoteFields.map(async (field) => {\n\t\t\t\tconst result = await evaluateVisibleWhen(field.visibleWhen, { object: this.formData })\n\t\t\t\tthis.remoteVisibility[field.key] = result\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Validate the VISIBLE fields in `fieldsList` via\n\t\t * `validateFieldValue`, populating / clearing `fieldErrors` as it\n\t\t * goes. Hidden fields are skipped entirely (REQ-MFL-10) and any\n\t\t * stale error for them is cleared.\n\t\t *\n\t\t * @param {Array<object>} fieldsList The fields to validate.\n\t\t * @return {string|null} The first invalid field's key, or `null` when all pass.\n\t\t */\n\t\tvalidateVisibleFields(fieldsList) {\n\t\t\tlet firstInvalidKey = null\n\t\t\tconst invalidKeys = []\n\t\t\tfieldsList.forEach((field) => {\n\t\t\t\tif (!field || typeof field.key !== 'string') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!this.isFieldVisible(field.key)) {\n\t\t\t\t\tdelete this.fieldErrors[field.key]\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst message = validateFieldValue(field, this.formData[field.key], this.resolveLabel)\n\t\t\t\tif (message) {\n\t\t\t\t\tthis.fieldErrors[field.key] = message\n\t\t\t\t\tinvalidKeys.push(field.key)\n\t\t\t\t\tif (!firstInvalidKey) {\n\t\t\t\t\t\tfirstInvalidKey = field.key\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tdelete this.fieldErrors[field.key]\n\t\t\t\t}\n\t\t\t})\n\t\t\tthis.summaryKeys = invalidKeys\n\t\t\treturn firstInvalidKey\n\t\t},\n\n\t\t/**\n\t\t * After a failed Next or submit: the board look moves focus to the error\n\t\t * summary, which links to every control; the Nextcloud look focuses the\n\t\t * first invalid field, as before.\n\t\t *\n\t\t * @param {string} firstInvalidKey The first invalid field's key.\n\t\t */\n\t\tfocusFirstError(firstInvalidKey) {\n\t\t\tif (!this.isBoardLook) {\n\t\t\t\tthis.focusField(firstInvalidKey)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (this.$refs.errorSummary && typeof this.$refs.errorSummary.focus === 'function') {\n\t\t\t\t\tthis.$refs.errorSummary.focus()\n\t\t\t\t} else {\n\t\t\t\t\tthis.focusField(firstInvalidKey)\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Move focus to the first invalid field's rendered input, after\n\t\t * the DOM reflects the current step / error state.\n\t\t *\n\t\t * @param {string} key The field key whose input should receive focus.\n\t\t */\n\t\tfocusField(key) {\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tconst refEntry = this.$refs[`field-${key}`]\n\t\t\t\tconst node = Array.isArray(refEntry) ? refEntry[0] : refEntry\n\t\t\t\tif (!node || typeof node.querySelector !== 'function') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst input = node.querySelector('input, textarea, select, [tabindex]')\n\t\t\t\tif (input && typeof input.focus === 'function') {\n\t\t\t\t\tinput.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Advance to the next non-fully-hidden step, in EITHER direction.\n\t\t * Returns -1 when there is none (caller treats that as a no-op).\n\t\t *\n\t\t * @param {number} fromIndex The step index to search from.\n\t\t * @param {1|-1} direction `1` for Next, `-1` for Back.\n\t\t * @return {number}\n\t\t */\n\t\tnextVisibleStepIndex(fromIndex, direction) {\n\t\t\tlet idx = fromIndex + direction\n\t\t\twhile (idx >= 0 && idx < this.steps.length) {\n\t\t\t\tif (!this.isStepHidden(this.steps[idx])) {\n\t\t\t\t\treturn idx\n\t\t\t\t}\n\t\t\t\tidx += direction\n\t\t\t}\n\t\t\treturn -1\n\t\t},\n\n\t\t/**\n\t\t * Next button handler: validates the current step's visible\n\t\t * fields before advancing (REQ-MFL-7); a failing field blocks\n\t\t * navigation and moves focus to it.\n\t\t */\n\t\tnext() {\n\t\t\tconst fieldsList = this.stepFields(this.steps[this.currentStepIndex])\n\t\t\tconst firstInvalidKey = this.validateVisibleFields(fieldsList)\n\t\t\tif (firstInvalidKey) {\n\t\t\t\tthis.focusFirstError(firstInvalidKey)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst targetIndex = this.nextVisibleStepIndex(this.currentStepIndex, 1)\n\t\t\tif (targetIndex !== -1) {\n\t\t\t\tconst from = this.currentStepIndex\n\t\t\t\tthis.currentStepIndex = targetIndex\n\t\t\t\t/**\n\t\t\t\t * @event step Emitted on step navigation (Next / Back).\n\t\t\t\t * @type {{ from: number, to: number }} Step indices.\n\t\t\t\t */\n\t\t\t\tthis.$emit('step', { from, to: targetIndex })\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Back button handler: NEVER validates (users may retreat with\n\t\t * invalid input) — REQ-MFL-7.\n\t\t */\n\t\tback() {\n\t\t\tconst targetIndex = this.nextVisibleStepIndex(this.currentStepIndex, -1)\n\t\t\tif (targetIndex !== -1) {\n\t\t\t\tconst from = this.currentStepIndex\n\t\t\t\tthis.currentStepIndex = targetIndex\n\t\t\t\t/**\n\t\t\t\t * @event step Emitted on step navigation (Next / Back).\n\t\t\t\t * @type {{ from: number, to: number }} Step indices.\n\t\t\t\t */\n\t\t\t\tthis.$emit('step', { from, to: targetIndex })\n\t\t\t}\n\t\t},\n\n\t\tupdateField(key, value) {\n\t\t\tthis.formData[key] = value\n\t\t\t// A hand edit: this field's rules stop until the form is reset.\n\t\t\tthis.handEdited[key] = true\n\t\t\tdelete this.assignedFrom[key]\n\t\t\tdelete this.fieldErrors[key]\n\t\t\t// Editing a suggested field accepts it.\n\t\t\tif (this.suggestions[key]) {\n\t\t\t\tthis.suggestions = Object.fromEntries(Object.entries(this.suggestions).filter(([k]) => k !== key))\n\t\t\t}\n\t\t\t/**\n\t\t\t * Field-level update event.\n\t\t\t *\n\t\t\t * @event input\n\t\t\t * @type {{key: string, value: unknown}}\n\t\t\t */\n\t\t\tthis.$emit('input', { key, value })\n\t\t\tthis.runAssignments([key])\n\t\t},\n\n\t\t/**\n\t\t * The initial values with the fields' defaults filled in underneath\n\t\t * (a default never replaces an initial value). Tokens resolve once, here.\n\t\t *\n\t\t * @return {object} The values the form opens with.\n\t\t */\n\t\tinitialWithDefaults() {\n\t\t\tconst initial = this.cloneInitial()\n\t\t\treturn { ...resolveFieldDefaults(this.fields, initial, { me: this.meProfile || {} }), ...initial }\n\t\t},\n\n\t\t/**\n\t\t * Fill in fields from other answers (`assign` rules). `changed` names the\n\t\t * answers that just changed; `null` is the pass at open, over empty fields.\n\t\t *\n\t\t * @param {string[]|null} changed Keys that changed.\n\t\t */\n\t\trunAssignments(changed) {\n\t\t\tconst { values, from } = computeAssignments({\n\t\t\t\tfields: this.fields,\n\t\t\t\tanswers: this.formData,\n\t\t\t\tchanged,\n\t\t\t\tedited: Object.keys(this.handEdited),\n\t\t\t\tctx: { me: this.meProfile, object: this.formData },\n\t\t\t})\n\t\t\tconst keys = Object.keys(values)\n\t\t\tfor (const key of keys) {\n\t\t\t\tthis.formData[key] = values[key]\n\t\t\t\tdelete this.fieldErrors[key]\n\t\t\t\tconst source = this.fieldsByKey[from[key]]\n\t\t\t\tthis.assignedFrom[key] = source ? this.resolveLabel(source.label || source.key) : from[key]\n\t\t\t\tthis.$emit('input', { key, value: values[key] })\n\t\t\t}\n\t\t\tif (changed === null && keys.length > 0) {\n\t\t\t\t// Filled in on open: not something the person changed.\n\t\t\t\tthis.baseline = JSON.stringify(this.formData)\n\t\t\t}\n\t\t\tthis.scheduleCalculations([...(changed || []), ...keys])\n\t\t},\n\n\t\t/**\n\t\t * Ask the host to recalculate the fields that read the changed answers,\n\t\t * once the person has stopped typing for 400 ms.\n\t\t *\n\t\t * @param {string[]} changed Keys that changed.\n\t\t */\n\t\tscheduleCalculations(changed) {\n\t\t\tif (typeof this.calculate !== 'function' || changed.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor (const field of this.fields) {\n\t\t\t\tconst inputs = field && field.calculate && Array.isArray(field.calculate.inputs) ? field.calculate.inputs : []\n\t\t\t\tif (!inputs.some((k) => changed.includes(k))) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tclearTimeout(this.calcTimers[field.key])\n\t\t\t\tthis.calcTimers[field.key] = setTimeout(() => this.runCalculation(field.key), 400)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Run the host's calculation for one field and write the answer.\n\t\t *\n\t\t * @param {string} key The field to calculate.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync runCalculation(key) {\n\t\t\tthis.calculating[key] = true\n\t\t\tdelete this.calcErrors[key]\n\t\t\ttry {\n\t\t\t\tconst value = await this.calculate(key, { ...this.formData })\n\t\t\t\tthis.formData[key] = value\n\t\t\t\tthis.$emit('input', { key, value })\n\t\t\t} catch {\n\t\t\t\tthis.calcErrors[key] = t('nextcloud-vue', 'Could not calculate')\n\t\t\t} finally {\n\t\t\t\tdelete this.calculating[key]\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fill in `@me.*` defaults the sync resolver could not: the e-mail comes\n\t\t * from the user's profile, asked for once and only when a default needs it.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadProfileDefaults() {\n\t\t\tconst needs = this.fields.filter((f) => f && typeof f.default === 'string' && /^@me\\.(email|displayName)$/.test(f.default) && (this.formData[f.key] === undefined || this.formData[f.key] === null || this.formData[f.key] === ''))\n\t\t\tif (needs.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.meProfile = await loadCurrentUserProfile()\n\t\t\tif (!this.meProfile.email && !this.meProfile.displayName) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst have = this.formData\n\t\t\tconst filled = resolveFieldDefaults(needs, have, { me: this.meProfile })\n\t\t\tfor (const [key, value] of Object.entries(filled)) {\n\t\t\t\tif (!this.handEdited[key]) {\n\t\t\t\t\tthis.formData[key] = value\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.baseline = JSON.stringify(this.formData)\n\t\t},\n\n\t\t/**\n\t\t * Dispatch the submit. When `steps` is present and the current\n\t\t * step is not the last (visible) one, a native form submit\n\t\t * (e.g. pressing Enter) is redirected to `next()` instead of\n\t\t * dispatching early. Otherwise validates ALL visible fields\n\t\t * across ALL steps (REQ-MFL-7); on failure, jumps to the\n\t\t * earliest step containing an invalid field. Picks endpoint mode\n\t\t * when `submitEndpoint` is set, otherwise handler mode. When\n\t\t * neither is set, emits `@error` with a clear message rather\n\t\t * than no-op silently.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync submit() {\n\t\t\tif (this.hasSteps && !this.isLastStep) {\n\t\t\t\tthis.next()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.submitBlocked) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst allFieldsList = this.hasSteps\n\t\t\t\t? this.steps.reduce((acc, step) => acc.concat(this.stepFields(step)), [])\n\t\t\t\t: this.fields\n\t\t\tconst firstInvalidKey = this.validateVisibleFields(allFieldsList)\n\t\t\tif (firstInvalidKey) {\n\t\t\t\tif (this.hasSteps) {\n\t\t\t\t\tconst stepIndex = this.steps.findIndex((step) => this.stepFields(step).some((f) => f.key === firstInvalidKey))\n\t\t\t\t\tif (stepIndex >= 0) {\n\t\t\t\t\t\tthis.currentStepIndex = stepIndex\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.focusFirstError(firstInvalidKey)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.lastError = null\n\t\t\tthis.submitting = true\n\t\t\ttry {\n\t\t\t\tif (this.submitEndpoint) {\n\t\t\t\t\tawait this.submitViaEndpoint()\n\t\t\t\t} else if (this.submitHandler) {\n\t\t\t\t\tawait this.submitViaHandler()\n\t\t\t\t} else {\n\t\t\t\t\tthrow new Error('CnFormPage: no submit destination configured (set submitHandler or submitEndpoint)')\n\t\t\t\t}\n\t\t\t\tthis.submitted = true\n\t\t\t\t// Once it is on the server, the local copy protects nothing.\n\t\t\t\tthis.forgetDraft(this.formDraftKey)\n\t\t\t\t/**\n\t\t\t\t * Successful submit event. Payload is the effective payload\n\t\t\t\t * (visible fields only).\n\t\t\t\t *\n\t\t\t\t * @event submit\n\t\t\t\t * @type {object}\n\t\t\t\t */\n\t\t\t\tthis.$emit('submit', this.effectivePayload)\n\t\t\t} catch (err) {\n\t\t\t\t// The server's own sentence, when it sent one. An axios message\n\t\t\t\t// describes the transport (\"Request failed with status code 400\")\n\t\t\t\t// and leaves the user to guess which field it meant.\n\t\t\t\tthis.lastError = serverErrorMessage(err)\n\t\t\t\t/**\n\t\t\t\t * Submit failure event. Payload is the thrown error / rejected reason.\n\t\t\t\t *\n\t\t\t\t * @event error\n\t\t\t\t * @type {Error}\n\t\t\t\t */\n\t\t\t\tthis.$emit('error', err)\n\t\t\t} finally {\n\t\t\t\tthis.submitting = false\n\t\t\t}\n\t\t},\n\n\t\tasync submitViaEndpoint() {\n\t\t\tconst method = (this.submitMethod || 'POST').toLowerCase()\n\t\t\tconst url = resolveParams(this.submitEndpoint, this.$route?.params || {})\n\t\t\tif (typeof axios[method] !== 'function') {\n\t\t\t\tthrow new Error(`CnFormPage: unsupported HTTP method \"${this.submitMethod}\"`)\n\t\t\t}\n\t\t\tawait axios[method](url, this.effectivePayload)\n\t\t},\n\n\t\tasync submitViaHandler() {\n\t\t\tconst handler = this.effectiveCustomComponents[this.submitHandler]\n\t\t\tif (typeof handler !== 'function') {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnFormPage] handler \"${this.submitHandler}\" not found in customComponents (or not a function). Did you register it?`)\n\t\t\t\tthrow new Error(`CnFormPage: handler \"${this.submitHandler}\" not registered`)\n\t\t\t}\n\t\t\tawait handler(this.effectivePayload, this.$route, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style>\n.cn-form-page {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 1rem;\n\tpadding: 1rem;\n\tmax-width: 720px;\n\tmargin: 0 auto;\n\tcolor: var(--color-main-text);\n}\n\n.cn-form-page__form {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 1rem;\n}\n\n.cn-form-page__steps-nav {\n\twidth: 100%;\n}\n\n.cn-form-page__steps {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 1rem;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-form-page__step {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-weight: normal;\n}\n\n.cn-form-page__step--current {\n\tcolor: var(--color-main-text);\n\tfont-weight: bold;\n}\n\n.cn-form-page__step--done {\n\tcolor: var(--color-main-text);\n}\n\n.cn-form-page__step-check {\n\tcolor: var(--color-primary-element);\n\tmargin-right: 0.25rem;\n}\n\n.cn-form-page__step-description {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n}\n\n.cn-form-page__field {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 0.25rem;\n}\n\n.cn-form-page__field-help {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n\n.cn-form-page__field-error {\n\tcolor: var(--color-error);\n\tfont-size: 0.85em;\n\tmargin: 0;\n}\n\n.cn-form-page__error {\n\tcolor: var(--color-error);\n\tbackground: var(--color-error-hover, transparent);\n\tpadding: 0.5rem 0.75rem;\n\tborder-radius: var(--border-radius);\n}\n\n.cn-form-page__success {\n\tcolor: var(--color-success-text, var(--color-main-text));\n\tbackground: var(--color-success-hover, transparent);\n\tpadding: 1rem;\n\tborder-radius: var(--border-radius);\n\ttext-align: center;\n}\n\n.cn-form-page__draft-state {\n\tmargin: 8px 0 0;\n\tmin-height: 1.2em;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n\n.cn-form-page__draft-actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-top: 8px;\n}\n\n.cn-form-page__actions {\n\tdisplay: flex;\n\tjustify-content: flex-end;\n\tgap: 0.5rem;\n}\n\n.cn-form-page__submit {\n\tdisplay: flex;\n\tjustify-content: flex-start;\n\tgap: 0.5rem;\n\tmargin-top: 0.5rem;\n}\n\n.cn-form-page__smart-paste {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-bottom: 12px;\n}\n\n.cn-form-page__smart-paste-notice {\n\tmargin: 0 0 8px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-form-page__smart-paste-notice:empty {\n\tdisplay: none;\n}\n\n.cn-form-page__suggested {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 4px;\n\tmargin-top: 4px;\n}\n\n.cn-form-page__suggested-tag {\n\tpadding: 2px 8px;\n\tborder-radius: var(--border-radius-pill, 999px);\n\tbackground: var(--color-primary-element-light);\n\tcolor: var(--color-primary-element-light-text);\n\tfont-size: 12px;\n}\n</style>\n"],"names":["_createElementBlock","_renderSlot","_openBlock","_toDisplayString","_createTextVNode","_createElementVNode","_createVNode","_createBlock","_Fragment","_renderList","_withDirectives","_resolveDynamicComponent","_mergeProps","_toHandlers","_normalizeClass"],"mappings":"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"}
1
+ {"version":3,"file":"CnFormPage.vue3.js","sources":["../../../../src/components/CnFormPage/CnFormPage.vue?vue&type=template&id=4fd51f4f&lang.js"],"sourcesContent":["<!--\n CnFormPage — Manifest-driven runtime form.\n\n Renders a flat field set + submit button declared in\n `pages[].config` for `type: \"form\"` pages. Closes the gap that\n forces every consumer's runtime-form route (public surveys, \"request\n a quote\" pages, ticket-create routes when no detail-page round-trip\n is needed) onto `type: \"custom\"`.\n\n manifest-form-logic adds three declarative capabilities on top of the\n flat field set (all additive — a manifest with no `steps` and no\n `field.visibleWhen` / `field.validation` renders byte-for-byte as\n before):\n\n - `steps` prop: ordered `{ id, title, description?, fields[] }`\n wizard groups (`fields[]` are KEY REFERENCES into the `fields`\n prop). An accessible step indicator + Back/Next/Submit footer\n renders when `steps` is non-empty; a step whose fields are ALL\n hidden by conditions is skipped in both navigation directions.\n - `field.visibleWhen`: the shared manifest visibility predicate.\n LOCAL-mode conditions (no `endpoint`/`source`) are evaluated\n SYNCHRONOUSLY against the live `formData`, in field declaration\n order (a hidden field reads as `undefined` for every later\n field's condition — cascading hides). `endpoint`/`source`\n conditions are resolved ONCE at mount (fail-safe: hidden on\n error) and never re-evaluated on keystrokes.\n - `field.validation`: `{ required, min, max, pattern, message }`,\n enforced via `validateFieldValue()` before Next/Submit advances.\n Errors render through `NcInputField`-family `error`/`helperText`\n props (via `cnRenderFormField`) or an adjacent `role=\"alert\"`\n element for widgets without native error props.\n\n DECISION (spec-fixed): a field hidden by its `visibleWhen` is\n EXCLUDED from validation AND from the dispatched payload — the\n payload always equals what the user saw and confirmed — but its\n draft value is RETAINED in component state, so toggling the\n condition back restores it.\n\n Submit dispatch picks one of two paths based on which prop is set:\n\n - `submitEndpoint` — `axios[method](url, effectivePayload)`. URL\n `:param` segments are resolved against `$route.params`.\n - `submitHandler` — looks the name up in the customComponents\n registry and calls the resolved value with\n `(effectivePayload, $route, $router)`.\n\n Field rendering is delegated to `cnRenderFormField` from\n `@conduction/nextcloud-vue/composables` so the same input set\n CnSettingsPage uses (boolean, number, string, password, enum, json)\n is available without duplication, plus the form-only `file` type\n (CnFileField, whose value is the picked file as a `data:` URL).\n `widget: \"textarea\"` overrides the default string input.\n\n Slots (mirrors CnSettingsPage):\n - `#header` — overrides CnPageHeader. Scope `{ title, description }`.\n - `#actions` — right-aligned actions area.\n - `#field-<key>` — replaces the input for a specific field.\n Scope `{ field, value, onInput, error }`.\n - `#submit` — replaces the submit button. Scope\n `{ submitting, dirty, submit }`.\n\n Events:\n - `@input` — `{ key, value }` on every field change.\n - `@step` — `{ from, to }` on step navigation.\n - `@submit` — the effective payload (visible fields only) after\n successful submit.\n - `@error` — error object after failed submit.\n\n Spec: REQ-MFPT-* (manifest-form-page-type), REQ-MFL-* (manifest-form-logic).\n-->\n<template>\n\t<div class=\"cn-form-page\" :data-mode=\"mode\" data-testid=\"cn-form-page\">\n\t\t<!--\n\t\t\t@slot header\n\t\t\t@description Replaces the default `CnPageHeader`. Receives `{ title, description }` as scoped props\n\t\t\tso a custom header can mirror the manifest-supplied labels.\n\t\t-->\n\t\t<slot\n\t\t\tname=\"header\"\n\t\t\t:title=\"title\"\n\t\t\t:description=\"description\">\n\t\t\t<CnPageHeader\n\t\t\t\tv-if=\"title\"\n\t\t\t\t:title=\"title\"\n\t\t\t\t:description=\"description\" />\n\t\t</slot>\n\n\t\t<div v-if=\"$slots.actions || $slots.actions\" class=\"cn-form-page__actions\">\n\t\t\t<!-- @slot actions Action buttons (back, cancel, …) rendered above the form. -->\n\t\t\t<slot name=\"actions\" />\n\t\t</div>\n\n\t\t<!-- Success banner -->\n\t\t<div v-if=\"submitted\" class=\"cn-form-page__success\">\n\t\t\t{{ resolveLabel(successMessage) }}\n\t\t</div>\n\n\t\t<!-- A draft this form was left in: offered, never applied. -->\n\t\t<NcNoteCard\n\t\t\tv-if=\"draftOffer !== null\"\n\t\t\ttype=\"info\"\n\t\t\tdata-testid=\"cn-form-page-draft-offer\">\n\t\t\t{{ t('nextcloud-vue', 'You have unsaved changes from an earlier visit.') }}\n\t\t\t<div class=\"cn-form-page__draft-actions\">\n\t\t\t\t<NcButton data-testid=\"cn-form-page-draft-restore\" @click=\"restoreDraft\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Restore') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton data-testid=\"cn-form-page-draft-discard\" @click=\"discardDraft\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Discard') }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t</NcNoteCard>\n\n\t\t<!-- Form body -->\n\t\t<form\n\t\t\tv-if=\"!submitted || mode !== 'public'\"\n\t\t\tclass=\"cn-form-page__form\"\n\t\t\t@submit.prevent=\"submit\">\n\t\t\t<!-- Paste to fill: only where it may be used (see smartPasteAvailable). -->\n\t\t\t<div v-if=\"smartPasteAvailable\" class=\"cn-form-page__smart-paste\">\n\t\t\t\t<NcButton type=\"button\" data-testid=\"cn-form-page-smart-paste\" @click=\"smartPasteOpen = true\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Paste to fill') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"hasSuggestions\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\tdata-testid=\"cn-form-page-accept-all\"\n\t\t\t\t\t@click=\"acceptAllSuggestions\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Accept all') }}\n\t\t\t\t</NcButton>\n\t\t\t</div>\n\t\t\t<p\n\t\t\t\tclass=\"cn-form-page__smart-paste-notice\"\n\t\t\t\taria-live=\"polite\"\n\t\t\t\tdata-testid=\"cn-form-page-smart-paste-notice\">\n\t\t\t\t{{ smartPasteNotice }}\n\t\t\t</p>\n\n\t\t\t<!-- Board look: every error of a failed submit, linked to its control. -->\n\t\t\t<CnFormErrorSummary\n\t\t\t\tv-if=\"isBoardLook && summaryItems.length > 0\"\n\t\t\t\tref=\"errorSummary\"\n\t\t\t\t:errors=\"summaryItems\"\n\t\t\t\t@focusField=\"focusField\" />\n\n\t\t\t<!-- Board look: the same stepper as CnWizardDialog. -->\n\t\t\t<CnStepper\n\t\t\t\tv-if=\"hasSteps && isBoardLook\"\n\t\t\t\t:steps=\"boardSteps\"\n\t\t\t\t:currentIndex=\"currentStepIndex\"\n\t\t\t\tlook=\"board\"\n\t\t\t\t:ariaLabel=\"t('nextcloud-vue', 'Form steps')\" />\n\n\t\t\t<!-- Step indicator — only rendered when `steps` is non-empty. -->\n\t\t\t<nav\n\t\t\t\tv-if=\"hasSteps && !isBoardLook\"\n\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Form steps')\"\n\t\t\t\tclass=\"cn-form-page__steps-nav\">\n\t\t\t\t<ol class=\"cn-form-page__steps\">\n\t\t\t\t\t<li\n\t\t\t\t\t\tv-for=\"(step, index) in steps\"\n\t\t\t\t\t\t:key=\"step.id\"\n\t\t\t\t\t\tclass=\"cn-form-page__step\"\n\t\t\t\t\t\t:class=\"{\n\t\t\t\t\t\t\t'cn-form-page__step--current': index === currentStepIndex,\n\t\t\t\t\t\t\t'cn-form-page__step--done': index < currentStepIndex,\n\t\t\t\t\t\t}\"\n\t\t\t\t\t\t:aria-current=\"index === currentStepIndex ? 'step' : null\">\n\t\t\t\t\t\t<span v-if=\"index < currentStepIndex\" class=\"cn-form-page__step-check\" aria-hidden=\"true\">✓</span>\n\t\t\t\t\t\t{{ resolveLabel(step.title) }}\n\t\t\t\t\t</li>\n\t\t\t\t</ol>\n\t\t\t</nav>\n\n\t\t\t<p v-if=\"hasSteps && currentStepDescription\" class=\"cn-form-page__step-description\">\n\t\t\t\t{{ resolveLabel(currentStepDescription) }}\n\t\t\t</p>\n\n\t\t\t<!-- A public board look form says what \"optional\" means. -->\n\t\t\t<p v-if=\"showsOptionalNotice\"\n\t\t\t\tclass=\"cn-form-page__optional-notice\"\n\t\t\t\tdata-testid=\"cn-form-page-optional-notice\">\n\t\t\t\t{{ optionalNoticeLabel }}\n\t\t\t</p>\n\n\t\t\t<div\n\t\t\t\tv-for=\"field in visibleCurrentStepFields\"\n\t\t\t\t:key=\"field.key\"\n\t\t\t\t:ref=\"`field-${field.key}`\"\n\t\t\t\tclass=\"cn-form-page__field\"\n\t\t\t\t:class=\"{\n\t\t\t\t\t'cn-form-field--half': isBoardLook && field.width === 'half',\n\t\t\t\t\t'cn-form-field--invalid': isBoardLook && !!fieldErrors[field.key],\n\t\t\t\t}\"\n\t\t\t\t:data-field-key=\"field.key\"\n\t\t\t\t:aria-describedby=\"fieldErrors[field.key] && !fieldHasNativeErrorSupport(field) ? `cn-form-page__field-error-${field.key}` : null\">\n\t\t\t\t<CnFormField\n\t\t\t\t\tv-if=\"showsBoardHead(field)\"\n\t\t\t\t\t:controlId=\"controlIdFor(field)\"\n\t\t\t\t\t:text=\"resolveLabel(field.label) || field.key\"\n\t\t\t\t\t:optional=\"!isFieldRequired(field)\"\n\t\t\t\t\t:optionalLabel=\"optionalLabel\"\n\t\t\t\t\t:error=\"fieldErrors[field.key] || ''\"\n\t\t\t\t\t:errorId=\"errorIdFor(field)\" />\n\t\t\t\t<!--\n\t\t\t\t\t@slot field-${field.key}\n\t\t\t\t\t@description Per-field override slot. Replaces the auto-rendered input for one specific field.\n\t\t\t\t\tScoped props: `{ field, value, onInput, error }` — `onInput(v)` updates the field via `updateField`.\n\t\t\t\t-->\n\t\t\t\t<slot\n\t\t\t\t\t:name=\"`field-${field.key}`\"\n\t\t\t\t\t:field=\"field\"\n\t\t\t\t\t:value=\"formData[field.key]\"\n\t\t\t\t\t:onInput=\"(v) => updateField(field.key, v)\"\n\t\t\t\t\t:error=\"fieldErrors[field.key] || null\">\n\t\t\t\t\t<component\n\t\t\t\t\t\t:is=\"resolveFieldRender(field).tag\"\n\t\t\t\t\t\tv-if=\"resolveFieldRender(field)\"\n\t\t\t\t\t\tv-cn-select-aria=\"selectAria(field)\"\n\t\t\t\t\t\tv-bind=\"fieldProps(field)\"\n\t\t\t\t\t\tv-on=\"resolveFieldRender(field).listeners\">\n\t\t\t\t\t\t<!-- NcCheckboxRadioSwitch puts its label in the slot -->\n\t\t\t\t\t\t<template\n\t\t\t\t\t\t\tv-if=\"resolveFieldRender(field).kind === 'boolean'\">\n\t\t\t\t\t\t\t{{ resolveFieldRender(field).labelText }}\n\t\t\t\t\t\t</template>\n\t\t\t\t\t</component>\n\t\t\t\t</slot>\n\t\t\t\t<p\n\t\t\t\t\tv-if=\"fieldErrors[field.key] && !fieldHasNativeErrorSupport(field) && !showsBoardHead(field)\"\n\t\t\t\t\t:id=\"`cn-form-page__field-error-${field.key}`\"\n\t\t\t\t\tclass=\"cn-form-page__field-error\"\n\t\t\t\t\trole=\"alert\">\n\t\t\t\t\t{{ fieldErrors[field.key] }}\n\t\t\t\t</p>\n\t\t\t\t<span v-if=\"suggestions[field.key]\" class=\"cn-form-page__suggested\" data-testid=\"cn-form-page-suggested\">\n\t\t\t\t\t<span class=\"cn-form-page__suggested-tag\">{{ t('nextcloud-vue', 'Suggested') }}</span>\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t\tvariant=\"tertiary\"\n\t\t\t\t\t\t:aria-label=\"t('nextcloud-vue', 'Accept suggestion for {field}', { field: resolveLabel(field.label) || field.key })\"\n\t\t\t\t\t\t@click=\"acceptSuggestion(field.key)\">\n\t\t\t\t\t\t{{ t('nextcloud-vue', 'Accept') }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</span>\n\t\t\t\t<small\n\t\t\t\t\tv-if=\"field.help\"\n\t\t\t\t\t:id=\"helpIdFor(field)\"\n\t\t\t\t\tclass=\"cn-form-page__field-help\">\n\t\t\t\t\t{{ resolveLabel(field.help) }}\n\t\t\t\t</small>\n\t\t\t\t<small\n\t\t\t\t\tv-if=\"assignedFrom[field.key]\"\n\t\t\t\t\tclass=\"cn-form-page__field-assigned\"\n\t\t\t\t\taria-live=\"polite\"\n\t\t\t\t\tdata-testid=\"cn-form-page-assigned\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Filled in from {field}', { field: assignedFrom[field.key] }) }}\n\t\t\t\t</small>\n\t\t\t\t<small\n\t\t\t\t\tv-if=\"calculating[field.key]\"\n\t\t\t\t\tclass=\"cn-form-page__field-calculating\"\n\t\t\t\t\trole=\"status\"\n\t\t\t\t\tdata-testid=\"cn-form-page-calculating\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Calculating') }}\n\t\t\t\t</small>\n\t\t\t\t<small\n\t\t\t\t\tv-if=\"calcErrors[field.key]\"\n\t\t\t\t\tclass=\"cn-form-page__field-error\"\n\t\t\t\t\trole=\"alert\"\n\t\t\t\t\tdata-testid=\"cn-form-page-calc-error\">\n\t\t\t\t\t{{ calcErrors[field.key] }}\n\t\t\t\t</small>\n\t\t\t</div>\n\n\t\t\t<!-- Error -->\n\t\t\t<p v-if=\"lastError\" class=\"cn-form-page__error\">\n\t\t\t\t{{ lastError }}\n\t\t\t</p>\n\n\t\t\t<!-- Conditions the host reports as unmet; with blockSubmit they hold the submit. -->\n\t\t\t<NcNoteCard\n\t\t\t\tv-if=\"unmetConditions.length > 0\"\n\t\t\t\ttype=\"warning\"\n\t\t\t\tclass=\"cn-form-page__unmet\"\n\t\t\t\tdata-testid=\"cn-form-page-unmet\">\n\t\t\t\t<ul id=\"cn-form-page-unmet-list\">\n\t\t\t\t\t<li v-for=\"(condition, index) in unmetConditions\" :key=\"index\">\n\t\t\t\t\t\t{{ condition.message }}\n\t\t\t\t\t</li>\n\t\t\t\t</ul>\n\t\t\t</NcNoteCard>\n\n\t\t\t<div class=\"cn-form-page__submit\" :class=\"{ 'cn-form-page__footer': isBoardLook }\">\n\t\t\t\t<!-- Board look: Cancel on the first step, at the left edge of the card. -->\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"isBoardLook && canCancel && (!hasSteps || isFirstStep)\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tdata-testid=\"cn-form-page-cancel\"\n\t\t\t\t\t@click=\"cancel\">\n\t\t\t\t\t{{ t('nextcloud-vue', 'Cancel') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"hasSteps && !isFirstStep\"\n\t\t\t\t\tvariant=\"secondary\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\tdata-testid=\"cn-form-page-back\"\n\t\t\t\t\t@click=\"back\">\n\t\t\t\t\t<template v-if=\"isBoardLook\" #icon>\n\t\t\t\t\t\t<ChevronLeft :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ isBoardLook ? t('nextcloud-vue', 'Previous') : t('nextcloud-vue', 'Back') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<NcButton\n\t\t\t\t\tv-if=\"hasSteps && !isLastStep\"\n\t\t\t\t\tclass=\"cn-form-page__primary\"\n\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\ttype=\"button\"\n\t\t\t\t\t:alignment=\"isBoardLook ? 'center-reverse' : 'center'\"\n\t\t\t\t\tdata-testid=\"cn-form-page-next\"\n\t\t\t\t\t@click=\"next\">\n\t\t\t\t\t<template v-if=\"isBoardLook\" #icon>\n\t\t\t\t\t\t<ChevronRight :size=\"20\" />\n\t\t\t\t\t</template>\n\t\t\t\t\t{{ t('nextcloud-vue', 'Next') }}\n\t\t\t\t</NcButton>\n\t\t\t\t<!-- @slot submit Replaces the default submit button. -->\n\t\t\t\t<!-- @binding {boolean} submitting Whether a submit is in flight. -->\n\t\t\t\t<!-- @binding {boolean} dirty Whether the form has unsaved changes. -->\n\t\t\t\t<!-- @binding {Function} submit Call to submit the form programmatically. -->\n\t\t\t\t<slot\n\t\t\t\t\tv-if=\"!hasSteps || isLastStep\"\n\t\t\t\t\tname=\"submit\"\n\t\t\t\t\t:submitting=\"submitting\"\n\t\t\t\t\t:dirty=\"dirty\"\n\t\t\t\t\t:submit=\"submit\">\n\t\t\t\t\t<!--\n\t\t\t\t\t\t`type`, not `native-type` — @nextcloud/vue 9's NcButton declares\n\t\t\t\t\t\tits native HTML button-type prop as `type` (`ButtonType = 'submit'\n\t\t\t\t\t\t| 'reset' | 'button'`, default `'button'`) and has no `nativeType`\n\t\t\t\t\t\tprop at all. `native-type` therefore fell through as an inert\n\t\t\t\t\t\tattribute and every button silently rendered as the component's\n\t\t\t\t\t\tdefault `type=\"button\"` — including THIS one. A `type=\"button\"`\n\t\t\t\t\t\tbutton inside a `<form>` does NOT fire the form's native `submit`\n\t\t\t\t\t\tevent on click, so `<form @submit.prevent=\"submit\">` above never\n\t\t\t\t\t\tran: clicking \"Submit\" did nothing, silently, in a real browser.\n\t\t\t\t\t\tJest's own local NcButton stub (`tests/components/CnFormPage.spec.js`)\n\t\t\t\t\t\tnever bound `:type` either, so its plain `<button>` defaulted to\n\t\t\t\t\t\tthe OPPOSITE — the HTML spec's implicit `type=\"submit\"` for a\n\t\t\t\t\t\ttype-less button in a form — which is why every jest submit test\n\t\t\t\t\t\tpassed while the real component was broken.\n\t\t\t\t\t-->\n\t\t\t\t\t<NcButton\n\t\t\t\t\t\tclass=\"cn-form-page__primary\"\n\t\t\t\t\t\tvariant=\"primary\"\n\t\t\t\t\t\ttype=\"submit\"\n\t\t\t\t\t\t:disabled=\"submitting || submitBlocked\"\n\t\t\t\t\t\t:title=\"blockedReason || null\"\n\t\t\t\t\t\t:aria-describedby=\"submitBlocked ? 'cn-form-page-unmet-list' : null\">\n\t\t\t\t\t\t<template #icon>\n\t\t\t\t\t\t\t<NcLoadingIcon v-if=\"submitting\" :size=\"20\" />\n\t\t\t\t\t\t\t<Send v-else :size=\"20\" />\n\t\t\t\t\t\t</template>\n\t\t\t\t\t\t{{ resolveLabel(submitLabel) }}\n\t\t\t\t\t</NcButton>\n\t\t\t\t</slot>\n\t\t\t</div>\n\t\t\t<!-- Local draft indicator: Saving, then Saved; one polite announcement per change. -->\n\t\t\t<p\n\t\t\t\tv-if=\"recoverDraft\"\n\t\t\t\tclass=\"cn-form-page__draft-state\"\n\t\t\t\taria-live=\"polite\"\n\t\t\t\tdata-testid=\"cn-form-page-draft-state\">\n\t\t\t\t{{ draftIndicatorLabel }}\n\t\t\t</p>\n\t\t</form>\n\t\t<CnSmartPasteDialog\n\t\t\tv-if=\"smartPasteOpen\"\n\t\t\t:hint=\"smartPaste && smartPaste.hint ? resolveLabel(smartPaste.hint) : ''\"\n\t\t\t:busy=\"smartPasteBusy\"\n\t\t\t:error=\"smartPasteError\"\n\t\t\t@fill=\"onSmartPasteFill\"\n\t\t\t@close=\"smartPasteOpen = false\" />\n\t</div>\n</template>\n\n<script>\nimport axios from '@nextcloud/axios'\nimport { translate as t } from '@nextcloud/l10n'\nimport { NcButton, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'\nimport ChevronLeft from 'vue-material-design-icons/ChevronLeft.vue'\nimport ChevronRight from 'vue-material-design-icons/ChevronRight.vue'\nimport Send from 'vue-material-design-icons/Send.vue'\nimport CnSmartPasteDialog from '../../dialogs/CnSmartPasteDialog.vue'\nimport CnFormErrorSummary from '../CnFormErrorSummary/CnFormErrorSummary.vue'\nimport CnFormField from '../CnFormField/CnFormField.vue'\nimport CnStepper from '../CnStepper/CnStepper.vue'\nimport { cnRenderFormField } from '../../composables/cnFormFieldRenderer.js'\nimport { draftIndicatorText, draftKey, formDraftMixin, readDraft } from '../../composables/useFormDraft.js'\nimport { normalizeLook } from '../../composables/useLook.js'\nimport { cnSelectAria } from '../../directives/cnSelectAria.js'\nimport { loadCurrentUserProfile } from '../../utils/currentUserProfile.js'\nimport { computeAssignments, resolveFieldDefaults } from '../../utils/formAssign.js'\nimport { validateFieldValue } from '../../utils/formValidation.js'\nimport { serverErrorMessage } from '../../utils/serverErrorMessage.js'\nimport { evaluateVisibleWhen, evaluateVisibleWhenLocal } from '../../utils/visibleWhen.js'\nimport { CnPageHeader } from '../CnPageHeader/index.js'\n\nconst ALLOWED_METHODS = ['POST', 'PUT', 'PATCH']\n\n/**\n * Resolve `:param` segments in a URL string against `$route.params`.\n * Mirrors the `:id` substitution Vue Router performs on its own\n * routes; reused here because the manifest declares the URL as a\n * static string and we want the runtime to fill the slot.\n *\n * @param {string} url URL template, e.g. `/api/survey/:token`.\n * @param {object} params $route.params.\n * @return {string}\n */\nfunction resolveParams(url, params) {\n\tif (!url || !params) {\n\t\treturn url\n\t}\n\treturn String(url).replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, (match, name) => {\n\t\tconst value = params[name]\n\t\treturn value === undefined || value === null ? match : encodeURIComponent(String(value))\n\t})\n}\n\n/**\n * @event submit Fired after a successful submit. Payload: the effective payload (visible fields only; hidden-by-condition field keys excluded).\n * @event error Fired when submit fails. Payload: the thrown error / rejected reason.\n * @event input Fired on every field-level update; payload is `{ key, value }`.\n * @event step Fired on step navigation (Next/Back); payload is `{ from, to }` (step indices).\n *\n * @slot header Replaces the default `CnPageHeader`. Scoped props: `{ title, description }`.\n * @slot actions Optional slot for action buttons rendered above the form. Hidden when empty.\n * @slot field-${field.key} Per-field override slot. Replaces the auto-rendered input for one specific field. Scoped props: `{ field, value, onInput, error }`.\n * @slot submit Replaces the default submit button. Scoped props: `{ submitting, dirty, submit }`.\n */\nexport default {\n\tname: 'CnFormPage',\n\n\tdirectives: { cnSelectAria },\n\n\tcomponents: {\n\t\tCnFormErrorSummary,\n\t\tCnFormField,\n\t\tChevronLeft,\n\t\tChevronRight,\n\t\tCnPageHeader,\n\t\tCnSmartPasteDialog,\n\t\tCnStepper,\n\t\tNcButton,\n\t\tNcLoadingIcon,\n\t\tNcNoteCard,\n\t\tSend,\n\t},\n\n\tmixins: [formDraftMixin()],\n\n\tinject: {\n\t\t/**\n\t\t * Custom-component registry from CnAppRoot. Used to resolve the\n\t\t * `submitHandler` name to a concrete function. Defaults to an\n\t\t * empty object when the page is mounted standalone.\n\t\t *\n\t\t * @type {object}\n\t\t */\n\t\tcnCustomComponents: { default: () => ({}) },\n\n\t\t/**\n\t\t * The label lookup from CnAppRoot (the manifest's own translations, then\n\t\t * the host's translate). Used when no `translate` prop is given, so a\n\t\t * manifest form page speaks the user's language.\n\t\t *\n\t\t * @type {Function|null}\n\t\t */\n\t\tcnTranslate: { default: null },\n\n\t\t/** The app's look (a page with `config.look` re-provides its own). */\n\t\tcnLook: { default: 'nextcloud' },\n\t},\n\n\tprops: {\n\t\t/**\n\t\t * Where Cancel goes (board look). With a route, Cancel navigates there and\n\t\t * emits `cancel`; with only a `cancel` listener it just emits. With neither\n\t\t * the board footer has no Cancel.\n\t\t */\n\t\tcancelRoute: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** The word shown as \"(optional)\" after an optional field's label in the board look. */\n\t\toptionalLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'optional'),\n\t\t},\n\n\t\t/** The sentence a public board look form shows above its first field. */\n\t\toptionalNoticeLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'A field without (optional) must be filled in.'),\n\t\t},\n\n\t\t/** Show the optional-fields sentence on a public board look form. Set false to hide it. */\n\t\tshowOptionalNotice: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: true,\n\t\t},\n\n\t\t/**\n\t\t * Keep what the user typed in the browser and offer it back when the\n\t\t * form reopens (local only; nothing reaches the server). Off by default:\n\t\t * a public form can run on a shared kiosk, so a host opts in.\n\t\t */\n\t\trecoverDraft: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** The app the draft belongs to; part of the draft's storage key. */\n\t\tdraftAppId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Who is typing; part of the draft's storage key so a shared browser profile never leaks a draft between users. */\n\t\tdraftUserId: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Names this form in the draft's storage key (for example the form id), so two forms never share a draft. */\n\t\tdraftScope: {\n\t\t\ttype: String,\n\t\t\tdefault: 'form',\n\t\t},\n\n\t\t/**\n\t\t * Host function that calculates a field: `(fieldKey, answers) => Promise<value>`.\n\t\t * Called for a field declaring `calculate.inputs` when one of those answers\n\t\t * changes, after 400 ms of quiet; the field is read-only. A rejection keeps\n\t\t * the last value and says it could not be calculated. The library knows no\n\t\t * rule language: calculation belongs to the host.\n\t\t *\n\t\t * @type {((fieldKey: string, answers: object) => Promise<unknown>)|null}\n\t\t */\n\t\tcalculate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Conditions the host reports as unmet, shown above the submit button.\n\t\t *\n\t\t * @type {Array<{message: string}>}\n\t\t */\n\t\tunmetConditions: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/** Disable submit while `unmetConditions` is not empty; the first message is the reason. */\n\t\tblockSubmit: {\n\t\t\ttype: Boolean,\n\t\t\tdefault: false,\n\t\t},\n\n\t\t/** Form fields. Each MUST conform to the `formField` $def. */\n\t\tfields: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Multi-step wizard groups: `Array<{id, title, description?,\n\t\t * fields: string[]}>`. `fields[]` entries are KEY REFERENCES into\n\t\t * the `fields` prop (single source of truth — no field\n\t\t * duplication). Empty (the default) renders today's single-step\n\t\t * form unchanged: no step indicator, no Next/Back.\n\t\t */\n\t\tsteps: {\n\t\t\ttype: Array,\n\t\t\tdefault: () => [],\n\t\t},\n\n\t\t/**\n\t\t * Registered submit handler name. Resolves against the\n\t\t * `cnCustomComponents` registry (or `customComponents` prop).\n\t\t * Mutually exclusive with `submitEndpoint` at the validator\n\t\t * level; the component itself prefers `submitEndpoint` when both\n\t\t * are set so a stale manifest doesn't crash.\n\t\t */\n\t\tsubmitHandler: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * URL the form data is dispatched to. `:paramName` segments are\n\t\t * resolved against `$route.params` at submit time.\n\t\t */\n\t\tsubmitEndpoint: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** HTTP method for endpoint mode. POST | PUT | PATCH. */\n\t\tsubmitMethod: {\n\t\t\ttype: String,\n\t\t\tdefault: 'POST',\n\t\t\tvalidator: (v) => typeof v === 'string' && ALLOWED_METHODS.includes(v.toUpperCase()),\n\t\t},\n\n\t\t/**\n\t\t * Form mode. `public` shows the success banner and hides the\n\t\t * form on submit; `edit` and `create` keep the form mounted so\n\t\t * the consumer can route away.\n\t\t */\n\t\tmode: {\n\t\t\ttype: String,\n\t\t\tdefault: 'public',\n\t\t\tvalidator: (v) => ['edit', 'create', 'public'].includes(v),\n\t\t},\n\n\t\t/**\n\t\t * \"Fill from pasted text\": `{ enabled, fields, hint?, handler }`. The\n\t\t * handler is a registry entry (a function, or `{ fill, available }`)\n\t\t * that proposes values for the allowed `fields`. Never shown in\n\t\t * `public` mode, when the handler is missing, or when it reports\n\t\t * unavailable.\n\t\t *\n\t\t * @type {{enabled: boolean, fields: string[], hint?: string, handler: string}|null}\n\t\t */\n\t\tsmartPaste: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** i18n key for the submit button label. */\n\t\tsubmitLabel: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Submit'),\n\t\t},\n\n\t\t/** i18n key for the success banner. */\n\t\tsuccessMessage: {\n\t\t\ttype: String,\n\t\t\tdefault: () => t('nextcloud-vue', 'Thank you!'),\n\t\t},\n\n\t\t/** Pre-filled form state. Consumed by `mode: \"edit\"`. */\n\t\tinitialValue: {\n\t\t\ttype: Object,\n\t\t\tdefault: () => ({}),\n\t\t},\n\n\t\t/** Page title. Forwarded to CnPageHeader. */\n\t\ttitle: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** Page description. Forwarded to CnPageHeader. */\n\t\tdescription: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Optional translation function. When provided, applied to\n\t\t * field labels, success messages, `validation.message`, etc.\n\t\t * Defaults to identity.\n\t\t *\n\t\t * @type {((key: string) => string)|null}\n\t\t */\n\t\ttranslate: {\n\t\t\ttype: Function,\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/**\n\t\t * Optional explicit custom-component registry. When set, takes\n\t\t * precedence over the injected `cnCustomComponents`. Mirrors\n\t\t * the resolution order in CnPageRenderer / CnSettingsPage.\n\t\t *\n\t\t * @type {object|null}\n\t\t */\n\t\tcustomComponents: {\n\t\t\ttype: Object,\n\t\t\tdefault: null,\n\t\t},\n\t},\n\n\t/**\n\t * Events:\n\t *\n\t * @event submit\n\t * @description Fired after a successful submit (handler returned, or endpoint POST/PUT/PATCH succeeded).\n\t * Payload is the effective payload — `formData` with hidden-by-condition field keys removed.\n\t *\n\t * @event error\n\t * @description Fired when submit fails (handler threw, or endpoint returned a non-2xx response).\n\t * Payload is the thrown error / rejected reason.\n\t *\n\t * @event input\n\t * @description Fired on every field-level update; payload is `{ key, value }`.\n\t *\n\t * @event step\n\t * @description Fired on Next/Back step navigation; payload is `{ from, to }` (step indices).\n\t */\n\temits: ['submit', 'error', 'input', 'step', 'cancel'],\n\n\tdata() {\n\t\treturn {\n\t\t\tformData: this.initialWithDefaults(),\n\t\t\t/** The values the form opened with (initial values plus defaults), to tell a changed form from a fresh one. */\n\t\t\tbaseline: JSON.stringify(this.initialWithDefaults()),\n\t\t\t/** Fields the user edited by hand: their `assign` rules stop until reset. */\n\t\t\thandEdited: {},\n\t\t\t/** Field key -> label of the answer a rule filled it in from. */\n\t\t\tassignedFrom: {},\n\t\t\t/** Field keys being calculated now. */\n\t\t\tcalculating: {},\n\t\t\t/** Field key -> sentence when the last calculation failed. */\n\t\t\tcalcErrors: {},\n\t\t\t/** The signed-in user's profile (`displayName`, `email`) for `@me.*` defaults. */\n\t\t\tmeProfile: {},\n\t\t\t/** Smart paste: the handler said it is available (resolved at mount). */\n\t\t\tsmartPasteAvailable: false,\n\t\t\tsmartPasteOpen: false,\n\t\t\tsmartPasteBusy: false,\n\t\t\tsmartPasteError: '',\n\t\t\tsmartPasteNotice: '',\n\t\t\t/** Keys of fields filled by smart paste and not yet accepted or edited. */\n\t\t\tsuggestions: {},\n\t\t\tsubmitting: false,\n\t\t\tsubmitted: false,\n\t\t\tlastError: null,\n\t\t\t/** Current wizard step index (unused when `steps` is empty). */\n\t\t\tcurrentStepIndex: 0,\n\t\t\t/** Keys of the fields the last failed validation flagged, in form order (the error summary). */\n\t\t\tsummaryKeys: [],\n\t\t\t/** Per-field validation error messages, keyed by field.key. */\n\t\t\tfieldErrors: {},\n\t\t\t/**\n\t\t\t * Cache of resolved `endpoint`/`source` visibleWhen outcomes,\n\t\t\t * keyed by field.key. Resolved ONCE in `mounted()` — see the\n\t\t\t * module docblock. `undefined` (not yet resolved) reads as\n\t\t\t * hidden, mirroring CnBannerWidget's fail-safe/pending posture.\n\t\t\t */\n\t\t\tremoteVisibility: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/** @return {Array<{id: string, label: string}>} The steps in the stepper's shape. */\n\t\tboardSteps() {\n\t\t\treturn this.steps.map((step) => ({ id: step.id, label: this.resolveLabel(step.title) }))\n\t\t},\n\n\t\t/** @return {boolean} Whether the page has somewhere to cancel to. */\n\t\tcanCancel() {\n\t\t\tconst listener = this.$.vnode && this.$.vnode.props && this.$.vnode.props.onCancel\n\t\t\treturn this.cancelRoute !== '' || !!listener\n\t\t},\n\n\t\t/** @return {boolean} Whether the board look is active. */\n\t\tisBoardLook() {\n\t\t\treturn normalizeLook(this.cnLook) === 'board'\n\t\t},\n\n\t\t/** @return {Array<{key: string, message: string, controlId: string}>} The errors the summary lists. */\n\t\tsummaryItems() {\n\t\t\treturn this.summaryKeys\n\t\t\t\t.filter((key) => this.fieldErrors[key])\n\t\t\t\t.map((key) => ({ key, message: this.fieldErrors[key], controlId: this.controlIdFor({ key }) }))\n\t\t},\n\n\t\t/** @return {boolean} Whether the public form says what \"optional\" means. */\n\t\tshowsOptionalNotice() {\n\t\t\treturn this.isBoardLook\n\t\t\t\t&& this.mode === 'public'\n\t\t\t\t&& this.showOptionalNotice\n\t\t\t\t&& this.fields.some((f) => this.isFieldRequired(f))\n\t\t},\n\n\t\t/** Whether the host's unmet conditions hold the submit. */\n\t\tsubmitBlocked() {\n\t\t\treturn this.blockSubmit === true && this.unmetConditions.length > 0\n\t\t},\n\n\t\t/** The reason shown for a held submit: the first unmet message. */\n\t\tblockedReason() {\n\t\t\treturn this.submitBlocked ? String(this.unmetConditions[0].message || '') : ''\n\t\t},\n\n\t\t/** Whether any field has changed since mount. */\n\t\tdirty() {\n\t\t\treturn JSON.stringify(this.formData) !== this.baseline\n\t\t},\n\n\t\t/**\n\t\t * Effective custom-component registry. Explicit prop wins over\n\t\t * the injected value (mirrors CnPageRenderer's resolution).\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\teffectiveCustomComponents() {\n\t\t\treturn this.customComponents ?? this.cnCustomComponents ?? {}\n\t\t},\n\n\t\t/** The translator for manifest labels: the `translate` prop, else the injected `cnTranslate`, else identity. */\n\t\tlabelTranslator() {\n\t\t\tif (typeof this.translate === 'function') {\n\t\t\t\treturn this.translate\n\t\t\t}\n\t\t\treturn typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k\n\t\t},\n\n\t\t/** Whether any field still carries a smart-paste suggestion mark. */\n\t\thasSuggestions() {\n\t\t\treturn Object.keys(this.suggestions).length > 0\n\t\t},\n\n\t\t/** Whether `steps` declares at least one entry. */\n\t\thasSteps() {\n\t\t\treturn Array.isArray(this.steps) && this.steps.length > 0\n\t\t},\n\n\t\t/** `config.fields[].key` → field object lookup. */\n\t\tfieldsByKey() {\n\t\t\tconst map = {}\n\t\t\tthis.fields.forEach((f) => {\n\t\t\t\tif (f && typeof f.key === 'string') {\n\t\t\t\t\tmap[f.key] = f\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn map\n\t\t},\n\n\t\t/**\n\t\t * Single-pass, declaration-order visibility cascade over `fields`\n\t\t * (REQ-MFL-9). LOCAL-mode conditions evaluate synchronously\n\t\t * against the effective (visibility-filtered) data built up as we\n\t\t * go — a hidden field reads as `undefined` for every LATER\n\t\t * field's condition. `endpoint`/`source` conditions read from the\n\t\t * `remoteVisibility` cache resolved once at mount.\n\t\t *\n\t\t * @return {object} `{ [fieldKey]: boolean }`\n\t\t */\n\t\teffectiveVisibility() {\n\t\t\tconst result = {}\n\t\t\tconst effectiveData = {}\n\t\t\tthis.fields.forEach((field) => {\n\t\t\t\tif (!field || typeof field.key !== 'string') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst cond = field.visibleWhen\n\t\t\t\tlet visible = true\n\t\t\t\tif (cond && (cond.endpoint || cond.source)) {\n\t\t\t\t\tvisible = this.remoteVisibility[field.key] === true\n\t\t\t\t} else if (cond) {\n\t\t\t\t\tvisible = evaluateVisibleWhenLocal(cond, effectiveData)\n\t\t\t\t}\n\t\t\t\tresult[field.key] = visible\n\t\t\t\teffectiveData[field.key] = visible ? this.formData[field.key] : undefined\n\t\t\t})\n\t\t\treturn result\n\t\t},\n\n\t\t/** Fields for the current step (or the full flat list when stepless). */\n\t\tcurrentStepFields() {\n\t\t\tif (this.hasSteps) {\n\t\t\t\treturn this.stepFields(this.steps[this.currentStepIndex])\n\t\t\t}\n\t\t\treturn this.fields\n\t\t},\n\n\t\t/** `currentStepFields` filtered to those currently visible. */\n\t\tvisibleCurrentStepFields() {\n\t\t\treturn this.currentStepFields.filter((f) => f && this.isFieldVisible(f.key))\n\t\t},\n\n\t\t/** Current step's optional description (stepless ⇒ ''). */\n\t\tcurrentStepDescription() {\n\t\t\tif (!this.hasSteps) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst step = this.steps[this.currentStepIndex]\n\t\t\treturn step && step.description ? step.description : ''\n\t\t},\n\n\t\t/** Indices of steps that are NOT fully hidden by conditions. */\n\t\tvisibleStepIndices() {\n\t\t\tif (!this.hasSteps) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn this.steps.map((_, i) => i).filter((i) => !this.isStepHidden(this.steps[i]))\n\t\t},\n\n\t\t/** Whether the current step is the first non-fully-hidden step. */\n\t\tisFirstStep() {\n\t\t\tif (!this.hasSteps) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tconst list = this.visibleStepIndices\n\t\t\treturn list.length === 0 || this.currentStepIndex === list[0]\n\t\t},\n\n\t\t/** Whether the current step is the last non-fully-hidden step. */\n\t\tisLastStep() {\n\t\t\tif (!this.hasSteps) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tconst list = this.visibleStepIndices\n\t\t\treturn list.length === 0 || this.currentStepIndex === list[list.length - 1]\n\t\t},\n\n\t\t/**\n\t\t * The dispatched payload (REQ-MFL-10): `formData` with every\n\t\t * hidden-by-condition declared field key removed. Keys not\n\t\t * declared in `fields` (e.g. an `id` carried by `initialValue`\n\t\t * for `mode: \"edit\"`) pass through untouched.\n\t\t *\n\t\t * @return {object}\n\t\t */\n\t\teffectivePayload() {\n\t\t\tconst payload = { ...this.formData }\n\t\t\tthis.fields.forEach((field) => {\n\t\t\t\tif (field && typeof field.key === 'string' && !this.isFieldVisible(field.key)) {\n\t\t\t\t\tdelete payload[field.key]\n\t\t\t\t}\n\t\t\t})\n\t\t\treturn payload\n\t\t},\n\n\t\t/**\n\t\t * Where this form's local draft lives, or '' when recovery is off.\n\t\t *\n\t\t * @return {string} The storage key.\n\t\t */\n\t\tformDraftKey() {\n\t\t\tif (this.recoverDraft !== true) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn draftKey({ appId: this.draftAppId, schema: this.draftScope, objectId: 'new', userId: this.draftUserId })\n\t\t},\n\n\t\t/**\n\t\t * What the draft indicator announces: Saving, then Saved with a relative time.\n\t\t * After a failed submit it says the draft is kept on this device instead,\n\t\t * so it cannot be read as the server save that just failed.\n\t\t *\n\t\t * @return {string} The text, or '' before anything is typed.\n\t\t * @spec openspec/changes/cell-labels-and-draft-indicator/specs/cell-labels-and-draft-indicator/spec.md#requirement-the-draft-indicator-never-reads-as-a-server-save-after-a-failed-save\n\t\t */\n\t\tdraftIndicatorLabel() {\n\t\t\treturn draftIndicatorText(this.draftState, this.draftSavedAt, Date.now(), { saveFailed: Boolean(this.lastError) })\n\t\t},\n\t},\n\n\twatch: {\n\t\t// Keep the local draft in step with what is typed. Only a form that differs\n\t\t// from its initial values counts as typed-in, and nothing is written while\n\t\t// an earlier draft is being offered (the user has not chosen yet).\n\t\tformData: {\n\t\t\tdeep: true,\n\t\t\thandler(values) {\n\t\t\t\tif (!this.formDraftKey || this.draftOffer !== null || !this.dirty) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tthis.scheduleDraftWrite(this.formDraftKey, values)\n\t\t\t},\n\t\t},\n\n\t\tinitialValue: {\n\t\t\tdeep: true,\n\t\t\thandler() {\n\t\t\t\t// A reset: the rules may fill in again.\n\t\t\t\tthis.formData = this.initialWithDefaults()\n\t\t\t\tthis.baseline = JSON.stringify(this.formData)\n\t\t\t\tthis.handEdited = {}\n\t\t\t\tthis.assignedFrom = {}\n\t\t\t\tthis.runAssignments(null)\n\t\t\t},\n\t\t},\n\t},\n\n\tcreated() {\n\t\t// Pending calculation timers, per field (not reactive).\n\t\tthis.calcTimers = {}\n\t\t// Offered, never applied: a form that fills itself is indistinguishable\n\t\t// from one the server prefilled.\n\t\tif (this.formDraftKey) {\n\t\t\tthis.draftOffer = readDraft(this.formDraftKey)\n\t\t}\n\t},\n\n\tmounted() {\n\t\tthis.resolveRemoteVisibility()\n\t\tthis.resolveSmartPaste()\n\t\tthis.runAssignments(null)\n\t\tthis.loadProfileDefaults()\n\t},\n\n\tbeforeUnmount() {\n\t\tObject.values(this.calcTimers).forEach((timer) => clearTimeout(timer))\n\t},\n\n\tmethods: {\n\t\tt,\n\n\t\t/**\n\t\t * The registered smart-paste handler as `{ fill, available }`, or null.\n\t\t * A bare function is a `fill` with no availability check.\n\t\t *\n\t\t * @return {{fill: Function, available: Function|null}|null} The handler.\n\t\t */\n\t\tsmartPasteHandler() {\n\t\t\tconst name = this.smartPaste && this.smartPaste.handler\n\t\t\tconst entry = name ? this.effectiveCustomComponents[name] : null\n\t\t\tif (typeof entry === 'function') {\n\t\t\t\treturn { fill: entry, available: null }\n\t\t\t}\n\t\t\tif (entry && typeof entry.fill === 'function') {\n\t\t\t\treturn { fill: entry.fill, available: typeof entry.available === 'function' ? entry.available : null }\n\t\t\t}\n\t\t\treturn null\n\t\t},\n\n\t\t/**\n\t\t * Decide once, at mount, whether \"Paste to fill\" shows: enabled, not a\n\t\t * public form, a handler that resolves and does not report unavailable.\n\t\t * Anything else leaves the form exactly as before.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync resolveSmartPaste() {\n\t\t\tthis.smartPasteAvailable = false\n\t\t\tif (!this.smartPaste || this.smartPaste.enabled !== true || this.mode === 'public') {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst handler = this.smartPasteHandler()\n\t\t\tif (!handler) {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnFormPage] smartPaste handler \"${this.smartPaste.handler}\" is not registered; hiding \"Paste to fill\".`)\n\t\t\t\treturn\n\t\t\t}\n\t\t\ttry {\n\t\t\t\tthis.smartPasteAvailable = handler.available ? (await handler.available()) === true : true\n\t\t\t} catch {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn('[CnFormPage] smartPaste availability check failed; hiding \"Paste to fill\".')\n\t\t\t\tthis.smartPasteAvailable = false\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * The allowed fields as the handler sees them: key, label, type and\n\t\t * allowed options. Never a value.\n\t\t *\n\t\t * @return {Array<{key: string, label: string, type: string, options: string[]|null}>} The descriptors.\n\t\t */\n\t\tsmartPasteFields() {\n\t\t\tconst allowed = new Set(Array.isArray(this.smartPaste?.fields) ? this.smartPaste.fields : [])\n\t\t\treturn this.fields\n\t\t\t\t.filter((f) => f && allowed.has(f.key))\n\t\t\t\t.map((f) => ({\n\t\t\t\t\tkey: f.key,\n\t\t\t\t\tlabel: this.resolveLabel(f.label) || f.key,\n\t\t\t\t\ttype: f.type || 'string',\n\t\t\t\t\toptions: f.type === 'enum' ? this.enumValues(f) : null,\n\t\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * The allowed values of an enum field.\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @return {string[]} The values.\n\t\t */\n\t\tenumValues(field) {\n\t\t\tconst raw = Array.isArray(field.enum) ? field.enum : (Array.isArray(field.options) ? field.options : [])\n\t\t\treturn raw.map((o) => (o && typeof o === 'object' ? o.value : o)).filter((v) => v !== undefined && v !== null).map(String)\n\t\t},\n\n\t\t/**\n\t\t * Coerce a proposed value to the field's type.\n\t\t *\n\t\t * @param {object} field The field.\n\t\t * @param {unknown} value The proposed value.\n\t\t * @return {{ok: boolean, value?: unknown}} The coerced value, or not ok.\n\t\t */\n\t\tcoerceProposal(field, value) {\n\t\t\tswitch (field.type || 'string') {\n\t\t\t\tcase 'number': {\n\t\t\t\t\tconst n = typeof value === 'number' ? value : (typeof value === 'string' && value.trim() !== '' ? Number(value) : NaN)\n\t\t\t\t\treturn Number.isFinite(n) ? { ok: true, value: n } : { ok: false }\n\t\t\t\t}\n\t\t\t\tcase 'boolean':\n\t\t\t\t\tif (value === true || value === 'true') {\n\t\t\t\t\t\treturn { ok: true, value: true }\n\t\t\t\t\t}\n\t\t\t\t\tif (value === false || value === 'false') {\n\t\t\t\t\t\treturn { ok: true, value: false }\n\t\t\t\t\t}\n\t\t\t\t\treturn { ok: false }\n\t\t\t\tcase 'enum':\n\t\t\t\t\treturn this.enumValues(field).includes(String(value)) ? { ok: true, value: String(value) } : { ok: false }\n\t\t\t\tcase 'string':\n\t\t\t\t\treturn typeof value === 'string' || typeof value === 'number' ? { ok: true, value: String(value) } : { ok: false }\n\t\t\t\tdefault:\n\t\t\t\t\treturn { ok: false }\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a field holds nothing the user typed.\n\t\t *\n\t\t * @param {unknown} value The field value.\n\t\t * @return {boolean} True when empty.\n\t\t */\n\t\tisEmptyField(value) {\n\t\t\treturn value === undefined || value === null || value === '' || value === false || (Array.isArray(value) && value.length === 0)\n\t\t},\n\n\t\t/**\n\t\t * Run the handler on the pasted text and land its proposals: only\n\t\t * allowed, visible fields that are empty (or all of them when\n\t\t * \"Replace what I typed\" is ticked), only values that fit the type and\n\t\t * pass the field's validation. Each landed field is marked as a\n\t\t * suggestion. Nothing is submitted.\n\t\t *\n\t\t * @param {{text: string, replace: boolean}} request The dialog's request.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync onSmartPasteFill({ text, replace }) {\n\t\t\tconst handler = this.smartPasteHandler()\n\t\t\tif (!handler) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.smartPasteBusy = true\n\t\t\tthis.smartPasteError = ''\n\t\t\tlet values\n\t\t\ttry {\n\t\t\t\tconst answer = await handler.fill(text, this.smartPasteFields())\n\t\t\t\tvalues = answer && typeof answer.values === 'object' && answer.values !== null ? answer.values : {}\n\t\t\t} catch (err) {\n\t\t\t\tthis.smartPasteError = (err && err.message) || t('nextcloud-vue', 'The text could not be read.')\n\t\t\t\tthis.smartPasteBusy = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst allowed = new Set(Array.isArray(this.smartPaste?.fields) ? this.smartPaste.fields : [])\n\t\t\tlet filled = 0\n\t\t\tlet skipped = 0\n\t\t\tconst marks = { ...this.suggestions }\n\t\t\tfor (const [key, proposed] of Object.entries(values)) {\n\t\t\t\tconst field = this.fields.find((f) => f && f.key === key)\n\t\t\t\tif (!field || !allowed.has(key) || !this.isFieldVisible(key)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif (!replace && !this.isEmptyField(this.formData[key])) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tconst coerced = this.coerceProposal(field, proposed)\n\t\t\t\tif (!coerced.ok || validateFieldValue(field, coerced.value, this.resolveLabel) !== null) {\n\t\t\t\t\tskipped += 1\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tthis.updateField(key, coerced.value)\n\t\t\t\tmarks[key] = true\n\t\t\t\tfilled += 1\n\t\t\t}\n\t\t\tthis.suggestions = { ...this.suggestions, ...marks }\n\t\t\tthis.smartPasteNotice = skipped > 0\n\t\t\t\t? t('nextcloud-vue', '{filled} fields filled, {skipped} skipped', { filled, skipped })\n\t\t\t\t: t('nextcloud-vue', '{filled} fields filled', { filled })\n\t\t\tthis.smartPasteBusy = false\n\t\t\tthis.smartPasteOpen = false\n\t\t},\n\n\t\t/**\n\t\t * Accept one suggestion (clears its mark, keeps the value).\n\t\t *\n\t\t * @param {string} key The field key.\n\t\t */\n\t\tacceptSuggestion(key) {\n\t\t\tthis.suggestions = Object.fromEntries(Object.entries(this.suggestions).filter(([k]) => k !== key))\n\t\t},\n\n\t\t/** Accept every suggestion. */\n\t\tacceptAllSuggestions() {\n\t\t\tthis.suggestions = {}\n\t\t},\n\n\t\t/**\n\t\t * Take the offered local draft into the form.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\trestoreDraft() {\n\t\t\tif (this.draftOffer === null) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.formData = { ...this.formData, ...this.draftOffer.values }\n\t\t\tthis.draftOffer = null\n\t\t},\n\n\t\t/**\n\t\t * Decline the offered local draft and forget it.\n\t\t *\n\t\t * @return {void}\n\t\t */\n\t\tdiscardDraft() {\n\t\t\tthis.forgetDraft(this.formDraftKey)\n\t\t},\n\n\t\tcloneInitial() {\n\t\t\ttry {\n\t\t\t\treturn JSON.parse(JSON.stringify(this.initialValue || {}))\n\t\t\t} catch {\n\t\t\t\treturn {}\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Whether a field must be filled in (`validation.required`, or `required`).\n\t\t *\n\t\t * @param {object} field The formField shape.\n\t\t * @return {boolean} True for a required field.\n\t\t */\n\t\tisFieldRequired(field) {\n\t\t\treturn !!(field && ((field.validation && field.validation.required) || field.required === true))\n\t\t},\n\n\t\t/**\n\t\t * Whether the board look draws this field's head (label above the control,\n\t\t * error between). A boolean keeps its inline label, and a `#field-<key>`\n\t\t * override keeps its own markup.\n\t\t *\n\t\t * @param {object} field The formField shape.\n\t\t * @return {boolean} True when the head is drawn.\n\t\t */\n\t\tshowsBoardHead(field) {\n\t\t\tif (!this.isBoardLook || this.$slots[`field-${field.key}`]) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst render = this.resolveFieldRender(field)\n\t\t\treturn !!render && render.kind !== 'boolean'\n\t\t},\n\n\t\tcontrolIdFor(field) {\n\t\t\treturn `cn-form-page__field-${field.key}`\n\t\t},\n\n\t\terrorIdFor(field) {\n\t\t\treturn `cn-form-page__field-error-${field.key}`\n\t\t},\n\n\t\thelpIdFor(field) {\n\t\t\treturn `cn-form-page__field-help-${field.key}`\n\t\t},\n\n\t\t/**\n\t\t * The bindings of a field's control: the renderer's props, plus in the\n\t\t * board look the outside label, an id for the label's `for`, and the error\n\t\t * moved above the control. For a text control, in both looks, an invalid\n\t\t * value is `aria-invalid` and `aria-describedby` lists the error before\n\t\t * the hint; `aria-required` is set in the board look.\n\t\t *\n\t\t * @param {object} field The formField shape.\n\t\t * @return {object} The props to bind.\n\t\t */\n\t\tfieldProps(field) {\n\t\t\tconst render = this.resolveFieldRender(field)\n\t\t\tconst props = { ...render.props }\n\t\t\tconst head = this.showsBoardHead(field)\n\t\t\tconst textControl = ['string', 'number', 'password', 'fallback', 'string-textarea'].includes(render.kind)\n\t\t\tconst error = this.fieldErrors[field.key]\n\t\t\tif (head) {\n\t\t\t\tprops.id = this.controlIdFor(field)\n\t\t\t\tif (render.kind === 'enum') {\n\t\t\t\t\tprops.labelOutside = true\n\t\t\t\t\tdelete props.inputLabel\n\t\t\t\t} else if (textControl) {\n\t\t\t\t\tprops.labelOutside = true\n\t\t\t\t}\n\t\t\t\tdelete props.error\n\t\t\t\tdelete props.helperText\n\t\t\t}\n\t\t\tif (textControl) {\n\t\t\t\tconst ids = []\n\t\t\t\tif (error && (head || !this.fieldHasNativeErrorSupport(field))) {\n\t\t\t\t\tids.push(this.errorIdFor(field))\n\t\t\t\t}\n\t\t\t\tif (field.help) {\n\t\t\t\t\tids.push(this.helpIdFor(field))\n\t\t\t\t}\n\t\t\t\tif (ids.length > 0) {\n\t\t\t\t\tprops['aria-describedby'] = ids.join(' ')\n\t\t\t\t}\n\t\t\t\tif (error) {\n\t\t\t\t\tprops['aria-invalid'] = 'true'\n\t\t\t\t}\n\t\t\t\tif (head && this.isFieldRequired(field)) {\n\t\t\t\t\tprops['aria-required'] = 'true'\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn props\n\t\t},\n\n\t\t/**\n\t\t * The accessibility attributes of an enum field's NcSelect. NcSelect\n\t\t * forwards no attributes to its input, so these go through\n\t\t * `v-cn-select-aria` onto the combobox input instead of `fieldProps`.\n\t\t * The error element exists for an enum in both looks (NcSelect has no\n\t\t * native error text), so an invalid value lists it before the hint.\n\t\t *\n\t\t * @param {object} field The formField shape.\n\t\t * @return {object|null} The attributes, or null for any other control.\n\t\t */\n\t\tselectAria(field) {\n\t\t\tconst render = this.resolveFieldRender(field)\n\t\t\tif (!render || render.kind !== 'enum') {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\tconst ids = []\n\t\t\tif (this.fieldErrors[field.key]) {\n\t\t\t\tids.push(this.errorIdFor(field))\n\t\t\t}\n\t\t\tif (field.help) {\n\t\t\t\tids.push(this.helpIdFor(field))\n\t\t\t}\n\t\t\treturn {\n\t\t\t\t'aria-invalid': this.fieldErrors[field.key] ? 'true' : null,\n\t\t\t\t'aria-describedby': ids.length > 0 ? ids.join(' ') : null,\n\t\t\t\t'aria-required': this.showsBoardHead(field) && this.isFieldRequired(field) ? 'true' : null,\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Cancel (board look footer): go to `cancelRoute` when set, and tell the\n\t\t * host.\n\t\t */\n\t\tcancel() {\n\t\t\tif (this.cancelRoute !== '' && this.$router && typeof this.$router.push === 'function') {\n\t\t\t\tthis.$router.push(this.cancelRoute)\n\t\t\t}\n\t\t\t/**\n\t\t\t * @event cancel Emitted when the user presses Cancel in the board look.\n\t\t\t */\n\t\t\tthis.$emit('cancel')\n\t\t},\n\n\t\tresolveLabel(key) {\n\t\t\tif (!key) {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\treturn this.labelTranslator(key)\n\t\t},\n\n\t\t/**\n\t\t * Resolve render bindings for a field by delegating to the\n\t\t * shared `cnRenderFormField` helper. Memoised inline so the\n\t\t * template can call it once per field per render without\n\t\t * re-allocating bindings on unrelated re-renders.\n\t\t *\n\t\t * @param {object} field The formField shape to render.\n\t\t * @return {object|null}\n\t\t */\n\t\tresolveFieldRender(field) {\n\t\t\tconst rendered = cnRenderFormField({\n\t\t\t\tfield,\n\t\t\t\tvalue: this.formData[field.key],\n\t\t\t\tonInput: (next) => this.updateField(field.key, next),\n\t\t\t\tt: this.labelTranslator,\n\t\t\t\terror: this.fieldErrors[field.key] || null,\n\t\t\t})\n\t\t\t// A calculated field is the host's to set: shown, not editable.\n\t\t\tif (rendered && field.calculate && Array.isArray(field.calculate.inputs)) {\n\t\t\t\trendered.props = { ...rendered.props, readonly: true, disabled: rendered.kind === 'enum' || rendered.kind === 'boolean' }\n\t\t\t}\n\t\t\treturn rendered\n\t\t},\n\n\t\t/**\n\t\t * Whether the resolved input for `field` surfaces its error via\n\t\t * native `NcInputField`-family props (so CnFormPage should NOT\n\t\t * also render the adjacent `role=\"alert\"` paragraph). A\n\t\t * `#field-<key>` slot override always reports `false` — the\n\t\t * consumer's custom markup gets the standard fallback alert too.\n\t\t *\n\t\t * @param {object} field The formField shape to check.\n\t\t * @return {boolean}\n\t\t */\n\t\tfieldHasNativeErrorSupport(field) {\n\t\t\tif (this.$slots[`field-${field.key}`] || this.$slots[`field-${field.key}`]) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tconst render = this.resolveFieldRender(field)\n\t\t\tif (!render) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif (['string', 'number', 'password', 'fallback'].includes(render.kind)) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif (render.kind === 'string-textarea') {\n\t\t\t\treturn render.tag !== 'textarea'\n\t\t\t}\n\t\t\treturn false\n\t\t},\n\n\t\t/**\n\t\t * Resolve the field objects (in step order) for a given step\n\t\t * entry, dropping any key that no longer matches a declared field.\n\t\t *\n\t\t * @param {{id: string, fields: string[]}} step The step entry.\n\t\t * @return {Array<object>}\n\t\t */\n\t\tstepFields(step) {\n\t\t\tif (!step || !Array.isArray(step.fields)) {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\treturn step.fields.map((key) => this.fieldsByKey[key]).filter(Boolean)\n\t\t},\n\n\t\t/**\n\t\t * Whether every field belonging to `step` is currently hidden by\n\t\t * its `visibleWhen` condition (REQ-MFL-6: such a step is skipped\n\t\t * by Next/Back in both directions).\n\t\t *\n\t\t * @param {object} step The step entry.\n\t\t * @return {boolean}\n\t\t */\n\t\tisStepHidden(step) {\n\t\t\tconst flds = this.stepFields(step)\n\t\t\treturn flds.length > 0 && flds.every((f) => !this.isFieldVisible(f.key))\n\t\t},\n\n\t\t/**\n\t\t * Whether `key`'s field is currently visible per the\n\t\t * `effectiveVisibility` cascade.\n\t\t *\n\t\t * @param {string} key The field key.\n\t\t * @return {boolean}\n\t\t */\n\t\tisFieldVisible(key) {\n\t\t\treturn this.effectiveVisibility[key] !== false\n\t\t},\n\n\t\t/**\n\t\t * Resolve `endpoint` / `source` visibleWhen conditions once at\n\t\t * mount into `remoteVisibility` (fail-safe: any error hides the\n\t\t * field). Never re-run on formData changes — see REQ-MFL-9.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync resolveRemoteVisibility() {\n\t\t\tconst remoteFields = this.fields.filter((f) => f && f.visibleWhen && (f.visibleWhen.endpoint || f.visibleWhen.source))\n\t\t\tawait Promise.all(remoteFields.map(async (field) => {\n\t\t\t\tconst result = await evaluateVisibleWhen(field.visibleWhen, { object: this.formData })\n\t\t\t\tthis.remoteVisibility[field.key] = result\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Validate the VISIBLE fields in `fieldsList` via\n\t\t * `validateFieldValue`, populating / clearing `fieldErrors` as it\n\t\t * goes. Hidden fields are skipped entirely (REQ-MFL-10) and any\n\t\t * stale error for them is cleared.\n\t\t *\n\t\t * @param {Array<object>} fieldsList The fields to validate.\n\t\t * @return {string|null} The first invalid field's key, or `null` when all pass.\n\t\t */\n\t\tvalidateVisibleFields(fieldsList) {\n\t\t\tlet firstInvalidKey = null\n\t\t\tconst invalidKeys = []\n\t\t\tfieldsList.forEach((field) => {\n\t\t\t\tif (!field || typeof field.key !== 'string') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif (!this.isFieldVisible(field.key)) {\n\t\t\t\t\tdelete this.fieldErrors[field.key]\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst message = validateFieldValue(field, this.formData[field.key], this.resolveLabel)\n\t\t\t\tif (message) {\n\t\t\t\t\tthis.fieldErrors[field.key] = message\n\t\t\t\t\tinvalidKeys.push(field.key)\n\t\t\t\t\tif (!firstInvalidKey) {\n\t\t\t\t\t\tfirstInvalidKey = field.key\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tdelete this.fieldErrors[field.key]\n\t\t\t\t}\n\t\t\t})\n\t\t\tthis.summaryKeys = invalidKeys\n\t\t\treturn firstInvalidKey\n\t\t},\n\n\t\t/**\n\t\t * After a failed Next or submit: the board look moves focus to the error\n\t\t * summary, which links to every control; the Nextcloud look focuses the\n\t\t * first invalid field, as before.\n\t\t *\n\t\t * @param {string} firstInvalidKey The first invalid field's key.\n\t\t */\n\t\tfocusFirstError(firstInvalidKey) {\n\t\t\tif (!this.isBoardLook) {\n\t\t\t\tthis.focusField(firstInvalidKey)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tif (this.$refs.errorSummary && typeof this.$refs.errorSummary.focus === 'function') {\n\t\t\t\t\tthis.$refs.errorSummary.focus()\n\t\t\t\t} else {\n\t\t\t\t\tthis.focusField(firstInvalidKey)\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Move focus to the first invalid field's rendered input, after\n\t\t * the DOM reflects the current step / error state.\n\t\t *\n\t\t * @param {string} key The field key whose input should receive focus.\n\t\t */\n\t\tfocusField(key) {\n\t\t\tthis.$nextTick(() => {\n\t\t\t\tconst refEntry = this.$refs[`field-${key}`]\n\t\t\t\tconst node = Array.isArray(refEntry) ? refEntry[0] : refEntry\n\t\t\t\tif (!node || typeof node.querySelector !== 'function') {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst input = node.querySelector('input, textarea, select, [tabindex]')\n\t\t\t\tif (input && typeof input.focus === 'function') {\n\t\t\t\t\tinput.focus()\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\n\t\t/**\n\t\t * Advance to the next non-fully-hidden step, in EITHER direction.\n\t\t * Returns -1 when there is none (caller treats that as a no-op).\n\t\t *\n\t\t * @param {number} fromIndex The step index to search from.\n\t\t * @param {1|-1} direction `1` for Next, `-1` for Back.\n\t\t * @return {number}\n\t\t */\n\t\tnextVisibleStepIndex(fromIndex, direction) {\n\t\t\tlet idx = fromIndex + direction\n\t\t\twhile (idx >= 0 && idx < this.steps.length) {\n\t\t\t\tif (!this.isStepHidden(this.steps[idx])) {\n\t\t\t\t\treturn idx\n\t\t\t\t}\n\t\t\t\tidx += direction\n\t\t\t}\n\t\t\treturn -1\n\t\t},\n\n\t\t/**\n\t\t * Next button handler: validates the current step's visible\n\t\t * fields before advancing (REQ-MFL-7); a failing field blocks\n\t\t * navigation and moves focus to it.\n\t\t */\n\t\tnext() {\n\t\t\tconst fieldsList = this.stepFields(this.steps[this.currentStepIndex])\n\t\t\tconst firstInvalidKey = this.validateVisibleFields(fieldsList)\n\t\t\tif (firstInvalidKey) {\n\t\t\t\tthis.focusFirstError(firstInvalidKey)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst targetIndex = this.nextVisibleStepIndex(this.currentStepIndex, 1)\n\t\t\tif (targetIndex !== -1) {\n\t\t\t\tconst from = this.currentStepIndex\n\t\t\t\tthis.currentStepIndex = targetIndex\n\t\t\t\t/**\n\t\t\t\t * @event step Emitted on step navigation (Next / Back).\n\t\t\t\t * @type {{ from: number, to: number }} Step indices.\n\t\t\t\t */\n\t\t\t\tthis.$emit('step', { from, to: targetIndex })\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Back button handler: NEVER validates (users may retreat with\n\t\t * invalid input) — REQ-MFL-7.\n\t\t */\n\t\tback() {\n\t\t\tconst targetIndex = this.nextVisibleStepIndex(this.currentStepIndex, -1)\n\t\t\tif (targetIndex !== -1) {\n\t\t\t\tconst from = this.currentStepIndex\n\t\t\t\tthis.currentStepIndex = targetIndex\n\t\t\t\t/**\n\t\t\t\t * @event step Emitted on step navigation (Next / Back).\n\t\t\t\t * @type {{ from: number, to: number }} Step indices.\n\t\t\t\t */\n\t\t\t\tthis.$emit('step', { from, to: targetIndex })\n\t\t\t}\n\t\t},\n\n\t\tupdateField(key, value) {\n\t\t\tthis.formData[key] = value\n\t\t\t// A hand edit: this field's rules stop until the form is reset.\n\t\t\tthis.handEdited[key] = true\n\t\t\tdelete this.assignedFrom[key]\n\t\t\tdelete this.fieldErrors[key]\n\t\t\t// Editing a suggested field accepts it.\n\t\t\tif (this.suggestions[key]) {\n\t\t\t\tthis.suggestions = Object.fromEntries(Object.entries(this.suggestions).filter(([k]) => k !== key))\n\t\t\t}\n\t\t\t/**\n\t\t\t * Field-level update event.\n\t\t\t *\n\t\t\t * @event input\n\t\t\t * @type {{key: string, value: unknown}}\n\t\t\t */\n\t\t\tthis.$emit('input', { key, value })\n\t\t\tthis.runAssignments([key])\n\t\t},\n\n\t\t/**\n\t\t * The initial values with the fields' defaults filled in underneath\n\t\t * (a default never replaces an initial value). Tokens resolve once, here.\n\t\t *\n\t\t * @return {object} The values the form opens with.\n\t\t */\n\t\tinitialWithDefaults() {\n\t\t\tconst initial = this.cloneInitial()\n\t\t\treturn { ...resolveFieldDefaults(this.fields, initial, { me: this.meProfile || {} }), ...initial }\n\t\t},\n\n\t\t/**\n\t\t * Fill in fields from other answers (`assign` rules). `changed` names the\n\t\t * answers that just changed; `null` is the pass at open, over empty fields.\n\t\t *\n\t\t * @param {string[]|null} changed Keys that changed.\n\t\t */\n\t\trunAssignments(changed) {\n\t\t\tconst { values, from } = computeAssignments({\n\t\t\t\tfields: this.fields,\n\t\t\t\tanswers: this.formData,\n\t\t\t\tchanged,\n\t\t\t\tedited: Object.keys(this.handEdited),\n\t\t\t\tctx: { me: this.meProfile, object: this.formData },\n\t\t\t})\n\t\t\tconst keys = Object.keys(values)\n\t\t\tfor (const key of keys) {\n\t\t\t\tthis.formData[key] = values[key]\n\t\t\t\tdelete this.fieldErrors[key]\n\t\t\t\tconst source = this.fieldsByKey[from[key]]\n\t\t\t\tthis.assignedFrom[key] = source ? this.resolveLabel(source.label || source.key) : from[key]\n\t\t\t\tthis.$emit('input', { key, value: values[key] })\n\t\t\t}\n\t\t\tif (changed === null && keys.length > 0) {\n\t\t\t\t// Filled in on open: not something the person changed.\n\t\t\t\tthis.baseline = JSON.stringify(this.formData)\n\t\t\t}\n\t\t\tthis.scheduleCalculations([...(changed || []), ...keys])\n\t\t},\n\n\t\t/**\n\t\t * Ask the host to recalculate the fields that read the changed answers,\n\t\t * once the person has stopped typing for 400 ms.\n\t\t *\n\t\t * @param {string[]} changed Keys that changed.\n\t\t */\n\t\tscheduleCalculations(changed) {\n\t\t\tif (typeof this.calculate !== 'function' || changed.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor (const field of this.fields) {\n\t\t\t\tconst inputs = field && field.calculate && Array.isArray(field.calculate.inputs) ? field.calculate.inputs : []\n\t\t\t\tif (!inputs.some((k) => changed.includes(k))) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tclearTimeout(this.calcTimers[field.key])\n\t\t\t\tthis.calcTimers[field.key] = setTimeout(() => this.runCalculation(field.key), 400)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Run the host's calculation for one field and write the answer.\n\t\t *\n\t\t * @param {string} key The field to calculate.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync runCalculation(key) {\n\t\t\tthis.calculating[key] = true\n\t\t\tdelete this.calcErrors[key]\n\t\t\ttry {\n\t\t\t\tconst value = await this.calculate(key, { ...this.formData })\n\t\t\t\tthis.formData[key] = value\n\t\t\t\tthis.$emit('input', { key, value })\n\t\t\t} catch {\n\t\t\t\tthis.calcErrors[key] = t('nextcloud-vue', 'Could not calculate')\n\t\t\t} finally {\n\t\t\t\tdelete this.calculating[key]\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Fill in `@me.*` defaults the sync resolver could not: the e-mail comes\n\t\t * from the user's profile, asked for once and only when a default needs it.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync loadProfileDefaults() {\n\t\t\tconst needs = this.fields.filter((f) => f && typeof f.default === 'string' && /^@me\\.(email|displayName)$/.test(f.default) && (this.formData[f.key] === undefined || this.formData[f.key] === null || this.formData[f.key] === ''))\n\t\t\tif (needs.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.meProfile = await loadCurrentUserProfile()\n\t\t\tif (!this.meProfile.email && !this.meProfile.displayName) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst have = this.formData\n\t\t\tconst filled = resolveFieldDefaults(needs, have, { me: this.meProfile })\n\t\t\tfor (const [key, value] of Object.entries(filled)) {\n\t\t\t\tif (!this.handEdited[key]) {\n\t\t\t\t\tthis.formData[key] = value\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.baseline = JSON.stringify(this.formData)\n\t\t},\n\n\t\t/**\n\t\t * Dispatch the submit. When `steps` is present and the current\n\t\t * step is not the last (visible) one, a native form submit\n\t\t * (e.g. pressing Enter) is redirected to `next()` instead of\n\t\t * dispatching early. Otherwise validates ALL visible fields\n\t\t * across ALL steps (REQ-MFL-7); on failure, jumps to the\n\t\t * earliest step containing an invalid field. Picks endpoint mode\n\t\t * when `submitEndpoint` is set, otherwise handler mode. When\n\t\t * neither is set, emits `@error` with a clear message rather\n\t\t * than no-op silently.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync submit() {\n\t\t\tif (this.hasSteps && !this.isLastStep) {\n\t\t\t\tthis.next()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (this.submitBlocked) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconst allFieldsList = this.hasSteps\n\t\t\t\t? this.steps.reduce((acc, step) => acc.concat(this.stepFields(step)), [])\n\t\t\t\t: this.fields\n\t\t\tconst firstInvalidKey = this.validateVisibleFields(allFieldsList)\n\t\t\tif (firstInvalidKey) {\n\t\t\t\tif (this.hasSteps) {\n\t\t\t\t\tconst stepIndex = this.steps.findIndex((step) => this.stepFields(step).some((f) => f.key === firstInvalidKey))\n\t\t\t\t\tif (stepIndex >= 0) {\n\t\t\t\t\t\tthis.currentStepIndex = stepIndex\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.focusFirstError(firstInvalidKey)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tthis.lastError = null\n\t\t\tthis.submitting = true\n\t\t\ttry {\n\t\t\t\tif (this.submitEndpoint) {\n\t\t\t\t\tawait this.submitViaEndpoint()\n\t\t\t\t} else if (this.submitHandler) {\n\t\t\t\t\tawait this.submitViaHandler()\n\t\t\t\t} else {\n\t\t\t\t\tthrow new Error('CnFormPage: no submit destination configured (set submitHandler or submitEndpoint)')\n\t\t\t\t}\n\t\t\t\tthis.submitted = true\n\t\t\t\t// Once it is on the server, the local copy protects nothing.\n\t\t\t\tthis.forgetDraft(this.formDraftKey)\n\t\t\t\t/**\n\t\t\t\t * Successful submit event. Payload is the effective payload\n\t\t\t\t * (visible fields only).\n\t\t\t\t *\n\t\t\t\t * @event submit\n\t\t\t\t * @type {object}\n\t\t\t\t */\n\t\t\t\tthis.$emit('submit', this.effectivePayload)\n\t\t\t} catch (err) {\n\t\t\t\t// The server's own sentence, when it sent one. An axios message\n\t\t\t\t// describes the transport (\"Request failed with status code 400\")\n\t\t\t\t// and leaves the user to guess which field it meant.\n\t\t\t\tthis.lastError = serverErrorMessage(err)\n\t\t\t\t/**\n\t\t\t\t * Submit failure event. Payload is the thrown error / rejected reason.\n\t\t\t\t *\n\t\t\t\t * @event error\n\t\t\t\t * @type {Error}\n\t\t\t\t */\n\t\t\t\tthis.$emit('error', err)\n\t\t\t} finally {\n\t\t\t\tthis.submitting = false\n\t\t\t}\n\t\t},\n\n\t\tasync submitViaEndpoint() {\n\t\t\tconst method = (this.submitMethod || 'POST').toLowerCase()\n\t\t\tconst url = resolveParams(this.submitEndpoint, this.$route?.params || {})\n\t\t\tif (typeof axios[method] !== 'function') {\n\t\t\t\tthrow new Error(`CnFormPage: unsupported HTTP method \"${this.submitMethod}\"`)\n\t\t\t}\n\t\t\tawait axios[method](url, this.effectivePayload)\n\t\t},\n\n\t\tasync submitViaHandler() {\n\t\t\tconst handler = this.effectiveCustomComponents[this.submitHandler]\n\t\t\tif (typeof handler !== 'function') {\n\t\t\t\t// eslint-disable-next-line no-console\n\t\t\t\tconsole.warn(`[CnFormPage] handler \"${this.submitHandler}\" not found in customComponents (or not a function). Did you register it?`)\n\t\t\t\tthrow new Error(`CnFormPage: handler \"${this.submitHandler}\" not registered`)\n\t\t\t}\n\t\t\tawait handler(this.effectivePayload, this.$route, this.$router)\n\t\t},\n\t},\n}\n</script>\n\n<style>\n.cn-form-page {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 1rem;\n\tpadding: 1rem;\n\tmax-width: 720px;\n\tmargin: 0 auto;\n\tcolor: var(--color-main-text);\n}\n\n.cn-form-page__form {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 1rem;\n}\n\n.cn-form-page__steps-nav {\n\twidth: 100%;\n}\n\n.cn-form-page__steps {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\tgap: 1rem;\n\tlist-style: none;\n\tmargin: 0;\n\tpadding: 0;\n}\n\n.cn-form-page__step {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-weight: normal;\n}\n\n.cn-form-page__step--current {\n\tcolor: var(--color-main-text);\n\tfont-weight: bold;\n}\n\n.cn-form-page__step--done {\n\tcolor: var(--color-main-text);\n}\n\n.cn-form-page__step-check {\n\tcolor: var(--color-primary-element);\n\tmargin-right: 0.25rem;\n}\n\n.cn-form-page__step-description {\n\tcolor: var(--color-text-maxcontrast);\n\tmargin: 0;\n}\n\n.cn-form-page__field {\n\tdisplay: flex;\n\tflex-direction: column;\n\tgap: 0.25rem;\n}\n\n.cn-form-page__field-help {\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n\n.cn-form-page__field-error {\n\tcolor: var(--color-error);\n\tfont-size: 0.85em;\n\tmargin: 0;\n}\n\n.cn-form-page__error {\n\tcolor: var(--color-error);\n\tbackground: var(--color-error-hover, transparent);\n\tpadding: 0.5rem 0.75rem;\n\tborder-radius: var(--border-radius);\n}\n\n.cn-form-page__success {\n\tcolor: var(--color-success-text, var(--color-main-text));\n\tbackground: var(--color-success-hover, transparent);\n\tpadding: 1rem;\n\tborder-radius: var(--border-radius);\n\ttext-align: center;\n}\n\n.cn-form-page__draft-state {\n\tmargin: 8px 0 0;\n\tmin-height: 1.2em;\n\tcolor: var(--color-text-maxcontrast);\n\tfont-size: 0.85em;\n}\n\n.cn-form-page__draft-actions {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-top: 8px;\n}\n\n.cn-form-page__actions {\n\tdisplay: flex;\n\tjustify-content: flex-end;\n\tgap: 0.5rem;\n}\n\n.cn-form-page__submit {\n\tdisplay: flex;\n\tjustify-content: flex-start;\n\tgap: 0.5rem;\n\tmargin-top: 0.5rem;\n}\n\n.cn-form-page__smart-paste {\n\tdisplay: flex;\n\tgap: 8px;\n\tmargin-bottom: 12px;\n}\n\n.cn-form-page__smart-paste-notice {\n\tmargin: 0 0 8px;\n\tcolor: var(--color-text-maxcontrast);\n}\n\n.cn-form-page__smart-paste-notice:empty {\n\tdisplay: none;\n}\n\n.cn-form-page__suggested {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 4px;\n\tmargin-top: 4px;\n}\n\n.cn-form-page__suggested-tag {\n\tpadding: 2px 8px;\n\tborder-radius: var(--border-radius-pill, 999px);\n\tbackground: var(--color-primary-element-light);\n\tcolor: var(--color-primary-element-light-text);\n\tfont-size: 12px;\n}\n</style>\n"],"names":["_createElementBlock","_renderSlot","_openBlock","_toDisplayString","_createTextVNode","_createElementVNode","_createVNode","_createBlock","_Fragment","_renderList","_withDirectives","_resolveDynamicComponent","_mergeProps","_toHandlers","_normalizeClass"],"mappings":"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"}
@@ -15,7 +15,7 @@ import __sfc_main$v from '../../dialogs/CnConfirmDialog.vue.js';
15
15
  import __sfc_main$w from '../../dialogs/CnQuickEditDialog.vue.js';
16
16
  import __sfc_main from '../CnBuildiqEditButton/CnBuildiqEditButton.vue.js';
17
17
  import __sfc_main$2 from '../CnEmptyContent/CnEmptyContent.vue.js';
18
- import __sfc_main$1 from '../CnFavouriteToggle/CnFavouriteToggle.vue.js';
18
+ import __sfc_main$1 from '../CnFollowToggle/CnFollowToggle.vue.js';
19
19
  import { useLook } from '../../composables/useLook.js';
20
20
  import { copyKindsOf } from '../../composables/useObjectCopy.js';
21
21
  import { createRefLabelResolver } from '../../composables/useRefLabels.js';
@@ -27,6 +27,7 @@ import { resolveRegisteredHandler, buildOnSuccessRoute } from '../../utils/actio
27
27
  import { reportBindingProblems } from '../../utils/diagnostics.js';
28
28
  import { fetchFilterCounts } from '../../utils/fetchFilterCounts.js';
29
29
  import { buildExportUrl } from '../../utils/indexExportHelpers.js';
30
+ import { lensUnavailableText } from '../../utils/lensAvailability.js';
30
31
  import { openRowTarget } from '../../utils/linkNavigation.js';
31
32
  import { resolveClaimedTeams, splitViewsIntoTabs, viewAsTab, resolveClaimTokens, viewIdOf } from '../../utils/listLenses.js';
32
33
  import { LIST_SHORTCUTS, listPaletteCommands, shortcutFor } from '../../utils/listShortcuts.js';
@@ -100,8 +101,8 @@ const CALENDAR_PAGE_SIZE = 200;
100
101
  */
101
102
  const RANGE_SEPARATOR = '..';
102
103
 
103
- /** Key of the synthetic star column (`showFavouriteColumn`). */
104
- const FAVOURITE_COLUMN_KEY = '__favourite';
104
+ /** Key of the synthetic follow column (`showFollowColumn`). */
105
+ const FOLLOW_COLUMN_KEY = '__follow';
105
106
 
106
107
  /**
107
108
  * Whether a schema property wants a from/to pair rather than a value list.
@@ -303,7 +304,7 @@ var script = {
303
304
 
304
305
  components: {
305
306
  CnBuildiqEditButton: __sfc_main,
306
- CnFavouriteToggle: __sfc_main$1,
307
+ CnFollowToggle: __sfc_main$1,
307
308
  NcLoadingIcon,
308
309
  CnEmptyContent: __sfc_main$2,
309
310
  NcActions,
@@ -639,9 +640,10 @@ var script = {
639
640
  },
640
641
 
641
642
  /**
642
- * Personal lenses appended to the quick filters: any of `favourite`
643
- * (`_favourite`), `recent` (`_recent`), `watching` (`_watching`) and
644
- * `unread` (`_unread`).
643
+ * Personal lenses appended to the quick filters: any of `watching`
644
+ * (`_watching`, labelled Following), `recent` (`_recent`) and `unread`
645
+ * (`_unread`). `favourite` is a deprecated alias of `watching`: a
646
+ * favourite is a follow now, and asking for both gives one tab.
645
647
  * They combine with every other filter. While Recent is active column
646
648
  * sorting is off, because the lens owns the order. Manifest
647
649
  * `config.personalLenses`.
@@ -654,9 +656,20 @@ var script = {
654
656
  },
655
657
 
656
658
  /**
657
- * Add a first column with a star per row (`CnFavouriteToggle`), bound to
658
- * each row's `@self.favourite`. Clicking it does not open the row.
659
- * Manifest `config.showFavouriteColumn`.
659
+ * Add a first column with a follow toggle per row (`CnFollowToggle`,
660
+ * compact), bound to each row's `@self.watching`. Clicking it does not
661
+ * open the row. Manifest `config.showFollowColumn`.
662
+ */
663
+ showFollowColumn: {
664
+ type: Boolean,
665
+ default: false,
666
+ },
667
+
668
+ /**
669
+ * Deprecated alias of `showFollowColumn`: the star column became the
670
+ * follow column (OpenRegister `merge-follow-and-favourites`).
671
+ *
672
+ * @deprecated Use `showFollowColumn`.
660
673
  */
661
674
  showFavouriteColumn: {
662
675
  type: Boolean,
@@ -1151,6 +1164,34 @@ var script = {
1151
1164
  default: 'No items found',
1152
1165
  },
1153
1166
 
1167
+ /**
1168
+ * Personal-lens report of the list on screen, for a host that fetches
1169
+ * itself: the response's `@self.lenses` (`{ recent: { available,
1170
+ * reason } }`). In self-fetch mode the page reads it from the store
1171
+ * and this prop is ignored. When a lens reports `available: false`
1172
+ * the empty state explains why instead of showing `emptyText`.
1173
+ *
1174
+ * @type {object|null}
1175
+ * @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
1176
+ */
1177
+ lenses: {
1178
+ type: Object,
1179
+ default: null,
1180
+ },
1181
+
1182
+ /**
1183
+ * App wording for an unavailable lens, keyed `<lens>.<reason>`
1184
+ * (`recent.audit-trail-disabled`) or `<reason>`; the specific key
1185
+ * wins, and both win over the library's object-neutral text.
1186
+ *
1187
+ * @type {{[key: string]: string}|null}
1188
+ * @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
1189
+ */
1190
+ lensReasonTexts: {
1191
+ type: Object,
1192
+ default: null,
1193
+ },
1194
+
1154
1195
  /** Accessible label for the loading spinner (NcLoadingIcon aria-label) */
1155
1196
  loadingText: {
1156
1197
  type: String,
@@ -2342,6 +2383,40 @@ var script = {
2342
2383
  return this.emptyText ? fn(this.emptyText) : this.emptyText
2343
2384
  },
2344
2385
 
2386
+ /**
2387
+ * The personal-lens reports of the list on screen: the store's copy
2388
+ * of the latest response in self-fetch mode, else the `lenses` prop.
2389
+ *
2390
+ * @return {object}
2391
+ * @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-a-list-response-carries-the-report-of-the-lenses-it-was-asked-for
2392
+ */
2393
+ effectiveLenses() {
2394
+ if (this.isNamedSource) {
2395
+ return {}
2396
+ }
2397
+ if (this.isSelfFetchMode) {
2398
+ return (this.list.lenses && this.list.lenses.value) || {}
2399
+ }
2400
+ return this.lenses || {}
2401
+ },
2402
+
2403
+ /**
2404
+ * Why the list is empty when a personal lens could not answer (the
2405
+ * audit trail is off, nobody is logged in, the history could not be
2406
+ * read), or `''` to keep the generic empty text.
2407
+ *
2408
+ * @return {string}
2409
+ * @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-an-unavailable-lens-explains-its-empty-page
2410
+ */
2411
+ lensReasonText() {
2412
+ const text = lensUnavailableText(this.effectiveLenses, this.lensReasonTexts);
2413
+ if (!text) {
2414
+ return ''
2415
+ }
2416
+ const fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k;
2417
+ return fn(text)
2418
+ },
2419
+
2345
2420
  /**
2346
2421
  * Whether the host manifest editor is in edit mode (unwraps the injected
2347
2422
  * `cnEditingBody`, which may be a Vue ref or a plain boolean). Drives the
@@ -3235,12 +3310,22 @@ var script = {
3235
3310
  if (this.recentLensActive) {
3236
3311
  cols = cols.map((col) => (col && typeof col === 'object' ? { ...col, sortable: false } : col));
3237
3312
  }
3238
- if (this.showFavouriteColumn && this.favouriteSchemaSlug !== '') {
3239
- cols = [{ key: FAVOURITE_COLUMN_KEY, label: '', sortable: false, width: '48px' }, ...cols];
3313
+ if (this.followColumnOn && this.favouriteSchemaSlug !== '') {
3314
+ cols = [{ key: FOLLOW_COLUMN_KEY, label: '', sortable: false, width: '48px' }, ...cols];
3240
3315
  }
3241
3316
  return cols
3242
3317
  },
3243
3318
 
3319
+ /**
3320
+ * Whether the follow column renders (`showFollowColumn`, or the deprecated `showFavouriteColumn`).
3321
+ *
3322
+ * @return {boolean}
3323
+ * @spec openspec/changes/one-follow-control/specs/record-follow/spec.md#requirement-index-pages-offer-one-following-lens-and-a-follow-column
3324
+ */
3325
+ followColumnOn() {
3326
+ return this.showFollowColumn || this.showFavouriteColumn
3327
+ },
3328
+
3244
3329
  /**
3245
3330
  * Whether the active quick filter is the Recent lens.
3246
3331
  *
@@ -3255,7 +3340,7 @@ var script = {
3255
3340
  },
3256
3341
 
3257
3342
  /**
3258
- * Schema slug the star column calls with: the page's own slug in self-fetch
3343
+ * Schema slug the follow column calls with: the page's own slug in self-fetch
3259
3344
  * mode, else the schema object's `slug`. Empty when neither is known.
3260
3345
  *
3261
3346
  * @return {string}
@@ -4132,6 +4217,7 @@ var script = {
4132
4217
  * stable key and a variant.
4133
4218
  *
4134
4219
  * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
4220
+ * @spec openspec/changes/screens-index-header-buttons-parity/specs/index-list-board-look/spec.md#requirement-the-add-header-button-carries-a-plus
4135
4221
  * @return {Array<{key: string, label: string, action: string, variant: string, icon: string, format: string}>}
4136
4222
  */
4137
4223
  resolvedHeaderButtons() {
@@ -4144,6 +4230,11 @@ var script = {
4144
4230
  if (label === '' && button.action === 'add') {
4145
4231
  label = this.resolvedAddLabel;
4146
4232
  }
4233
+ // The board's primary button reads "+ New request": the Add
4234
+ // button takes the plus unless the manifest names an icon.
4235
+ if (this.isBoardLook && button.action === 'add' && icon === '') {
4236
+ icon = 'Plus';
4237
+ }
4147
4238
  // The board's header: Export reads "Download" with its icon and
4148
4239
  // the Actions menu reads "Actions", without the manifest saying so.
4149
4240
  if (this.isBoardLook && label === '' && button.action === 'export') {