@conduction/nextcloud-vue 2.80.0 → 2.81.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.
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +3 -3
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +80 -4
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +95 -91
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +140 -10
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +52 -16
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +17 -3
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +63 -52
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js +10 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js +252 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js +140 -0
- package/dist/esm/components/CnChipsFieldWidget/CnChipsFieldWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChipsFieldWidget/index.js +26 -0
- package/dist/esm/components/CnChipsFieldWidget/index.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js +10 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js +354 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js +172 -0
- package/dist/esm/components/CnChoicePickerWidget/CnChoicePickerWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChoicePickerWidget/index.js +27 -0
- package/dist/esm/components/CnChoicePickerWidget/index.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +2 -2
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +65 -5
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +19 -11
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +3 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +72 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +21 -4
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js +7 -7
- package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +131 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +80 -5
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +282 -69
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +302 -4
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +417 -244
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js +101 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +141 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +60 -0
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +74 -49
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +29 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +48 -4
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +7 -7
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +1 -1
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +65 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +6 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +63 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +8 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +14 -0
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js +5 -1
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js +10 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js +174 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js +75 -0
- package/dist/esm/components/CnRadioCardsWidget/CnRadioCardsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnRadioCardsWidget/index.js +28 -0
- package/dist/esm/components/CnRadioCardsWidget/index.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +30 -6
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +34 -25
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js +10 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js +222 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js +64 -0
- package/dist/esm/components/CnSavedViewsWidget/CnSavedViewsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSavedViewsWidget/index.js +30 -0
- package/dist/esm/components/CnSavedViewsWidget/index.js.map +1 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js +10 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js +347 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js +184 -0
- package/dist/esm/components/CnSettingsFormWidget/CnSettingsFormWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSettingsFormWidget/index.js +27 -0
- package/dist/esm/components/CnSettingsFormWidget/index.js.map +1 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +87 -6
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +84 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +85 -41
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js +10 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js +159 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js +60 -0
- package/dist/esm/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSwitchRowsWidget/index.js +26 -0
- package/dist/esm/components/CnSwitchRowsWidget/index.js.map +1 -0
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +47 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +16 -14
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js +80 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js +75 -32
- package/dist/esm/components/CnTasksWidget/CnTasksWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +10 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js.map +1 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +269 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js +106 -0
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnToggleTableWidget/index.js +27 -0
- package/dist/esm/components/CnToggleTableWidget/index.js.map +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +7 -2
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
- package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +197 -6
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +96 -50
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
- package/dist/esm/composables/useListView.js +4 -4
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/index.js +17 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +9 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +55 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +1041 -12
- package/dist/esm/utils/contentHeaderLink.js +51 -0
- package/dist/esm/utils/contentHeaderLink.js.map +1 -0
- package/dist/esm/utils/facets.js +75 -3
- package/dist/esm/utils/facets.js.map +1 -1
- package/dist/esm/utils/fetchFilterCounts.js +40 -5
- package/dist/esm/utils/fetchFilterCounts.js.map +1 -1
- package/dist/esm/utils/fillTemplate.js +7 -1
- package/dist/esm/utils/fillTemplate.js.map +1 -1
- package/dist/esm/utils/initials.js +32 -0
- package/dist/esm/utils/initials.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +7 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/objectWrite.js +59 -0
- package/dist/esm/utils/objectWrite.js.map +1 -0
- package/dist/esm/utils/pageControls.js +81 -0
- package/dist/esm/utils/pageControls.js.map +1 -0
- package/dist/esm/utils/personalLenses.js +40 -3
- package/dist/esm/utils/personalLenses.js.map +1 -1
- package/dist/esm/utils/resolveFilterTokens.js +1 -1
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/resolveManifestTokens.js +2 -0
- package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +19 -1
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/userGroupsPeek.js +91 -0
- package/dist/esm/utils/userGroupsPeek.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/visibleWhen.js +57 -5
- package/dist/esm/utils/visibleWhen.js.map +1 -1
- package/dist/esm/utils/widgetDispatch.js +1 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/esm/utils/widgetVisibility.js +4 -57
- package/dist/esm/utils/widgetVisibility.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +13589 -7833
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1041 -12
- package/l10n/en.json +9 -1
- package/l10n/nl.json +55 -1
- package/package.json +2 -1
- package/scripts/check-docs-mdx.mjs +108 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
- package/src/components/CnAppRoot/CnAppRoot.md +4 -2
- package/src/components/CnAppRoot/CnAppRoot.vue +151 -59
- package/src/components/CnBannerWidget/CnBannerWidget.vue +205 -20
- package/src/components/CnCellRenderer/CnCellRenderer.md +2 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +47 -4
- package/src/components/CnChipsFieldWidget/CnChipsFieldWidget.vue +433 -0
- package/src/components/CnChipsFieldWidget/index.js +25 -0
- package/src/components/CnChoicePickerWidget/CnChoicePickerWidget.vue +556 -0
- package/src/components/CnChoicePickerWidget/index.js +26 -0
- package/src/components/CnConversationThread/CnConversationWidget.vue +1 -1
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +2 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +78 -5
- package/src/components/CnDataTable/CnDataTable.vue +21 -12
- package/src/components/CnDetailPage/CnDetailPage.vue +89 -3
- package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +1 -1
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +7 -7
- package/src/components/CnFactsWidget/CnFactsWidget.vue +142 -2
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +268 -6
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +588 -117
- package/src/components/CnFilesBrowser/filesBrowser.js +100 -0
- package/src/components/CnFilesBrowser/filesBrowserColumns.js +140 -0
- package/src/components/CnFormPage/CnFormPage.vue +81 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +56 -4
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +7 -7
- package/src/components/CnNextStepCard/CnNextStepWidget.vue +1 -1
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +69 -2
- package/src/components/CnObjectSidebar/CnFilesTab.vue +70 -1
- package/src/components/CnObjectSidebar/CnNotesTab.vue +16 -0
- package/src/components/CnRadioCardsWidget/CnRadioCardsWidget.vue +313 -0
- package/src/components/CnRadioCardsWidget/index.js +27 -0
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +55 -6
- package/src/components/CnSavedViewsWidget/CnSavedViewsWidget.vue +296 -0
- package/src/components/CnSavedViewsWidget/index.js +29 -0
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +13 -0
- package/src/components/CnSettingsFormWidget/CnSettingsFormWidget.vue +555 -0
- package/src/components/CnSettingsFormWidget/index.js +26 -0
- package/src/components/CnStatWidget/CnStatWidget.vue +87 -6
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +180 -3
- package/src/components/CnSwitchRowsWidget/CnSwitchRowsWidget.vue +226 -0
- package/src/components/CnSwitchRowsWidget/index.js +25 -0
- package/src/components/CnTabsWidget/CnTabsWidget.vue +82 -3
- package/src/components/CnTasksWidget/CnTasksWidget.vue +175 -15
- package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +386 -0
- package/src/components/CnToggleTableWidget/index.js +26 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +7 -0
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +7 -2
- package/src/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
- package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +309 -11
- package/src/components/index.js +15 -0
- package/src/composables/useListView.js +4 -4
- package/src/css/look-board-detail.css +36 -2
- package/src/css/look-board-index.css +39 -8
- package/src/css/look-board.css +72 -0
- package/src/index.js +15 -0
- package/src/schemas/app-manifest-v2.schema.json +93 -9
- package/src/utils/contentHeaderLink.js +48 -0
- package/src/utils/facets.js +74 -2
- package/src/utils/fetchFilterCounts.js +39 -4
- package/src/utils/fillTemplate.js +7 -1
- package/src/utils/initials.js +29 -0
- package/src/utils/libraryWidgetKeys.js +7 -0
- package/src/utils/objectWrite.js +55 -0
- package/src/utils/pageControls.js +78 -0
- package/src/utils/personalLenses.js +39 -2
- package/src/utils/resolveFilterTokens.js +3 -1
- package/src/utils/resolveManifestTokens.js +2 -0
- package/src/utils/sentinelTokens.js +19 -1
- package/src/utils/userGroupsPeek.js +87 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/visibleWhen.js +56 -4
- package/src/utils/widgetDispatch.js +1 -1
- package/src/utils/widgetVisibility.js +7 -56
|
@@ -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 { clearAnonymousDrafts, draftIndicatorText, draftKey, formDraftMixin, readDraft, resolveDraftUserId } 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. Empty: the signed-in Nextcloud user. */\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.draftUserOwner })\n\t\t},\n\n\t\t/**\n\t\t * Who the draft belongs to: `draftUserId`, else the signed-in user.\n\t\t *\n\t\t * @return {string} The user id, or '' when nobody is signed in.\n\t\t * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-belongs-to-the-signed-in-user\n\t\t */\n\t\tdraftUserOwner() {\n\t\t\treturn resolveDraftUserId(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\t// A draft under `anonymous` may be somebody else's: removed once a\n\t\t// signed-in user is known, never offered.\n\t\tif (this.formDraftKey && this.draftUserOwner) {\n\t\t\tclearAnonymousDrafts()\n\t\t}\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<!-- Spam protection: a field no person sees or reaches. A filled one\n\t\t\t means a bot; the submit then sends nothing (see `honeypot`). -->\n\t\t\t<div v-if=\"honeypot\" class=\"cn-form-page__honeypot\" aria-hidden=\"true\">\n\t\t\t\t<label :for=\"`cn-form-page-hp-${honeypot}`\">{{ t('nextcloud-vue', 'Leave this field empty') }}</label>\n\t\t\t\t<input\n\t\t\t\t\t:id=\"`cn-form-page-hp-${honeypot}`\"\n\t\t\t\t\tv-model=\"honeypotValue\"\n\t\t\t\t\t:name=\"honeypot\"\n\t\t\t\t\ttype=\"text\"\n\t\t\t\t\ttabindex=\"-1\"\n\t\t\t\t\tautocomplete=\"off\"\n\t\t\t\t\tdata-testid=\"cn-form-page-honeypot\">\n\t\t\t</div>\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 { clearAnonymousDrafts, draftIndicatorText, draftKey, formDraftMixin, readDraft, resolveDraftUserId } 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. Empty: the signed-in Nextcloud user. */\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/**\n\t\t * Name of a honeypot field for a form anonymous visitors fill in.\n\t\t * The field is rendered out of sight and out of the tab order; when\n\t\t * it is filled, submit sends nothing and shows the normal success,\n\t\t * so a bot learns nothing. Empty (default): no honeypot.\n\t\t */\n\t\thoneypot: {\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/** What a bot typed into the honeypot field; stays empty for a person. */\n\t\t\thoneypotValue: '',\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.draftUserOwner })\n\t\t},\n\n\t\t/**\n\t\t * Who the draft belongs to: `draftUserId`, else the signed-in user.\n\t\t *\n\t\t * @return {string} The user id, or '' when nobody is signed in.\n\t\t * @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-draft-belongs-to-the-signed-in-user\n\t\t */\n\t\tdraftUserOwner() {\n\t\t\treturn resolveDraftUserId(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\t// A draft under `anonymous` may be somebody else's: removed once a\n\t\t// signed-in user is known, never offered.\n\t\tif (this.formDraftKey && this.draftUserOwner) {\n\t\t\tclearAnonymousDrafts()\n\t\t}\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\tif (this.honeypot && this.honeypotValue !== '') {\n\t\t\t\t// A bot filled the field no person sees: send nothing, look done.\n\t\t\t\tthis.submitted = true\n\t\t\t\treturn\n\t\t\t}\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// A refusal that names its fields (`findings[]`, as OpenRegister's\n\t\t\t\t// form destination check answers 422) is marked on each field.\n\t\t\t\tthis.showServerFindings(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\t/**\n\t\t * Put each finding of a refused submit on its field.\n\t\t *\n\t\t * Reads `response.data.findings[]`, each `{ field?, property, message }`,\n\t\t * and marks the field whose key is the finding's `field`, or else its\n\t\t * `property`. Findings on no field of this form stay in the general\n\t\t * error above the form. Nothing happens for an answer without findings.\n\t\t *\n\t\t * @param {object} err The rejected request.\n\t\t * @return {void}\n\t\t */\n\t\tshowServerFindings(err) {\n\t\t\tconst findings = err?.response?.data?.findings\n\t\t\tif (!Array.isArray(findings) || findings.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst keys = new Set(this.fields.map((f) => f && f.key))\n\t\t\tconst marked = []\n\t\t\tfor (const finding of findings) {\n\t\t\t\tconst key = finding && (finding.field || finding.property)\n\t\t\t\tif (!key || !keys.has(key) || this.fieldErrors[key]) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tthis.fieldErrors[key] = String(finding.message || t('nextcloud-vue', 'This answer was refused.'))\n\t\t\t\tmarked.push(key)\n\t\t\t}\n\t\t\tif (marked.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tthis.summaryKeys = this.fields.map((f) => f.key).filter((key) => marked.includes(key))\n\t\t\tif (this.hasSteps) {\n\t\t\t\tconst stepIndex = this.steps.findIndex((step) => this.stepFields(step).some((f) => f.key === this.summaryKeys[0]))\n\t\t\t\tif (stepIndex >= 0) {\n\t\t\t\t\tthis.currentStepIndex = stepIndex\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.focusFirstError(this.summaryKeys[0])\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__honeypot {\n\tposition: absolute;\n\tleft: -10000px;\n\twidth: 1px;\n\theight: 1px;\n\toverflow: hidden;\n}\n\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":["_renderSlot","_openBlock","_createElementBlock","_toDisplayString","_createBlock","_createTextVNode","_createElementVNode","_createVNode","_Fragment","_renderList","_withDirectives","_resolveDynamicComponent","_mergeProps","_toHandlers","_normalizeClass"],"mappings":"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"}
|
|
@@ -10,6 +10,7 @@ import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue';
|
|
|
10
10
|
import Export from 'vue-material-design-icons/Export.vue';
|
|
11
11
|
import Eye from 'vue-material-design-icons/Eye.vue';
|
|
12
12
|
import FilterOutline from 'vue-material-design-icons/FilterOutline.vue';
|
|
13
|
+
import Tune from 'vue-material-design-icons/Tune.vue';
|
|
13
14
|
import ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue';
|
|
14
15
|
import __sfc_main$w from '../../dialogs/CnConfirmDialog.vue.js';
|
|
15
16
|
import __sfc_main$x from '../../dialogs/CnQuickEditDialog.vue.js';
|
|
@@ -320,6 +321,7 @@ var script = {
|
|
|
320
321
|
DatabaseSearch,
|
|
321
322
|
Export,
|
|
322
323
|
FilterOutline,
|
|
324
|
+
Tune,
|
|
323
325
|
ViewColumnOutline,
|
|
324
326
|
NcCheckboxRadioSwitch,
|
|
325
327
|
CnPageHeader: __sfc_main$4,
|
|
@@ -501,7 +503,6 @@ var script = {
|
|
|
501
503
|
default: true,
|
|
502
504
|
},
|
|
503
505
|
|
|
504
|
-
// eslint-disable-next-line vue/no-unused-properties
|
|
505
506
|
/**
|
|
506
507
|
* Draw this page in the `board` look (or `nextcloud`) whatever the app
|
|
507
508
|
* does. Empty follows the `cnLook` CnAppRoot provides. Manifest key
|
|
@@ -652,7 +653,10 @@ var script = {
|
|
|
652
653
|
* unchanged. Under the board look a tab with `activeLabel` ("Pipeline:
|
|
653
654
|
* Sales") adds a removable chip with that text to the Active line and
|
|
654
655
|
* counts on the Filter badge while it is selected; removing the chip
|
|
655
|
-
* goes back to the default tab.
|
|
656
|
+
* goes back to the default tab. A tab can say `empty: "field"` (or a
|
|
657
|
+
* list of fields) for the rows where the field has no value, and
|
|
658
|
+
* `notEmpty` for the rows where it has one: it reads as the filter
|
|
659
|
+
* `{ field: { isnull: true } }`.
|
|
656
660
|
*/
|
|
657
661
|
quickFilters: {
|
|
658
662
|
type: Array,
|
|
@@ -1937,6 +1941,8 @@ var script = {
|
|
|
1937
1941
|
* Show a filter menu (funnel button) in the table header, above the
|
|
1938
1942
|
* row-actions column. Its menu lists every enum/badge column's values as
|
|
1939
1943
|
* toggleable facet filters — a compact alternative to the facet sidebar.
|
|
1944
|
+
* Under the board look, on a page with no filter sidebar, it is the
|
|
1945
|
+
* toolbar's labelled Filter button instead (row 1, before the view switch).
|
|
1940
1946
|
* Fed from the manifest as `pages[].config.filterMenu`.
|
|
1941
1947
|
*/
|
|
1942
1948
|
filterMenu: {
|
|
@@ -4651,6 +4657,27 @@ var script = {
|
|
|
4651
4657
|
return !!this.resolvedSidebar.enabled && this.resolvedSidebar.show !== false
|
|
4652
4658
|
},
|
|
4653
4659
|
|
|
4660
|
+
/**
|
|
4661
|
+
* Whether the board toolbar draws the Filter button as a menu: the
|
|
4662
|
+
* board look, `filterMenu` on, something to filter by, and no filter
|
|
4663
|
+
* sidebar (whose toggle is that button otherwise).
|
|
4664
|
+
*
|
|
4665
|
+
* @return {boolean}
|
|
4666
|
+
*/
|
|
4667
|
+
boardFilterMenu() {
|
|
4668
|
+
return this.isBoardLook && this.filterMenu && !this.hasSidebar && this.filterableFields.length > 0
|
|
4669
|
+
},
|
|
4670
|
+
|
|
4671
|
+
/**
|
|
4672
|
+
* Whether the funnel menu sits in the table header: `filterMenu` with
|
|
4673
|
+
* values to offer, unless the toolbar's Filter button is that menu.
|
|
4674
|
+
*
|
|
4675
|
+
* @return {boolean}
|
|
4676
|
+
*/
|
|
4677
|
+
showHeaderFilterMenu() {
|
|
4678
|
+
return this.filterMenu && this.filterableFields.length > 0 && !this.boardFilterMenu
|
|
4679
|
+
},
|
|
4680
|
+
|
|
4654
4681
|
/** Search props forwarded to the embedded CnIndexSidebar (defaults applied per CnIndexSidebar). */
|
|
4655
4682
|
sidebarSearchProps() {
|
|
4656
4683
|
return (this.sidebar && this.sidebar.search) || {}
|