@conduction/nextcloud-vue 2.78.0 → 2.80.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/bundled-packages.json +0 -2
- package/dist/esm/bundled-packages.json +0 -2
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +3 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +32 -3
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +115 -104
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +12 -2
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +39 -3
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +158 -0
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +157 -96
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +139 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +140 -92
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +77 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +5 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +62 -4
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +85 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +43 -4
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +11 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +736 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +238 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/index.js +30 -0
- package/dist/esm/components/CnFactsWidget/index.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +10 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +551 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +183 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/index.js +29 -0
- package/dist/esm/components/CnFieldListWidget/index.js.map +1 -0
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +173 -5
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +181 -99
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js +29 -11
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/openRegisterSource.js +252 -0
- package/dist/esm/components/CnFilesBrowser/openRegisterSource.js.map +1 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +66 -18
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +4 -4
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +18 -3
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +330 -74
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +34 -15
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +10 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js.map +1 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +99 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +48 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +100 -3
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -2
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +29 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +3 -2
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +10 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +387 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +169 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/index.js +31 -0
- package/dist/esm/components/CnRecordListWidget/index.js.map +1 -0
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +10 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +351 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +71 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/index.js +35 -0
- package/dist/esm/components/CnSectionsWidget/index.js.map +1 -0
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +81 -2
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +1 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +152 -8
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +84 -23
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
- 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/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +106 -2
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +105 -53
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
- package/dist/esm/composables/useAppManifest.js +8 -3
- package/dist/esm/composables/useAppManifest.js.map +1 -1
- package/dist/esm/composables/useFormDraft.js +140 -9
- package/dist/esm/composables/useFormDraft.js.map +1 -1
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +28 -20
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js +9 -0
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/field-inspection.js +21 -0
- package/dist/esm/integrations/builtin/field-inspection.js.map +1 -1
- package/dist/esm/integrations/offline/checklistSubmission.js +141 -0
- package/dist/esm/integrations/offline/checklistSubmission.js.map +1 -0
- package/dist/esm/integrations/offline/fieldCollectionHelpers.js +70 -5
- package/dist/esm/integrations/offline/fieldCollectionHelpers.js.map +1 -1
- package/dist/esm/integrations/offline/index.js +2 -1
- package/dist/esm/integrations/offline/index.js.map +1 -1
- package/dist/esm/integrations/offline/offlineDb.js +6 -1
- package/dist/esm/integrations/offline/offlineDb.js.map +1 -1
- package/dist/esm/integrations/offline/syncReplayService.js +8 -2
- package/dist/esm/integrations/offline/syncReplayService.js.map +1 -1
- package/dist/esm/l10n/en.json.js +72 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +120 -2
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +1314 -74
- package/dist/esm/utils/boardDate.js +193 -0
- package/dist/esm/utils/boardDate.js.map +1 -0
- package/dist/esm/utils/detailActionModel.js +60 -3
- package/dist/esm/utils/detailActionModel.js.map +1 -1
- package/dist/esm/utils/dynamicProperties.js +5 -1
- package/dist/esm/utils/dynamicProperties.js.map +1 -1
- package/dist/esm/utils/fillTemplate.js +114 -0
- package/dist/esm/utils/fillTemplate.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +4 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/queryPresets.js +96 -0
- package/dist/esm/utils/queryPresets.js.map +1 -0
- package/dist/esm/utils/resolveFilterTokens.js +11 -0
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/schema.js +21 -0
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +3 -3
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +57 -6
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/widgetDispatch.js +7 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +14278 -8169
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1314 -74
- package/l10n/en.json +72 -1
- package/l10n/nl.json +120 -2
- package/package.json +1 -1
- package/scripts/build-validators.js +26 -0
- package/src/components/CnActionButtons/CnActionButtons.vue +2 -0
- package/src/components/CnActionsBar/CnActionsBar.md +4 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +83 -42
- package/src/components/CnAppRoot/CnAppRoot.vue +12 -2
- package/src/components/CnBannerWidget/CnBannerWidget.vue +39 -3
- package/src/components/CnCellRenderer/CnCellRenderer.vue +306 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +219 -4
- package/src/components/CnDataTable/CnDataTable.md +2 -0
- package/src/components/CnDataTable/CnDataTable.vue +82 -2
- package/src/components/CnDetailPage/CnDetailPage.md +1 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +63 -3
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +127 -1
- package/src/components/CnFactsWidget/CnFactsWidget.vue +1050 -0
- package/src/components/CnFactsWidget/index.js +29 -0
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +746 -0
- package/src/components/CnFieldListWidget/index.js +28 -0
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +232 -11
- package/src/components/CnFilesBrowser/filesBrowser.js +43 -2
- package/src/components/CnFilesBrowser/index.js +1 -0
- package/src/components/CnFilesBrowser/openRegisterSource.js +248 -0
- package/src/components/CnFormDialog/CnFormDialog.md +9 -3
- package/src/components/CnFormDialog/CnFormDialog.vue +70 -22
- package/src/components/CnFormPage/CnFormPage.vue +18 -3
- package/src/components/CnIndexPage/CnIndexPage.vue +281 -9
- package/src/components/CnIndexPage/CnIndexPageWidgets.vue +169 -0
- package/src/components/CnObjectSidebar/CnFilesTab.vue +103 -4
- package/src/components/CnPageRenderer/CnPageRenderer.vue +29 -1
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +3 -2
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +603 -0
- package/src/components/CnRecordListWidget/index.js +30 -0
- package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +1 -1
- package/src/components/CnSectionsWidget/CnSectionsWidget.vue +489 -0
- package/src/components/CnSectionsWidget/index.js +34 -0
- package/src/components/CnStagesWidget/CnStagesWidget.vue +81 -2
- package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
- package/src/components/CnTabsWidget/CnTabsWidget.vue +214 -9
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
- package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +188 -4
- package/src/components/index.js +5 -1
- package/src/composables/__tests__/useFormDraft.spec.js +69 -0
- package/src/composables/useAppManifest.js +8 -3
- package/src/composables/useFormDraft.js +139 -8
- package/src/css/form-field.css +14 -0
- package/src/css/index-page.css +71 -0
- package/src/css/kpi-card.css +32 -0
- package/src/css/look-board-detail.css +87 -0
- package/src/css/look-board-index.css +143 -14
- package/src/css/look-board.css +114 -2
- package/src/css/table.css +56 -0
- package/src/index.js +8 -0
- package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +35 -20
- package/src/integrations/builtin/field-inspection.js +21 -0
- package/src/integrations/offline/checklistSubmission.js +137 -0
- package/src/integrations/offline/fieldCollectionHelpers.js +69 -4
- package/src/integrations/offline/index.js +3 -0
- package/src/integrations/offline/offlineDb.js +6 -1
- package/src/integrations/offline/syncReplayService.js +8 -2
- package/src/schemas/app-manifest-v2.schema.json +149 -5
- package/src/utils/boardDate.js +189 -0
- package/src/utils/detailActionModel.js +59 -2
- package/src/utils/dynamicProperties.js +5 -1
- package/src/utils/fillTemplate.js +110 -0
- package/src/utils/libraryWidgetKeys.js +4 -0
- package/src/utils/queryPresets.js +93 -0
- package/src/utils/resolveFilterTokens.js +11 -0
- package/src/utils/schema.js +21 -0
- package/src/utils/sentinelTokens.js +3 -3
- package/src/utils/validateManifestV2.compiled.js +57 -2
- package/src/utils/widgetDispatch.js +7 -1
- package/dist/esm/_virtual/equal.js +0 -4
- package/dist/esm/_virtual/equal.js.map +0 -1
- package/dist/esm/_virtual/ucs2length.js +0 -4
- package/dist/esm/_virtual/ucs2length.js.map +0 -1
- package/dist/esm/node_modules/ajv/dist/runtime/equal.js +0 -9
- package/dist/esm/node_modules/ajv/dist/runtime/equal.js.map +0 -1
- package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js +0 -25
- package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js.map +0 -1
- package/dist/esm/node_modules/fast-deep-equal/index.js +0 -47
- package/dist/esm/node_modules/fast-deep-equal/index.js.map +0 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* A data source for CnFilesBrowser that reads and writes one OpenRegister
|
|
6
|
+
* object's folder through OpenRegister's own files API, not WebDAV.
|
|
7
|
+
*
|
|
8
|
+
* Why: an object's files live in the `openregister` account's home, outside
|
|
9
|
+
* every person's Files app, so a reader has no WebDAV path to them. Every call
|
|
10
|
+
* here goes through the object's access rule on the server, as the signed-in
|
|
11
|
+
* person: a listing needs read on the object, a change needs update. Nothing
|
|
12
|
+
* is shared to make the folder visible.
|
|
13
|
+
*
|
|
14
|
+
* Paths are relative to the object folder and written with a leading slash
|
|
15
|
+
* (`/`, `/Bijlagen`), the same shape the WebDAV browser uses for its own
|
|
16
|
+
* paths, so crumbs and navigation work unchanged.
|
|
17
|
+
*
|
|
18
|
+
* @spec openspec/changes/files-browser-openregister-source/specs/files-browser/spec.md
|
|
19
|
+
*/
|
|
20
|
+
import axios from '@nextcloud/axios'
|
|
21
|
+
import { FileType } from '@nextcloud/files'
|
|
22
|
+
import { generateUrl } from '@nextcloud/router'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A browser path (`/a/b`) as the API's relative path (`a/b`).
|
|
26
|
+
*
|
|
27
|
+
* @param {string} path The browser path.
|
|
28
|
+
* @return {string} The relative path, empty for the root.
|
|
29
|
+
*/
|
|
30
|
+
export function toApiPath(path) {
|
|
31
|
+
return String(path || '').replace(/^\/+|\/+$/g, '')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One API listing entry as a row the browser can render and sort.
|
|
36
|
+
*
|
|
37
|
+
* The shape follows what the browser reads off an `@nextcloud/files` node
|
|
38
|
+
* (basename, type, mime, size, mtime, fileid, path, source, attributes), so
|
|
39
|
+
* the table, the sorter and the icons need no second code path.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} entry The API entry: `{ id, name, type, mimetype, size, mtime, path }`.
|
|
42
|
+
* @return {object} The row.
|
|
43
|
+
*/
|
|
44
|
+
export function entryToNode(entry) {
|
|
45
|
+
const isFolder = entry.type === 'folder'
|
|
46
|
+
return {
|
|
47
|
+
source: `openregister:${entry.id}`,
|
|
48
|
+
fileid: entry.id,
|
|
49
|
+
basename: entry.name,
|
|
50
|
+
displayname: entry.name,
|
|
51
|
+
path: `/${toApiPath(entry.path)}`,
|
|
52
|
+
type: isFolder ? FileType.Folder : FileType.File,
|
|
53
|
+
mime: isFolder ? 'httpd/unix-directory' : (entry.mimetype || ''),
|
|
54
|
+
size: typeof entry.size === 'number' ? entry.size : undefined,
|
|
55
|
+
mtime: typeof entry.mtime === 'number' ? new Date(entry.mtime * 1000) : undefined,
|
|
56
|
+
attributes: {},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The status of a failed request, or null.
|
|
62
|
+
*
|
|
63
|
+
* @param {Error} err The error.
|
|
64
|
+
* @return {number|null} The HTTP status.
|
|
65
|
+
*/
|
|
66
|
+
function statusOf(err) {
|
|
67
|
+
return err?.response?.status ?? err?.status ?? null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The server's own message on a failed request, or null.
|
|
72
|
+
*
|
|
73
|
+
* @param {Error} err The error.
|
|
74
|
+
* @return {string|null} The message.
|
|
75
|
+
*/
|
|
76
|
+
function messageOf(err) {
|
|
77
|
+
const data = err?.response?.data
|
|
78
|
+
return (data && typeof data.error === 'string' && data.error !== '') ? data.error : null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build the OpenRegister source for one object.
|
|
83
|
+
*
|
|
84
|
+
* @param {object} options The object.
|
|
85
|
+
* @param {string} [options.apiBase] OpenRegister's API base, `/apps/openregister/api`.
|
|
86
|
+
* @param {string} options.register The register slug or id.
|
|
87
|
+
* @param {string} options.schema The schema slug or id.
|
|
88
|
+
* @param {string} options.objectId The object's uuid.
|
|
89
|
+
* @return {object} The source CnFilesBrowser takes as `source`.
|
|
90
|
+
*/
|
|
91
|
+
export function createOpenRegisterSource({ apiBase = '/apps/openregister/api', register, schema, objectId }) {
|
|
92
|
+
const objectUrl = (suffix) => generateUrl(`${apiBase}/objects/{register}/{schema}/{objectId}${suffix}`, { register, schema, objectId })
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Re-throw a failed request with its status and the server's message.
|
|
96
|
+
*
|
|
97
|
+
* @param {Error} err The error.
|
|
98
|
+
* @return {never}
|
|
99
|
+
*/
|
|
100
|
+
const fail = (err) => {
|
|
101
|
+
const error = new Error(messageOf(err) || err?.message || 'Request failed')
|
|
102
|
+
error.status = statusOf(err)
|
|
103
|
+
throw error
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
kind: 'openregister',
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* List a folder.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} path The browser path.
|
|
113
|
+
* @return {Promise<{folder: object, nodes: Array<object>}>} The folder (with `canChange`) and its rows.
|
|
114
|
+
*/
|
|
115
|
+
async list(path) {
|
|
116
|
+
try {
|
|
117
|
+
const { data } = await axios.get(objectUrl('/folder'), { params: { path: toApiPath(path) } })
|
|
118
|
+
if (!Array.isArray(data?.entries)) {
|
|
119
|
+
// Not a folder listing: an OpenRegister without these endpoints.
|
|
120
|
+
const error = new Error('This OpenRegister has no folder endpoint')
|
|
121
|
+
error.status = 501
|
|
122
|
+
throw error
|
|
123
|
+
}
|
|
124
|
+
const relative = toApiPath(data?.path ?? path)
|
|
125
|
+
return {
|
|
126
|
+
folder: {
|
|
127
|
+
fileid: data?.folderId ?? null,
|
|
128
|
+
path: `/${relative}`,
|
|
129
|
+
basename: relative.split('/').pop() || '',
|
|
130
|
+
type: FileType.Folder,
|
|
131
|
+
canChange: data?.canChange === true,
|
|
132
|
+
},
|
|
133
|
+
nodes: data.entries.map(entryToNode),
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (err?.status && !err.response) {
|
|
137
|
+
throw err
|
|
138
|
+
}
|
|
139
|
+
return fail(err)
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create a folder.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} path Where, as a browser path.
|
|
147
|
+
* @param {string} name The new folder's name.
|
|
148
|
+
* @return {Promise<object>} The new folder's row.
|
|
149
|
+
*/
|
|
150
|
+
async createFolder(path, name) {
|
|
151
|
+
try {
|
|
152
|
+
const { data } = await axios.post(objectUrl('/folder'), { path: toApiPath(path), name })
|
|
153
|
+
return entryToNode(data)
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return fail(err)
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Upload one file into a folder.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} path The target folder, as a browser path.
|
|
163
|
+
* @param {File} file The file.
|
|
164
|
+
* @param {(percent: number) => void} [onProgress] Called with 0..100.
|
|
165
|
+
* @return {Promise<object>} The stored file's row.
|
|
166
|
+
*/
|
|
167
|
+
async upload(path, file, onProgress) {
|
|
168
|
+
const form = new FormData()
|
|
169
|
+
form.append('path', toApiPath(path))
|
|
170
|
+
form.append('files[]', file, file.name)
|
|
171
|
+
try {
|
|
172
|
+
const { data } = await axios.post(objectUrl('/folder/upload'), form, {
|
|
173
|
+
onUploadProgress: (progress) => {
|
|
174
|
+
if (progress?.total && typeof onProgress === 'function') {
|
|
175
|
+
onProgress(Math.round((progress.loaded / progress.total) * 100))
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
const stored = Array.isArray(data?.stored) ? data.stored : []
|
|
180
|
+
if (stored.length === 0) {
|
|
181
|
+
const error = new Error(data?.rejected?.[0]?.error || 'The upload failed')
|
|
182
|
+
error.status = 400
|
|
183
|
+
throw error
|
|
184
|
+
}
|
|
185
|
+
return entryToNode(stored[0])
|
|
186
|
+
} catch (err) {
|
|
187
|
+
if (err?.status && !err.response) {
|
|
188
|
+
throw err
|
|
189
|
+
}
|
|
190
|
+
return fail(err)
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Rename a file or folder.
|
|
196
|
+
*
|
|
197
|
+
* @param {object} node The row.
|
|
198
|
+
* @param {string} name The new name.
|
|
199
|
+
* @return {Promise<object>} The renamed row.
|
|
200
|
+
*/
|
|
201
|
+
async rename(node, name) {
|
|
202
|
+
try {
|
|
203
|
+
const { data } = await axios.put(objectUrl(`/folder/${encodeURIComponent(node.fileid)}`), { name })
|
|
204
|
+
return entryToNode(data)
|
|
205
|
+
} catch (err) {
|
|
206
|
+
return fail(err)
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Delete a file or folder.
|
|
212
|
+
*
|
|
213
|
+
* @param {object} node The row.
|
|
214
|
+
* @return {Promise<void>}
|
|
215
|
+
*/
|
|
216
|
+
async remove(node) {
|
|
217
|
+
try {
|
|
218
|
+
await axios.delete(objectUrl(`/folder/${encodeURIComponent(node.fileid)}`))
|
|
219
|
+
} catch (err) {
|
|
220
|
+
fail(err)
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Where a file downloads from: the object's own file endpoint, which
|
|
226
|
+
* checks read on the object.
|
|
227
|
+
*
|
|
228
|
+
* @param {object} node The row.
|
|
229
|
+
* @return {string|null} The url, or null for a folder.
|
|
230
|
+
*/
|
|
231
|
+
downloadUrl(node) {
|
|
232
|
+
if (!node?.fileid || node.type === FileType.Folder) {
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
return objectUrl(`/files/${encodeURIComponent(node.fileid)}`)
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A small preview of an image, through the object's preview endpoint.
|
|
240
|
+
*
|
|
241
|
+
* @param {object} node The row.
|
|
242
|
+
* @return {string} The url.
|
|
243
|
+
*/
|
|
244
|
+
previewUrl(node) {
|
|
245
|
+
return objectUrl(`/files/${encodeURIComponent(node.fileid)}/preview?width=64&height=64`)
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -113,7 +113,7 @@ export default {
|
|
|
113
113
|
| `allowDraft` | `false` | Offer a **Save draft** button that stores the record with a draft marker instead of validating it. Inert unless the schema declares `draftField`. |
|
|
114
114
|
| `draftField` | `'isDraft'` | The boolean property that marks a record as a draft. |
|
|
115
115
|
| `draftAppId` | `''` | The app the draft belongs to, for the storage key. |
|
|
116
|
-
| `draftUserId` | `''` | Who is typing, for the storage key.
|
|
116
|
+
| `draftUserId` | `''` | Who is typing, for the storage key. Empty: the signed-in Nextcloud user, and `anonymous` only when nobody is signed in. |
|
|
117
117
|
|
|
118
118
|
#### What the draft does and does not do
|
|
119
119
|
|
|
@@ -122,9 +122,15 @@ Restore and Discard; the values go in only when somebody presses Restore. A form
|
|
|
122
122
|
that filled itself would be indistinguishable from one the server prefilled, and
|
|
123
123
|
somebody would submit last week's answers without knowing they were there.
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
The user is part of the storage key. A shared browser profile at a service
|
|
126
126
|
desk is the ordinary case in a municipality, and a draft keyed without the user
|
|
127
|
-
hands the next person at the counter what the last one typed.
|
|
127
|
+
hands the next person at the counter what the last one typed. Without
|
|
128
|
+
`draftUserId` the dialog uses the signed-in Nextcloud user. A draft stored
|
|
129
|
+
under `anonymous` is removed, never offered, once somebody is signed in.
|
|
130
|
+
|
|
131
|
+
A draft is written only after a person changes a field, and only while that
|
|
132
|
+
field differs from the value the form opened with. Opening a dialog, or the
|
|
133
|
+
form filling in values by itself, writes nothing and shows no Saving or Saved.
|
|
128
134
|
|
|
129
135
|
A draft is dropped rather than offered when it is more than seven days old, when
|
|
130
136
|
the record has been saved by somebody else since it was written, or when the
|
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
:error="!!errors[field.key]"
|
|
187
187
|
:type="field.widget === 'email' ? 'email' : field.widget === 'url' ? 'url' : 'text'"
|
|
188
188
|
:disabled="field.readOnly"
|
|
189
|
-
:placeholder="field
|
|
189
|
+
:placeholder="fieldPlaceholder(field)"
|
|
190
190
|
@update:modelValue="value => updateField(field.key, value)" />
|
|
191
191
|
<CnFieldHelper
|
|
192
192
|
:id="helpId(field)"
|
|
@@ -208,7 +208,7 @@
|
|
|
208
208
|
:error="!!errors[field.key]"
|
|
209
209
|
type="number"
|
|
210
210
|
:disabled="field.readOnly"
|
|
211
|
-
:placeholder="field
|
|
211
|
+
:placeholder="fieldPlaceholder(field)"
|
|
212
212
|
@update:modelValue="value => updateField(field.key, value !== '' ? Number(value) : null)" />
|
|
213
213
|
<CnFieldHelper
|
|
214
214
|
:id="helpId(field)"
|
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
v-bind="fieldAria(field)"
|
|
231
231
|
:value="formData[field.key] || ''"
|
|
232
232
|
:disabled="field.readOnly"
|
|
233
|
-
:placeholder="field
|
|
233
|
+
:placeholder="fieldPlaceholder(field)"
|
|
234
234
|
rows="4"
|
|
235
235
|
@input="updateField(field.key, $event.target.value)" />
|
|
236
236
|
<CnFieldHelper
|
|
@@ -652,7 +652,7 @@
|
|
|
652
652
|
:modelValue="formData[field.key] != null ? String(formData[field.key]) : ''"
|
|
653
653
|
:error="!!errors[field.key]"
|
|
654
654
|
:disabled="field.readOnly"
|
|
655
|
-
:placeholder="field
|
|
655
|
+
:placeholder="fieldPlaceholder(field)"
|
|
656
656
|
@update:modelValue="value => updateField(field.key, value)" />
|
|
657
657
|
<CnFieldHelper
|
|
658
658
|
:id="helpId(field)"
|
|
@@ -787,7 +787,7 @@ import CnPropertySourceField from '../CnPropertySourceField/CnPropertySourceFiel
|
|
|
787
787
|
import CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'
|
|
788
788
|
import CnSubObjectsField from '../CnSubObjectsField/CnSubObjectsField.vue'
|
|
789
789
|
import { describeRowProblem, useChildRecords } from '../../composables/useChildRecords.js'
|
|
790
|
-
import { draftIndicatorText, draftKey, formDraftMixin, readDraft } from '../../composables/useFormDraft.js'
|
|
790
|
+
import { clearAnonymousDrafts, draftIndicatorText, draftKey, formDraftMixin, hasDraftChanges, readDraft, resolveDraftUserId } from '../../composables/useFormDraft.js'
|
|
791
791
|
import { heldFromFailures, splitHeldFiles, uploadHeldFiles } from '../../composables/useHeldFileUpload.js'
|
|
792
792
|
import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
|
|
793
793
|
import { TENANT_CONTEXT_KEY } from '../../composables/useTenantContext.js'
|
|
@@ -1172,16 +1172,16 @@ export default {
|
|
|
1172
1172
|
* 🔴 THE USER IS PART OF THE DRAFT KEY. A shared browser profile at a
|
|
1173
1173
|
* service desk is the ordinary case in a municipality, and a draft
|
|
1174
1174
|
* keyed without the user hands the next person at the counter what the
|
|
1175
|
-
* last one typed. A host that passes nothing gets
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1175
|
+
* last one typed. A host that passes nothing gets the signed-in
|
|
1176
|
+
* Nextcloud user (`@nextcloud/auth`), and `anonymous` only when
|
|
1177
|
+
* nobody is signed in.
|
|
1178
1178
|
*/
|
|
1179
1179
|
draftAppId: {
|
|
1180
1180
|
type: String,
|
|
1181
1181
|
default: '',
|
|
1182
1182
|
},
|
|
1183
1183
|
|
|
1184
|
-
/** Who is typing, for the draft key. See `draftAppId`. */
|
|
1184
|
+
/** Who is typing, for the draft key. Empty: the signed-in user. See `draftAppId`. */
|
|
1185
1185
|
draftUserId: {
|
|
1186
1186
|
type: String,
|
|
1187
1187
|
default: '',
|
|
@@ -1442,10 +1442,20 @@ export default {
|
|
|
1442
1442
|
appId: this.draftAppId,
|
|
1443
1443
|
schema: (this.schema?.slug || this.schema?.title || ''),
|
|
1444
1444
|
objectId: (this.item?.id || this.item?.uuid || ''),
|
|
1445
|
-
userId: this.
|
|
1445
|
+
userId: this.draftUserOwner,
|
|
1446
1446
|
})
|
|
1447
1447
|
},
|
|
1448
1448
|
|
|
1449
|
+
/**
|
|
1450
|
+
* Who the draft belongs to: `draftUserId`, else the signed-in user.
|
|
1451
|
+
*
|
|
1452
|
+
* @return {string} The user id, or '' when nobody is signed in.
|
|
1453
|
+
* @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
|
|
1454
|
+
*/
|
|
1455
|
+
draftUserOwner() {
|
|
1456
|
+
return resolveDraftUserId(this.draftUserId)
|
|
1457
|
+
},
|
|
1458
|
+
|
|
1449
1459
|
/**
|
|
1450
1460
|
* Whether "Save draft" may be offered.
|
|
1451
1461
|
*
|
|
@@ -1757,17 +1767,6 @@ export default {
|
|
|
1757
1767
|
formData: {
|
|
1758
1768
|
deep: true,
|
|
1759
1769
|
handler(values) {
|
|
1760
|
-
// 🔴 THE FIRST CHANGE IS THE COMPONENT SEEDING THE FORM, NOT
|
|
1761
|
-
// SOMEBODY TYPING. Writing a draft for it stores the empty
|
|
1762
|
-
// form over a real one, and the indicator announces "Saving
|
|
1763
|
-
// draft" about work nobody did — an instrument reporting
|
|
1764
|
-
// activity that did not happen, which is the whole class of
|
|
1765
|
-
// defect this library keeps paying down.
|
|
1766
|
-
if (this.draftSeeded !== true) {
|
|
1767
|
-
this.draftSeeded = true
|
|
1768
|
-
return
|
|
1769
|
-
}
|
|
1770
|
-
|
|
1771
1770
|
// Nothing is written while a draft is being offered: the user
|
|
1772
1771
|
// has not chosen yet, and overwriting the stored draft with the
|
|
1773
1772
|
// empty form they are looking at would destroy the thing they
|
|
@@ -1776,6 +1775,19 @@ export default {
|
|
|
1776
1775
|
return
|
|
1777
1776
|
}
|
|
1778
1777
|
|
|
1778
|
+
// 🔴 ONLY A PERSON'S CHANGE IS WORTH A DRAFT. The form fills
|
|
1779
|
+
// in values by itself after it opens (an input reporting its
|
|
1780
|
+
// value back, normalised dates, defaults, the organisation),
|
|
1781
|
+
// and each of those used to write a draft and say "Saved just
|
|
1782
|
+
// now" on a dialog nobody had touched. A field counts once a
|
|
1783
|
+
// person edited it AND it differs from what the form opened
|
|
1784
|
+
// with, so undoing the edit drops the draft again.
|
|
1785
|
+
const edited = Object.keys(this.touchedFields || {}).filter((key) => this.touchedFields[key])
|
|
1786
|
+
if (!hasDraftChanges(values, this.draftBaseline, edited)) {
|
|
1787
|
+
this.settleUnchangedDraft(this.formDraftKey)
|
|
1788
|
+
return
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1779
1791
|
this.scheduleDraftWrite(this.formDraftKey, values)
|
|
1780
1792
|
},
|
|
1781
1793
|
},
|
|
@@ -1940,6 +1952,12 @@ export default {
|
|
|
1940
1952
|
// Silently refilling a form is how somebody submits last week's answers
|
|
1941
1953
|
// without noticing they were there, and a form that fills itself is
|
|
1942
1954
|
// indistinguishable from one the server prefilled.
|
|
1955
|
+
// A draft stored under `anonymous` was written by a page that did not
|
|
1956
|
+
// know who was typing. Once somebody is signed in it is removed, never
|
|
1957
|
+
// offered: it may be another person's.
|
|
1958
|
+
if (this.formDraftKey && this.draftUserOwner) {
|
|
1959
|
+
clearAnonymousDrafts()
|
|
1960
|
+
}
|
|
1943
1961
|
if (this.formDraftKey) {
|
|
1944
1962
|
this.draftOffer = readDraft(this.formDraftKey, {
|
|
1945
1963
|
objectUpdated: (this.item?.updated || this.item?.['@self']?.updated || ''),
|
|
@@ -2298,6 +2316,10 @@ export default {
|
|
|
2298
2316
|
this.runAssignments(null)
|
|
2299
2317
|
this.loadProfileDefaults()
|
|
2300
2318
|
}
|
|
2319
|
+
// What the form opened with. A draft is written only once a field
|
|
2320
|
+
// a person edited differs from this (see the `formData` watcher).
|
|
2321
|
+
// Not reactive: nothing renders from it.
|
|
2322
|
+
this.draftBaseline = JSON.parse(JSON.stringify(this.formData || {}))
|
|
2301
2323
|
},
|
|
2302
2324
|
|
|
2303
2325
|
/**
|
|
@@ -3085,6 +3107,27 @@ export default {
|
|
|
3085
3107
|
return this.showsOuterLabel(field) ? '' : this.errors[field.key]
|
|
3086
3108
|
},
|
|
3087
3109
|
|
|
3110
|
+
/**
|
|
3111
|
+
* The placeholder of a text, number or textarea control.
|
|
3112
|
+
*
|
|
3113
|
+
* The field's description already shows as the help line under the
|
|
3114
|
+
* control, so the placeholder stays empty rather than repeat it word
|
|
3115
|
+
* for word. The help line is the one kept: it is still there after
|
|
3116
|
+
* somebody typed (a placeholder disappears with the first keystroke),
|
|
3117
|
+
* it is read out through `aria-describedby`, and in the Nextcloud look
|
|
3118
|
+
* a placeholder is hidden until the field has focus anyway. While an
|
|
3119
|
+
* error takes the help line's place, the description returns as the
|
|
3120
|
+
* placeholder so it is not lost.
|
|
3121
|
+
*
|
|
3122
|
+
* @param {object} field The field definition.
|
|
3123
|
+
* @return {string} The placeholder, or '' when the help line says it.
|
|
3124
|
+
* @spec openspec/changes/form-draft-on-change-and-help-text-spacing/specs/form-draft-on-change-and-help-text-spacing/spec.md#requirement-a-field-description-shows-once
|
|
3125
|
+
*/
|
|
3126
|
+
fieldPlaceholder(field) {
|
|
3127
|
+
const description = field.description || ''
|
|
3128
|
+
return this.helperError(field) ? description : ''
|
|
3129
|
+
},
|
|
3130
|
+
|
|
3088
3131
|
/**
|
|
3089
3132
|
* The accessibility attributes of a text control. In both looks an invalid
|
|
3090
3133
|
* control is `aria-invalid` and `aria-describedby` lists the error before
|
|
@@ -3891,7 +3934,7 @@ export default {
|
|
|
3891
3934
|
const mappedProps = propertiesFromDefinitions(
|
|
3892
3935
|
Array.isArray(records) ? records : [],
|
|
3893
3936
|
config,
|
|
3894
|
-
{ orderFrom: 1000 + Object.keys(properties).length },
|
|
3937
|
+
{ orderFrom: 1000 + Object.keys(properties).length, create: this.isCreateMode },
|
|
3895
3938
|
)
|
|
3896
3939
|
Object.assign(properties, mappedProps.properties)
|
|
3897
3940
|
required.push(...mappedProps.required)
|
|
@@ -4567,6 +4610,11 @@ export default {
|
|
|
4567
4610
|
}
|
|
4568
4611
|
|
|
4569
4612
|
this.formData = { ...this.formData, ...this.draftOffer.values }
|
|
4613
|
+
// What the person typed last time is their edit now: it counts for
|
|
4614
|
+
// the next draft write and survives the record arriving late.
|
|
4615
|
+
for (const key of Object.keys(this.draftOffer.values)) {
|
|
4616
|
+
this.touchedFields[key] = true
|
|
4617
|
+
}
|
|
4570
4618
|
this.draftOffer = null
|
|
4571
4619
|
},
|
|
4572
4620
|
|
|
@@ -397,7 +397,7 @@ import CnFormErrorSummary from '../CnFormErrorSummary/CnFormErrorSummary.vue'
|
|
|
397
397
|
import CnFormField from '../CnFormField/CnFormField.vue'
|
|
398
398
|
import CnStepper from '../CnStepper/CnStepper.vue'
|
|
399
399
|
import { cnRenderFormField } from '../../composables/cnFormFieldRenderer.js'
|
|
400
|
-
import { draftIndicatorText, draftKey, formDraftMixin, readDraft } from '../../composables/useFormDraft.js'
|
|
400
|
+
import { clearAnonymousDrafts, draftIndicatorText, draftKey, formDraftMixin, readDraft, resolveDraftUserId } from '../../composables/useFormDraft.js'
|
|
401
401
|
import { normalizeLook } from '../../composables/useLook.js'
|
|
402
402
|
import { cnSelectAria } from '../../directives/cnSelectAria.js'
|
|
403
403
|
import { loadCurrentUserProfile } from '../../utils/currentUserProfile.js'
|
|
@@ -529,7 +529,7 @@ export default {
|
|
|
529
529
|
default: '',
|
|
530
530
|
},
|
|
531
531
|
|
|
532
|
-
/** Who is typing; part of the draft's storage key so a shared browser profile never leaks a draft between users. */
|
|
532
|
+
/** 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. */
|
|
533
533
|
draftUserId: {
|
|
534
534
|
type: String,
|
|
535
535
|
default: '',
|
|
@@ -949,7 +949,17 @@ export default {
|
|
|
949
949
|
if (this.recoverDraft !== true) {
|
|
950
950
|
return ''
|
|
951
951
|
}
|
|
952
|
-
return draftKey({ appId: this.draftAppId, schema: this.draftScope, objectId: 'new', userId: this.
|
|
952
|
+
return draftKey({ appId: this.draftAppId, schema: this.draftScope, objectId: 'new', userId: this.draftUserOwner })
|
|
953
|
+
},
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* Who the draft belongs to: `draftUserId`, else the signed-in user.
|
|
957
|
+
*
|
|
958
|
+
* @return {string} The user id, or '' when nobody is signed in.
|
|
959
|
+
* @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
|
|
960
|
+
*/
|
|
961
|
+
draftUserOwner() {
|
|
962
|
+
return resolveDraftUserId(this.draftUserId)
|
|
953
963
|
},
|
|
954
964
|
|
|
955
965
|
/**
|
|
@@ -997,6 +1007,11 @@ export default {
|
|
|
997
1007
|
this.calcTimers = {}
|
|
998
1008
|
// Offered, never applied: a form that fills itself is indistinguishable
|
|
999
1009
|
// from one the server prefilled.
|
|
1010
|
+
// A draft under `anonymous` may be somebody else's: removed once a
|
|
1011
|
+
// signed-in user is known, never offered.
|
|
1012
|
+
if (this.formDraftKey && this.draftUserOwner) {
|
|
1013
|
+
clearAnonymousDrafts()
|
|
1014
|
+
}
|
|
1000
1015
|
if (this.formDraftKey) {
|
|
1001
1016
|
this.draftOffer = readDraft(this.formDraftKey)
|
|
1002
1017
|
}
|