@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"filesBrowser.js","sources":["../../../../src/components/CnFilesBrowser/filesBrowser.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * The pure half of CnFilesBrowser: paths, crumbs and the folder lookup.\n *\n * Kept out of the component so a test can pin them without a DAV server:\n * a wrong path here sends every listing to the wrong folder, silently.\n */\nimport axios from '@nextcloud/axios'\nimport { generateUrl } from '@nextcloud/router'\n\n/**\n * Actions the Files app registers that cannot run on a page without the Files\n * app's own list: the details action opens the Files sidebar, which on\n * Nextcloud 34 is a store bound to the Files router and node list; the\n * folder-navigation actions route through that list; and rename turns the\n * list's own name cell into an input, which is not here. Verified on a case\n * page: Rename ran and nothing happened. The browser offers its own rename\n * instead, a dialog over a DAV move.\n *\n * @type {string[]}\n */\nexport const ACTIONS_NEEDING_THE_FILES_PAGE = ['details', 'sidebar', 'open-folder', 'view-in-folder', 'rename']\n\n/**\n * A DAV href for a node under the user's files root, as a path relative to\n * that root: `/remote.php/dav/files/admin/Open%20Registers/x/` becomes\n * `/Open Registers/x`.\n *\n * @param {string} href The href as the DAV server returned it.\n * @param {string} uid The current user's id.\n * @return {string} The user-relative path without a trailing slash, `/` at least.\n */\nexport function userRelativePathFromHref(href, uid) {\n\tlet decoded\n\ttry {\n\t\tdecoded = decodeURIComponent(String(href || ''))\n\t} catch {\n\t\tdecoded = String(href || '')\n\t}\n\tconst marker = `/dav/files/${uid}`\n\tconst at = decoded.indexOf(marker)\n\tconst rest = at === -1 ? decoded : decoded.slice(at + marker.length)\n\tconst trimmed = rest.replace(/\\/+$/, '')\n\treturn trimmed === '' ? '/' : trimmed\n}\n\n/**\n * Join a directory and a name into a path, with exactly one slash between.\n *\n * @param {string} dir The directory path.\n * @param {string} name The child name.\n * @return {string} The child's path.\n */\nexport function joinPath(dir, name) {\n\treturn `${String(dir || '').replace(/\\/+$/, '')}/${String(name || '').replace(/^\\/+/, '')}`\n}\n\n/**\n * The breadcrumb trail: every folder from the user's files root down to the\n * open folder, the way the Files app draws it.\n *\n * The folders ABOVE the browser's root are part of the trail too (a reader\n * wants to see where this folder lives), but the browser never navigates\n * above its root, so those crumbs carry `aboveRoot: true` and the component\n * links them into the Files app instead. The root reads `rootLabel` when the\n * caller gave one and its own basename otherwise; every other crumb is the\n * folder's basename.\n *\n * @param {string} rootPath The browser's root, user-relative.\n * @param {string} currentPath The open folder, user-relative, at or below the root.\n * @param {string|null} [rootLabel] What the root crumb reads; null for its basename.\n * @return {Array<{name: string, path: string, aboveRoot: boolean}>} The crumbs, outermost first.\n */\nexport function crumbsFor(rootPath, currentPath, rootLabel = null) {\n\tconst root = String(rootPath || '/').replace(/\\/+$/, '') || '/'\n\tconst current = String(currentPath || root).replace(/\\/+$/, '') || '/'\n\tconst crumbs = []\n\tlet path = ''\n\tfor (const segment of root.split('/').filter(Boolean)) {\n\t\tpath = joinPath(path, segment)\n\t\tcrumbs.push({ name: segment, path, aboveRoot: path !== root })\n\t}\n\tif (root === '/') {\n\t\tcrumbs.push({ name: rootLabel || '/', path: '/', aboveRoot: false })\n\t} else if (rootLabel) {\n\t\tcrumbs[crumbs.length - 1].name = rootLabel\n\t}\n\tif (current === root || !current.startsWith(root === '/' ? '/' : `${root}/`)) {\n\t\treturn crumbs\n\t}\n\tconst below = root === '/' ? current.slice(1) : current.slice(root.length + 1)\n\tpath = root\n\tfor (const segment of below.split('/').filter(Boolean)) {\n\t\tpath = joinPath(path, segment)\n\t\tcrumbs.push({ name: segment, path, aboveRoot: false })\n\t}\n\treturn crumbs\n}\n\n/**\n * The DAV SEARCH body that finds one node by its file id under the user's root.\n *\n * @param {number|string} fileId The Nextcloud file id.\n * @param {string} uid The current user's id.\n * @return {string} The request body.\n */\nexport function fileIdSearchBody(fileId, uid) {\n\tconst id = String(fileId).replace(/[^0-9]/g, '')\n\tconst scope = `/files/${uid}`.replace(/[<>&]/g, '')\n\treturn '<?xml version=\"1.0\"?>'\n\t\t+ '<d:searchrequest xmlns:d=\"DAV:\" xmlns:oc=\"http://owncloud.org/ns\"><d:basicsearch>'\n\t\t+ '<d:select><d:prop><oc:fileid/></d:prop></d:select>'\n\t\t+ `<d:from><d:scope><d:href>${scope}</d:href><d:depth>infinity</d:depth></d:scope></d:from>`\n\t\t+ `<d:where><d:eq><d:prop><oc:fileid/></d:prop><d:literal>${id}</d:literal></d:eq></d:where>`\n\t\t+ '</d:basicsearch></d:searchrequest>'\n}\n\n/**\n * The first `d:href` in a DAV multistatus body, or null.\n *\n * @param {string} xml The response body.\n * @return {string|null} The href.\n */\nexport function firstHrefIn(xml) {\n\ttry {\n\t\tconst doc = new DOMParser().parseFromString(String(xml || ''), 'application/xml')\n\t\tconst href = doc.getElementsByTagNameNS('DAV:', 'href')[0]\n\t\treturn href ? href.textContent : null\n\t} catch {\n\t\treturn null\n\t}\n}\n\n/**\n * Where an OpenRegister object keeps its files, as a user-relative path.\n *\n * The object carries the folder's file id in `@self.folder`; a DAV search\n * by that id turns it into a path the DAV client can list. Null when the\n * object has no folder, or the search cannot see it (a user the folder is\n * not shared with), in which case the caller falls back to the object's\n * files endpoint.\n *\n * @param {object} options The lookup.\n * @param {string} options.apiBase OpenRegister's API base, `/apps/openregister/api`.\n * @param {string} options.register The register slug.\n * @param {string} options.schema The schema slug.\n * @param {string} options.objectId The object's uuid.\n * @param {string} options.uid The current user's id.\n * @param {string} options.remoteUrl The DAV remote url, `…/remote.php/dav`.\n * @return {Promise<string|null>} The user-relative folder path, or null.\n */\nexport async function resolveObjectFolder({ apiBase, register, schema, objectId, uid, remoteUrl }) {\n\tif (!register || !schema || !objectId || !uid) {\n\t\treturn null\n\t}\n\tlet folderId\n\ttry {\n\t\tconst { data } = await axios.get(generateUrl(`${apiBase}/objects/{register}/{schema}/{objectId}`, { register, schema, objectId }))\n\t\tfolderId = data?.['@self']?.folder ?? null\n\t} catch {\n\t\treturn null\n\t}\n\tif (folderId === null || folderId === '' || Number.isNaN(Number(folderId))) {\n\t\treturn null\n\t}\n\ttry {\n\t\tconst response = await axios.request({\n\t\t\tmethod: 'SEARCH',\n\t\t\turl: `${remoteUrl.replace(/\\/+$/, '')}/`,\n\t\t\theaders: { 'Content-Type': 'application/xml' },\n\t\t\tdata: fileIdSearchBody(folderId, uid),\n\t\t\tresponseType: 'text',\n\t\t})\n\t\tconst href = firstHrefIn(response.data)\n\t\treturn href === null ? null : userRelativePathFromHref(href, uid)\n\t} catch {\n\t\treturn null\n\t}\n}\n"],"names":["axios"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"filesBrowser.js","sources":["../../../../src/components/CnFilesBrowser/filesBrowser.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * The pure half of CnFilesBrowser: paths, crumbs and the folder lookup.\n *\n * Kept out of the component so a test can pin them without a DAV server:\n * a wrong path here sends every listing to the wrong folder, silently.\n */\nimport axios from '@nextcloud/axios'\nimport { generateUrl } from '@nextcloud/router'\n\n/**\n * Actions the Files app registers that cannot run on a page without the Files\n * app's own list: the details action opens the Files sidebar, which on\n * Nextcloud 34 is a store bound to the Files router and node list; the\n * folder-navigation actions route through that list; and rename turns the\n * list's own name cell into an input, which is not here. Verified on a case\n * page: Rename ran and nothing happened. The browser offers its own rename\n * instead, a dialog over a DAV move.\n *\n * @type {string[]}\n */\nexport const ACTIONS_NEEDING_THE_FILES_PAGE = ['details', 'sidebar', 'open-folder', 'view-in-folder', 'rename']\n\n/**\n * A DAV href for a node under the user's files root, as a path relative to\n * that root: `/remote.php/dav/files/admin/Open%20Registers/x/` becomes\n * `/Open Registers/x`.\n *\n * @param {string} href The href as the DAV server returned it.\n * @param {string} uid The current user's id.\n * @return {string} The user-relative path without a trailing slash, `/` at least.\n */\nexport function userRelativePathFromHref(href, uid) {\n\tlet decoded\n\ttry {\n\t\tdecoded = decodeURIComponent(String(href || ''))\n\t} catch {\n\t\tdecoded = String(href || '')\n\t}\n\tconst marker = `/dav/files/${uid}`\n\tconst at = decoded.indexOf(marker)\n\tconst rest = at === -1 ? decoded : decoded.slice(at + marker.length)\n\tconst trimmed = rest.replace(/\\/+$/, '')\n\treturn trimmed === '' ? '/' : trimmed\n}\n\n/**\n * Join a directory and a name into a path, with exactly one slash between.\n *\n * @param {string} dir The directory path.\n * @param {string} name The child name.\n * @return {string} The child's path.\n */\nexport function joinPath(dir, name) {\n\treturn `${String(dir || '').replace(/\\/+$/, '')}/${String(name || '').replace(/^\\/+/, '')}`\n}\n\n/**\n * The breadcrumb trail: every folder from the user's files root down to the\n * open folder, the way the Files app draws it.\n *\n * The folders ABOVE the browser's root are part of the trail too (a reader\n * wants to see where this folder lives), but the browser never navigates\n * above its root, so those crumbs carry `aboveRoot: true` and the component\n * links them into the Files app instead. The root reads `rootLabel` when the\n * caller gave one and its own basename otherwise; every other crumb is the\n * folder's basename.\n *\n * @param {string} rootPath The browser's root, user-relative.\n * @param {string} currentPath The open folder, user-relative, at or below the root.\n * @param {string|null} [rootLabel] What the root crumb reads; null for its basename.\n * @return {Array<{name: string, path: string, aboveRoot: boolean}>} The crumbs, outermost first.\n */\nexport function crumbsFor(rootPath, currentPath, rootLabel = null) {\n\tconst root = String(rootPath || '/').replace(/\\/+$/, '') || '/'\n\tconst current = String(currentPath || root).replace(/\\/+$/, '') || '/'\n\tconst crumbs = []\n\tlet path = ''\n\tfor (const segment of root.split('/').filter(Boolean)) {\n\t\tpath = joinPath(path, segment)\n\t\tcrumbs.push({ name: segment, path, aboveRoot: path !== root })\n\t}\n\tif (root === '/') {\n\t\tcrumbs.push({ name: rootLabel || '/', path: '/', aboveRoot: false })\n\t} else if (rootLabel) {\n\t\tcrumbs[crumbs.length - 1].name = rootLabel\n\t}\n\tif (current === root || !current.startsWith(root === '/' ? '/' : `${root}/`)) {\n\t\treturn crumbs\n\t}\n\tconst below = root === '/' ? current.slice(1) : current.slice(root.length + 1)\n\tpath = root\n\tfor (const segment of below.split('/').filter(Boolean)) {\n\t\tpath = joinPath(path, segment)\n\t\tcrumbs.push({ name: segment, path, aboveRoot: false })\n\t}\n\treturn crumbs\n}\n\n/**\n * The DAV SEARCH body that finds one node by its file id under the user's root.\n *\n * @param {number|string} fileId The Nextcloud file id.\n * @param {string} uid The current user's id.\n * @return {string} The request body.\n */\nexport function fileIdSearchBody(fileId, uid) {\n\tconst id = String(fileId).replace(/[^0-9]/g, '')\n\tconst scope = `/files/${uid}`.replace(/[<>&]/g, '')\n\treturn '<?xml version=\"1.0\"?>'\n\t\t+ '<d:searchrequest xmlns:d=\"DAV:\" xmlns:oc=\"http://owncloud.org/ns\"><d:basicsearch>'\n\t\t+ '<d:select><d:prop><oc:fileid/></d:prop></d:select>'\n\t\t+ `<d:from><d:scope><d:href>${scope}</d:href><d:depth>infinity</d:depth></d:scope></d:from>`\n\t\t+ `<d:where><d:eq><d:prop><oc:fileid/></d:prop><d:literal>${id}</d:literal></d:eq></d:where>`\n\t\t+ '</d:basicsearch></d:searchrequest>'\n}\n\n/**\n * The first `d:href` in a DAV multistatus body, or null.\n *\n * @param {string} xml The response body.\n * @return {string|null} The href.\n */\nexport function firstHrefIn(xml) {\n\ttry {\n\t\tconst doc = new DOMParser().parseFromString(String(xml || ''), 'application/xml')\n\t\tconst href = doc.getElementsByTagNameNS('DAV:', 'href')[0]\n\t\treturn href ? href.textContent : null\n\t} catch {\n\t\treturn null\n\t}\n}\n\n/**\n * Where an OpenRegister object keeps its files, as a user-relative path.\n *\n * The object carries the folder's file id in `@self.folder`; a DAV search\n * by that id turns it into a path the DAV client can list. Null when the\n * object has no folder, or the search cannot see it (a user the folder is\n * not shared with), in which case the caller falls back to the object's\n * files endpoint.\n *\n * @param {object} options The lookup.\n * @param {string} options.apiBase OpenRegister's API base, `/apps/openregister/api`.\n * @param {string} options.register The register slug.\n * @param {string} options.schema The schema slug.\n * @param {string} options.objectId The object's uuid.\n * @param {string} options.uid The current user's id.\n * @param {string} options.remoteUrl The DAV remote url, `…/remote.php/dav`.\n * @return {Promise<string|null>} The user-relative folder path, or null.\n */\nexport async function resolveObjectFolder(options) {\n\tconst info = await resolveObjectFolderInfo(options)\n\treturn info === null ? null : info.path\n}\n\n/**\n * The object's display name: `@self.name`, else `title`, else `name`. A\n * value equal to the object's uuid is no name (the folder already reads so).\n *\n * @param {object|null} object The object as OpenRegister returns it.\n * @param {string} objectId The object's uuid.\n * @return {string|null} The name, or null.\n * @spec openspec/changes/dutch-library-strings-and-files-crumb/specs/dutch-library-strings-and-files-crumb/spec.md#requirement-the-files-browser-names-the-object-in-its-root-crumb\n */\nexport function objectDisplayName(object, objectId) {\n\tconst candidates = [object?.['@self']?.name, object?.title, object?.name]\n\tfor (const candidate of candidates) {\n\t\tif (typeof candidate === 'string' && candidate.trim() !== '' && candidate.trim() !== String(objectId)) {\n\t\t\treturn candidate.trim()\n\t\t}\n\t}\n\treturn null\n}\n\n/**\n * `resolveObjectFolder`, plus the object's display name from the same read,\n * so the browser can name the root crumb after the object instead of the\n * uuid its folder is called on disk.\n *\n * @param {object} options The lookup, as for `resolveObjectFolder`.\n * @param {string} options.apiBase OpenRegister's API base, `/apps/openregister/api`.\n * @param {string} options.register The register slug.\n * @param {string} options.schema The schema slug.\n * @param {string} options.objectId The object's uuid.\n * @param {string} options.uid The current user's id.\n * @param {string} options.remoteUrl The DAV remote url, `…/remote.php/dav`.\n * @return {Promise<{path: string, name: (string|null)}|null>} The folder and the object's name, or null.\n * @spec openspec/changes/dutch-library-strings-and-files-crumb/specs/dutch-library-strings-and-files-crumb/spec.md#requirement-the-files-browser-names-the-object-in-its-root-crumb\n */\nexport async function resolveObjectFolderInfo({ apiBase, register, schema, objectId, uid, remoteUrl }) {\n\tif (!register || !schema || !objectId || !uid) {\n\t\treturn null\n\t}\n\tlet folderId\n\tlet name\n\ttry {\n\t\tconst { data } = await axios.get(generateUrl(`${apiBase}/objects/{register}/{schema}/{objectId}`, { register, schema, objectId }))\n\t\tfolderId = data?.['@self']?.folder ?? null\n\t\tname = objectDisplayName(data, objectId)\n\t} catch {\n\t\treturn null\n\t}\n\tif (folderId === null || folderId === '' || Number.isNaN(Number(folderId))) {\n\t\treturn null\n\t}\n\ttry {\n\t\tconst response = await axios.request({\n\t\t\tmethod: 'SEARCH',\n\t\t\turl: `${remoteUrl.replace(/\\/+$/, '')}/`,\n\t\t\theaders: { 'Content-Type': 'application/xml' },\n\t\t\tdata: fileIdSearchBody(folderId, uid),\n\t\t\tresponseType: 'text',\n\t\t})\n\t\tconst href = firstHrefIn(response.data)\n\t\treturn href === null ? null : { path: userRelativePathFromHref(href, uid), name }\n\t} catch {\n\t\treturn null\n\t}\n}\n"],"names":["axios"],"mappings":"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"}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import axios__default from '@nextcloud/axios';
|
|
2
|
+
import { FileType } from '@nextcloud/files';
|
|
3
|
+
import { generateUrl } from '@nextcloud/router';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
7
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
8
|
+
*
|
|
9
|
+
* A data source for CnFilesBrowser that reads and writes one OpenRegister
|
|
10
|
+
* object's folder through OpenRegister's own files API, not WebDAV.
|
|
11
|
+
*
|
|
12
|
+
* Why: an object's files live in the `openregister` account's home, outside
|
|
13
|
+
* every person's Files app, so a reader has no WebDAV path to them. Every call
|
|
14
|
+
* here goes through the object's access rule on the server, as the signed-in
|
|
15
|
+
* person: a listing needs read on the object, a change needs update. Nothing
|
|
16
|
+
* is shared to make the folder visible.
|
|
17
|
+
*
|
|
18
|
+
* Paths are relative to the object folder and written with a leading slash
|
|
19
|
+
* (`/`, `/Bijlagen`), the same shape the WebDAV browser uses for its own
|
|
20
|
+
* paths, so crumbs and navigation work unchanged.
|
|
21
|
+
*
|
|
22
|
+
* @spec openspec/changes/files-browser-openregister-source/specs/files-browser/spec.md
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A browser path (`/a/b`) as the API's relative path (`a/b`).
|
|
27
|
+
*
|
|
28
|
+
* @param {string} path The browser path.
|
|
29
|
+
* @return {string} The relative path, empty for the root.
|
|
30
|
+
*/
|
|
31
|
+
function toApiPath(path) {
|
|
32
|
+
return String(path || '').replace(/^\/+|\/+$/g, '')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One API listing entry as a row the browser can render and sort.
|
|
37
|
+
*
|
|
38
|
+
* The shape follows what the browser reads off an `@nextcloud/files` node
|
|
39
|
+
* (basename, type, mime, size, mtime, fileid, path, source, attributes), so
|
|
40
|
+
* the table, the sorter and the icons need no second code path.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} entry The API entry: `{ id, name, type, mimetype, size, mtime, path }`.
|
|
43
|
+
* @return {object} The row.
|
|
44
|
+
*/
|
|
45
|
+
function entryToNode(entry) {
|
|
46
|
+
const isFolder = entry.type === 'folder';
|
|
47
|
+
return {
|
|
48
|
+
source: `openregister:${entry.id}`,
|
|
49
|
+
fileid: entry.id,
|
|
50
|
+
basename: entry.name,
|
|
51
|
+
displayname: entry.name,
|
|
52
|
+
path: `/${toApiPath(entry.path)}`,
|
|
53
|
+
type: isFolder ? FileType.Folder : FileType.File,
|
|
54
|
+
mime: isFolder ? 'httpd/unix-directory' : (entry.mimetype || ''),
|
|
55
|
+
size: typeof entry.size === 'number' ? entry.size : undefined,
|
|
56
|
+
mtime: typeof entry.mtime === 'number' ? new Date(entry.mtime * 1000) : undefined,
|
|
57
|
+
attributes: {},
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The status of a failed request, or null.
|
|
63
|
+
*
|
|
64
|
+
* @param {Error} err The error.
|
|
65
|
+
* @return {number|null} The HTTP status.
|
|
66
|
+
*/
|
|
67
|
+
function statusOf(err) {
|
|
68
|
+
return err?.response?.status ?? err?.status ?? null
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The server's own message on a failed request, or null.
|
|
73
|
+
*
|
|
74
|
+
* @param {Error} err The error.
|
|
75
|
+
* @return {string|null} The message.
|
|
76
|
+
*/
|
|
77
|
+
function messageOf(err) {
|
|
78
|
+
const data = err?.response?.data;
|
|
79
|
+
return (data && typeof data.error === 'string' && data.error !== '') ? data.error : null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build the OpenRegister source for one object.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} options The object.
|
|
86
|
+
* @param {string} [options.apiBase] OpenRegister's API base, `/apps/openregister/api`.
|
|
87
|
+
* @param {string} options.register The register slug or id.
|
|
88
|
+
* @param {string} options.schema The schema slug or id.
|
|
89
|
+
* @param {string} options.objectId The object's uuid.
|
|
90
|
+
* @return {object} The source CnFilesBrowser takes as `source`.
|
|
91
|
+
*/
|
|
92
|
+
function createOpenRegisterSource({ apiBase = '/apps/openregister/api', register, schema, objectId }) {
|
|
93
|
+
const objectUrl = (suffix) => generateUrl(`${apiBase}/objects/{register}/{schema}/{objectId}${suffix}`, { register, schema, objectId });
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Re-throw a failed request with its status and the server's message.
|
|
97
|
+
*
|
|
98
|
+
* @param {Error} err The error.
|
|
99
|
+
* @return {never}
|
|
100
|
+
*/
|
|
101
|
+
const fail = (err) => {
|
|
102
|
+
const error = new Error(messageOf(err) || err?.message || 'Request failed');
|
|
103
|
+
error.status = statusOf(err);
|
|
104
|
+
throw error
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
kind: 'openregister',
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* List a folder.
|
|
112
|
+
*
|
|
113
|
+
* @param {string} path The browser path.
|
|
114
|
+
* @return {Promise<{folder: object, nodes: Array<object>}>} The folder (with `canChange`) and its rows.
|
|
115
|
+
*/
|
|
116
|
+
async list(path) {
|
|
117
|
+
try {
|
|
118
|
+
const { data } = await axios__default.get(objectUrl('/folder'), { params: { path: toApiPath(path) } });
|
|
119
|
+
if (!Array.isArray(data?.entries)) {
|
|
120
|
+
// Not a folder listing: an OpenRegister without these endpoints.
|
|
121
|
+
const error = new Error('This OpenRegister has no folder endpoint');
|
|
122
|
+
error.status = 501;
|
|
123
|
+
throw error
|
|
124
|
+
}
|
|
125
|
+
const relative = toApiPath(data?.path ?? path);
|
|
126
|
+
return {
|
|
127
|
+
folder: {
|
|
128
|
+
fileid: data?.folderId ?? null,
|
|
129
|
+
path: `/${relative}`,
|
|
130
|
+
basename: relative.split('/').pop() || '',
|
|
131
|
+
type: FileType.Folder,
|
|
132
|
+
canChange: data?.canChange === true,
|
|
133
|
+
},
|
|
134
|
+
nodes: data.entries.map(entryToNode),
|
|
135
|
+
}
|
|
136
|
+
} catch (err) {
|
|
137
|
+
if (err?.status && !err.response) {
|
|
138
|
+
throw err
|
|
139
|
+
}
|
|
140
|
+
return fail(err)
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Create a folder.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} path Where, as a browser path.
|
|
148
|
+
* @param {string} name The new folder's name.
|
|
149
|
+
* @return {Promise<object>} The new folder's row.
|
|
150
|
+
*/
|
|
151
|
+
async createFolder(path, name) {
|
|
152
|
+
try {
|
|
153
|
+
const { data } = await axios__default.post(objectUrl('/folder'), { path: toApiPath(path), name });
|
|
154
|
+
return entryToNode(data)
|
|
155
|
+
} catch (err) {
|
|
156
|
+
return fail(err)
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Upload one file into a folder.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} path The target folder, as a browser path.
|
|
164
|
+
* @param {File} file The file.
|
|
165
|
+
* @param {(percent: number) => void} [onProgress] Called with 0..100.
|
|
166
|
+
* @return {Promise<object>} The stored file's row.
|
|
167
|
+
*/
|
|
168
|
+
async upload(path, file, onProgress) {
|
|
169
|
+
const form = new FormData();
|
|
170
|
+
form.append('path', toApiPath(path));
|
|
171
|
+
form.append('files[]', file, file.name);
|
|
172
|
+
try {
|
|
173
|
+
const { data } = await axios__default.post(objectUrl('/folder/upload'), form, {
|
|
174
|
+
onUploadProgress: (progress) => {
|
|
175
|
+
if (progress?.total && typeof onProgress === 'function') {
|
|
176
|
+
onProgress(Math.round((progress.loaded / progress.total) * 100));
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
const stored = Array.isArray(data?.stored) ? data.stored : [];
|
|
181
|
+
if (stored.length === 0) {
|
|
182
|
+
const error = new Error(data?.rejected?.[0]?.error || 'The upload failed');
|
|
183
|
+
error.status = 400;
|
|
184
|
+
throw error
|
|
185
|
+
}
|
|
186
|
+
return entryToNode(stored[0])
|
|
187
|
+
} catch (err) {
|
|
188
|
+
if (err?.status && !err.response) {
|
|
189
|
+
throw err
|
|
190
|
+
}
|
|
191
|
+
return fail(err)
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Rename a file or folder.
|
|
197
|
+
*
|
|
198
|
+
* @param {object} node The row.
|
|
199
|
+
* @param {string} name The new name.
|
|
200
|
+
* @return {Promise<object>} The renamed row.
|
|
201
|
+
*/
|
|
202
|
+
async rename(node, name) {
|
|
203
|
+
try {
|
|
204
|
+
const { data } = await axios__default.put(objectUrl(`/folder/${encodeURIComponent(node.fileid)}`), { name });
|
|
205
|
+
return entryToNode(data)
|
|
206
|
+
} catch (err) {
|
|
207
|
+
return fail(err)
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Delete a file or folder.
|
|
213
|
+
*
|
|
214
|
+
* @param {object} node The row.
|
|
215
|
+
* @return {Promise<void>}
|
|
216
|
+
*/
|
|
217
|
+
async remove(node) {
|
|
218
|
+
try {
|
|
219
|
+
await axios__default.delete(objectUrl(`/folder/${encodeURIComponent(node.fileid)}`));
|
|
220
|
+
} catch (err) {
|
|
221
|
+
fail(err);
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Where a file downloads from: the object's own file endpoint, which
|
|
227
|
+
* checks read on the object.
|
|
228
|
+
*
|
|
229
|
+
* @param {object} node The row.
|
|
230
|
+
* @return {string|null} The url, or null for a folder.
|
|
231
|
+
*/
|
|
232
|
+
downloadUrl(node) {
|
|
233
|
+
if (!node?.fileid || node.type === FileType.Folder) {
|
|
234
|
+
return null
|
|
235
|
+
}
|
|
236
|
+
return objectUrl(`/files/${encodeURIComponent(node.fileid)}`)
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* A small preview of an image, through the object's preview endpoint.
|
|
241
|
+
*
|
|
242
|
+
* @param {object} node The row.
|
|
243
|
+
* @return {string} The url.
|
|
244
|
+
*/
|
|
245
|
+
previewUrl(node) {
|
|
246
|
+
return objectUrl(`/files/${encodeURIComponent(node.fileid)}/preview?width=64&height=64`)
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export { createOpenRegisterSource, entryToNode, toApiPath };
|
|
252
|
+
//# sourceMappingURL=openRegisterSource.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openRegisterSource.js","sources":["../../../../src/components/CnFilesBrowser/openRegisterSource.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * A data source for CnFilesBrowser that reads and writes one OpenRegister\n * object's folder through OpenRegister's own files API, not WebDAV.\n *\n * Why: an object's files live in the `openregister` account's home, outside\n * every person's Files app, so a reader has no WebDAV path to them. Every call\n * here goes through the object's access rule on the server, as the signed-in\n * person: a listing needs read on the object, a change needs update. Nothing\n * is shared to make the folder visible.\n *\n * Paths are relative to the object folder and written with a leading slash\n * (`/`, `/Bijlagen`), the same shape the WebDAV browser uses for its own\n * paths, so crumbs and navigation work unchanged.\n *\n * @spec openspec/changes/files-browser-openregister-source/specs/files-browser/spec.md\n */\nimport axios from '@nextcloud/axios'\nimport { FileType } from '@nextcloud/files'\nimport { generateUrl } from '@nextcloud/router'\n\n/**\n * A browser path (`/a/b`) as the API's relative path (`a/b`).\n *\n * @param {string} path The browser path.\n * @return {string} The relative path, empty for the root.\n */\nexport function toApiPath(path) {\n\treturn String(path || '').replace(/^\\/+|\\/+$/g, '')\n}\n\n/**\n * One API listing entry as a row the browser can render and sort.\n *\n * The shape follows what the browser reads off an `@nextcloud/files` node\n * (basename, type, mime, size, mtime, fileid, path, source, attributes), so\n * the table, the sorter and the icons need no second code path.\n *\n * @param {object} entry The API entry: `{ id, name, type, mimetype, size, mtime, path }`.\n * @return {object} The row.\n */\nexport function entryToNode(entry) {\n\tconst isFolder = entry.type === 'folder'\n\treturn {\n\t\tsource: `openregister:${entry.id}`,\n\t\tfileid: entry.id,\n\t\tbasename: entry.name,\n\t\tdisplayname: entry.name,\n\t\tpath: `/${toApiPath(entry.path)}`,\n\t\ttype: isFolder ? FileType.Folder : FileType.File,\n\t\tmime: isFolder ? 'httpd/unix-directory' : (entry.mimetype || ''),\n\t\tsize: typeof entry.size === 'number' ? entry.size : undefined,\n\t\tmtime: typeof entry.mtime === 'number' ? new Date(entry.mtime * 1000) : undefined,\n\t\tattributes: {},\n\t}\n}\n\n/**\n * The status of a failed request, or null.\n *\n * @param {Error} err The error.\n * @return {number|null} The HTTP status.\n */\nfunction statusOf(err) {\n\treturn err?.response?.status ?? err?.status ?? null\n}\n\n/**\n * The server's own message on a failed request, or null.\n *\n * @param {Error} err The error.\n * @return {string|null} The message.\n */\nfunction messageOf(err) {\n\tconst data = err?.response?.data\n\treturn (data && typeof data.error === 'string' && data.error !== '') ? data.error : null\n}\n\n/**\n * Build the OpenRegister source for one object.\n *\n * @param {object} options The object.\n * @param {string} [options.apiBase] OpenRegister's API base, `/apps/openregister/api`.\n * @param {string} options.register The register slug or id.\n * @param {string} options.schema The schema slug or id.\n * @param {string} options.objectId The object's uuid.\n * @return {object} The source CnFilesBrowser takes as `source`.\n */\nexport function createOpenRegisterSource({ apiBase = '/apps/openregister/api', register, schema, objectId }) {\n\tconst objectUrl = (suffix) => generateUrl(`${apiBase}/objects/{register}/{schema}/{objectId}${suffix}`, { register, schema, objectId })\n\n\t/**\n\t * Re-throw a failed request with its status and the server's message.\n\t *\n\t * @param {Error} err The error.\n\t * @return {never}\n\t */\n\tconst fail = (err) => {\n\t\tconst error = new Error(messageOf(err) || err?.message || 'Request failed')\n\t\terror.status = statusOf(err)\n\t\tthrow error\n\t}\n\n\treturn {\n\t\tkind: 'openregister',\n\n\t\t/**\n\t\t * List a folder.\n\t\t *\n\t\t * @param {string} path The browser path.\n\t\t * @return {Promise<{folder: object, nodes: Array<object>}>} The folder (with `canChange`) and its rows.\n\t\t */\n\t\tasync list(path) {\n\t\t\ttry {\n\t\t\t\tconst { data } = await axios.get(objectUrl('/folder'), { params: { path: toApiPath(path) } })\n\t\t\t\tif (!Array.isArray(data?.entries)) {\n\t\t\t\t\t// Not a folder listing: an OpenRegister without these endpoints.\n\t\t\t\t\tconst error = new Error('This OpenRegister has no folder endpoint')\n\t\t\t\t\terror.status = 501\n\t\t\t\t\tthrow error\n\t\t\t\t}\n\t\t\t\tconst relative = toApiPath(data?.path ?? path)\n\t\t\t\treturn {\n\t\t\t\t\tfolder: {\n\t\t\t\t\t\tfileid: data?.folderId ?? null,\n\t\t\t\t\t\tpath: `/${relative}`,\n\t\t\t\t\t\tbasename: relative.split('/').pop() || '',\n\t\t\t\t\t\ttype: FileType.Folder,\n\t\t\t\t\t\tcanChange: data?.canChange === true,\n\t\t\t\t\t},\n\t\t\t\t\tnodes: data.entries.map(entryToNode),\n\t\t\t\t}\n\t\t\t} catch (err) {\n\t\t\t\tif (err?.status && !err.response) {\n\t\t\t\t\tthrow err\n\t\t\t\t}\n\t\t\t\treturn fail(err)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Create a folder.\n\t\t *\n\t\t * @param {string} path Where, as a browser path.\n\t\t * @param {string} name The new folder's name.\n\t\t * @return {Promise<object>} The new folder's row.\n\t\t */\n\t\tasync createFolder(path, name) {\n\t\t\ttry {\n\t\t\t\tconst { data } = await axios.post(objectUrl('/folder'), { path: toApiPath(path), name })\n\t\t\t\treturn entryToNode(data)\n\t\t\t} catch (err) {\n\t\t\t\treturn fail(err)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Upload one file into a folder.\n\t\t *\n\t\t * @param {string} path The target folder, as a browser path.\n\t\t * @param {File} file The file.\n\t\t * @param {(percent: number) => void} [onProgress] Called with 0..100.\n\t\t * @return {Promise<object>} The stored file's row.\n\t\t */\n\t\tasync upload(path, file, onProgress) {\n\t\t\tconst form = new FormData()\n\t\t\tform.append('path', toApiPath(path))\n\t\t\tform.append('files[]', file, file.name)\n\t\t\ttry {\n\t\t\t\tconst { data } = await axios.post(objectUrl('/folder/upload'), form, {\n\t\t\t\t\tonUploadProgress: (progress) => {\n\t\t\t\t\t\tif (progress?.total && typeof onProgress === 'function') {\n\t\t\t\t\t\t\tonProgress(Math.round((progress.loaded / progress.total) * 100))\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\tconst stored = Array.isArray(data?.stored) ? data.stored : []\n\t\t\t\tif (stored.length === 0) {\n\t\t\t\t\tconst error = new Error(data?.rejected?.[0]?.error || 'The upload failed')\n\t\t\t\t\terror.status = 400\n\t\t\t\t\tthrow error\n\t\t\t\t}\n\t\t\t\treturn entryToNode(stored[0])\n\t\t\t} catch (err) {\n\t\t\t\tif (err?.status && !err.response) {\n\t\t\t\t\tthrow err\n\t\t\t\t}\n\t\t\t\treturn fail(err)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Rename a file or folder.\n\t\t *\n\t\t * @param {object} node The row.\n\t\t * @param {string} name The new name.\n\t\t * @return {Promise<object>} The renamed row.\n\t\t */\n\t\tasync rename(node, name) {\n\t\t\ttry {\n\t\t\t\tconst { data } = await axios.put(objectUrl(`/folder/${encodeURIComponent(node.fileid)}`), { name })\n\t\t\t\treturn entryToNode(data)\n\t\t\t} catch (err) {\n\t\t\t\treturn fail(err)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Delete a file or folder.\n\t\t *\n\t\t * @param {object} node The row.\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync remove(node) {\n\t\t\ttry {\n\t\t\t\tawait axios.delete(objectUrl(`/folder/${encodeURIComponent(node.fileid)}`))\n\t\t\t} catch (err) {\n\t\t\t\tfail(err)\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Where a file downloads from: the object's own file endpoint, which\n\t\t * checks read on the object.\n\t\t *\n\t\t * @param {object} node The row.\n\t\t * @return {string|null} The url, or null for a folder.\n\t\t */\n\t\tdownloadUrl(node) {\n\t\t\tif (!node?.fileid || node.type === FileType.Folder) {\n\t\t\t\treturn null\n\t\t\t}\n\t\t\treturn objectUrl(`/files/${encodeURIComponent(node.fileid)}`)\n\t\t},\n\n\t\t/**\n\t\t * A small preview of an image, through the object's preview endpoint.\n\t\t *\n\t\t * @param {object} node The row.\n\t\t * @return {string} The url.\n\t\t */\n\t\tpreviewUrl(node) {\n\t\t\treturn objectUrl(`/files/${encodeURIComponent(node.fileid)}/preview?width=64&height=64`)\n\t\t},\n\t}\n}\n"],"names":["axios"],"mappings":"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"}
|
|
@@ -17,7 +17,7 @@ import __sfc_main$2 from '../CnPropertySourceField/CnPropertySourceField.vue.js'
|
|
|
17
17
|
import __sfc_main$a from '../CnResourceSelect/CnResourceSelect.vue.js';
|
|
18
18
|
import __sfc_main$4 from '../CnSubObjectsField/CnSubObjectsField.vue.js';
|
|
19
19
|
import { describeRowProblem, useChildRecords } from '../../composables/useChildRecords.js';
|
|
20
|
-
import { formDraftMixin, draftKey, draftIndicatorText, readDraft } from '../../composables/useFormDraft.js';
|
|
20
|
+
import { formDraftMixin, draftKey, resolveDraftUserId, draftIndicatorText, hasDraftChanges, clearAnonymousDrafts, readDraft } from '../../composables/useFormDraft.js';
|
|
21
21
|
import { splitHeldFiles, uploadHeldFiles, heldFromFailures } from '../../composables/useHeldFileUpload.js';
|
|
22
22
|
import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js';
|
|
23
23
|
import { TENANT_CONTEXT_KEY } from '../../composables/useTenantContext.js';
|
|
@@ -393,16 +393,16 @@ var script = {
|
|
|
393
393
|
* 🔴 THE USER IS PART OF THE DRAFT KEY. A shared browser profile at a
|
|
394
394
|
* service desk is the ordinary case in a municipality, and a draft
|
|
395
395
|
* keyed without the user hands the next person at the counter what the
|
|
396
|
-
* last one typed. A host that passes nothing gets
|
|
397
|
-
*
|
|
398
|
-
*
|
|
396
|
+
* last one typed. A host that passes nothing gets the signed-in
|
|
397
|
+
* Nextcloud user (`@nextcloud/auth`), and `anonymous` only when
|
|
398
|
+
* nobody is signed in.
|
|
399
399
|
*/
|
|
400
400
|
draftAppId: {
|
|
401
401
|
type: String,
|
|
402
402
|
default: '',
|
|
403
403
|
},
|
|
404
404
|
|
|
405
|
-
/** Who is typing, for the draft key. See `draftAppId`. */
|
|
405
|
+
/** Who is typing, for the draft key. Empty: the signed-in user. See `draftAppId`. */
|
|
406
406
|
draftUserId: {
|
|
407
407
|
type: String,
|
|
408
408
|
default: '',
|
|
@@ -663,10 +663,20 @@ var script = {
|
|
|
663
663
|
appId: this.draftAppId,
|
|
664
664
|
schema: (this.schema?.slug || this.schema?.title || ''),
|
|
665
665
|
objectId: (this.item?.id || this.item?.uuid || ''),
|
|
666
|
-
userId: this.
|
|
666
|
+
userId: this.draftUserOwner,
|
|
667
667
|
})
|
|
668
668
|
},
|
|
669
669
|
|
|
670
|
+
/**
|
|
671
|
+
* Who the draft belongs to: `draftUserId`, else the signed-in user.
|
|
672
|
+
*
|
|
673
|
+
* @return {string} The user id, or '' when nobody is signed in.
|
|
674
|
+
* @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
|
|
675
|
+
*/
|
|
676
|
+
draftUserOwner() {
|
|
677
|
+
return resolveDraftUserId(this.draftUserId)
|
|
678
|
+
},
|
|
679
|
+
|
|
670
680
|
/**
|
|
671
681
|
* Whether "Save draft" may be offered.
|
|
672
682
|
*
|
|
@@ -978,17 +988,6 @@ var script = {
|
|
|
978
988
|
formData: {
|
|
979
989
|
deep: true,
|
|
980
990
|
handler(values) {
|
|
981
|
-
// 🔴 THE FIRST CHANGE IS THE COMPONENT SEEDING THE FORM, NOT
|
|
982
|
-
// SOMEBODY TYPING. Writing a draft for it stores the empty
|
|
983
|
-
// form over a real one, and the indicator announces "Saving
|
|
984
|
-
// draft" about work nobody did — an instrument reporting
|
|
985
|
-
// activity that did not happen, which is the whole class of
|
|
986
|
-
// defect this library keeps paying down.
|
|
987
|
-
if (this.draftSeeded !== true) {
|
|
988
|
-
this.draftSeeded = true;
|
|
989
|
-
return
|
|
990
|
-
}
|
|
991
|
-
|
|
992
991
|
// Nothing is written while a draft is being offered: the user
|
|
993
992
|
// has not chosen yet, and overwriting the stored draft with the
|
|
994
993
|
// empty form they are looking at would destroy the thing they
|
|
@@ -997,6 +996,19 @@ var script = {
|
|
|
997
996
|
return
|
|
998
997
|
}
|
|
999
998
|
|
|
999
|
+
// 🔴 ONLY A PERSON'S CHANGE IS WORTH A DRAFT. The form fills
|
|
1000
|
+
// in values by itself after it opens (an input reporting its
|
|
1001
|
+
// value back, normalised dates, defaults, the organisation),
|
|
1002
|
+
// and each of those used to write a draft and say "Saved just
|
|
1003
|
+
// now" on a dialog nobody had touched. A field counts once a
|
|
1004
|
+
// person edited it AND it differs from what the form opened
|
|
1005
|
+
// with, so undoing the edit drops the draft again.
|
|
1006
|
+
const edited = Object.keys(this.touchedFields || {}).filter((key) => this.touchedFields[key]);
|
|
1007
|
+
if (!hasDraftChanges(values, this.draftBaseline, edited)) {
|
|
1008
|
+
this.settleUnchangedDraft(this.formDraftKey);
|
|
1009
|
+
return
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1000
1012
|
this.scheduleDraftWrite(this.formDraftKey, values);
|
|
1001
1013
|
},
|
|
1002
1014
|
},
|
|
@@ -1161,6 +1173,12 @@ var script = {
|
|
|
1161
1173
|
// Silently refilling a form is how somebody submits last week's answers
|
|
1162
1174
|
// without noticing they were there, and a form that fills itself is
|
|
1163
1175
|
// indistinguishable from one the server prefilled.
|
|
1176
|
+
// A draft stored under `anonymous` was written by a page that did not
|
|
1177
|
+
// know who was typing. Once somebody is signed in it is removed, never
|
|
1178
|
+
// offered: it may be another person's.
|
|
1179
|
+
if (this.formDraftKey && this.draftUserOwner) {
|
|
1180
|
+
clearAnonymousDrafts();
|
|
1181
|
+
}
|
|
1164
1182
|
if (this.formDraftKey) {
|
|
1165
1183
|
this.draftOffer = readDraft(this.formDraftKey, {
|
|
1166
1184
|
objectUpdated: (this.item?.updated || this.item?.['@self']?.updated || ''),
|
|
@@ -1519,6 +1537,10 @@ var script = {
|
|
|
1519
1537
|
this.runAssignments(null);
|
|
1520
1538
|
this.loadProfileDefaults();
|
|
1521
1539
|
}
|
|
1540
|
+
// What the form opened with. A draft is written only once a field
|
|
1541
|
+
// a person edited differs from this (see the `formData` watcher).
|
|
1542
|
+
// Not reactive: nothing renders from it.
|
|
1543
|
+
this.draftBaseline = JSON.parse(JSON.stringify(this.formData || {}));
|
|
1522
1544
|
},
|
|
1523
1545
|
|
|
1524
1546
|
/**
|
|
@@ -2306,6 +2328,27 @@ var script = {
|
|
|
2306
2328
|
return this.showsOuterLabel(field) ? '' : this.errors[field.key]
|
|
2307
2329
|
},
|
|
2308
2330
|
|
|
2331
|
+
/**
|
|
2332
|
+
* The placeholder of a text, number or textarea control.
|
|
2333
|
+
*
|
|
2334
|
+
* The field's description already shows as the help line under the
|
|
2335
|
+
* control, so the placeholder stays empty rather than repeat it word
|
|
2336
|
+
* for word. The help line is the one kept: it is still there after
|
|
2337
|
+
* somebody typed (a placeholder disappears with the first keystroke),
|
|
2338
|
+
* it is read out through `aria-describedby`, and in the Nextcloud look
|
|
2339
|
+
* a placeholder is hidden until the field has focus anyway. While an
|
|
2340
|
+
* error takes the help line's place, the description returns as the
|
|
2341
|
+
* placeholder so it is not lost.
|
|
2342
|
+
*
|
|
2343
|
+
* @param {object} field The field definition.
|
|
2344
|
+
* @return {string} The placeholder, or '' when the help line says it.
|
|
2345
|
+
* @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
|
|
2346
|
+
*/
|
|
2347
|
+
fieldPlaceholder(field) {
|
|
2348
|
+
const description = field.description || '';
|
|
2349
|
+
return this.helperError(field) ? description : ''
|
|
2350
|
+
},
|
|
2351
|
+
|
|
2309
2352
|
/**
|
|
2310
2353
|
* The accessibility attributes of a text control. In both looks an invalid
|
|
2311
2354
|
* control is `aria-invalid` and `aria-describedby` lists the error before
|
|
@@ -3112,7 +3155,7 @@ var script = {
|
|
|
3112
3155
|
const mappedProps = propertiesFromDefinitions(
|
|
3113
3156
|
Array.isArray(records) ? records : [],
|
|
3114
3157
|
config,
|
|
3115
|
-
{ orderFrom: 1000 + Object.keys(properties).length },
|
|
3158
|
+
{ orderFrom: 1000 + Object.keys(properties).length, create: this.isCreateMode },
|
|
3116
3159
|
);
|
|
3117
3160
|
Object.assign(properties, mappedProps.properties);
|
|
3118
3161
|
required.push(...mappedProps.required);
|
|
@@ -3788,6 +3831,11 @@ var script = {
|
|
|
3788
3831
|
}
|
|
3789
3832
|
|
|
3790
3833
|
this.formData = { ...this.formData, ...this.draftOffer.values };
|
|
3834
|
+
// What the person typed last time is their edit now: it counts for
|
|
3835
|
+
// the next draft write and survives the record arriving late.
|
|
3836
|
+
for (const key of Object.keys(this.draftOffer.values)) {
|
|
3837
|
+
this.touchedFields[key] = true;
|
|
3838
|
+
}
|
|
3791
3839
|
this.draftOffer = null;
|
|
3792
3840
|
},
|
|
3793
3841
|
|