@conduction/nextcloud-vue 2.76.0 → 2.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js +23 -0
- package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js.map +1 -1
- package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue3.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +18 -3
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +49 -3
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +2 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +79 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +229 -203
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js +9 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js +50 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js +13 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js.map +1 -0
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +37 -0
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +2 -2
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -4
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +48 -6
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +90 -51
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue.js +1 -1
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js +45 -2
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js.map +1 -1
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +16 -28
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +12 -24
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js +7 -2
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js.map +1 -1
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +114 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +115 -62
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +89 -7
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js +62 -30
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +7 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +4 -1
- 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 +104 -13
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +13 -9
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +199 -7
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +155 -119
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +6 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +36 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +35 -23
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js +6 -2
- package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js.map +1 -1
- package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue3.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/composables/useFormDraft.js +11 -1
- package/dist/esm/composables/useFormDraft.js.map +1 -1
- package/dist/esm/composables/useListView.js +4 -0
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/composables/useObjectPresence.js +15 -4
- package/dist/esm/composables/useObjectPresence.js.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +32 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +32 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +500 -75
- package/dist/esm/store/plugins/interactions.js +20 -10
- package/dist/esm/store/plugins/interactions.js.map +1 -1
- package/dist/esm/store/useObjectStore.js +17 -0
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/badgeVariants.js +32 -0
- package/dist/esm/utils/badgeVariants.js.map +1 -0
- package/dist/esm/utils/builtInFormatters.js +15 -7
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/groupAutocomplete.js +63 -1
- package/dist/esm/utils/groupAutocomplete.js.map +1 -1
- package/dist/esm/utils/lensAvailability.js +90 -0
- package/dist/esm/utils/lensAvailability.js.map +1 -0
- package/dist/esm/utils/personalLenses.js +13 -6
- package/dist/esm/utils/personalLenses.js.map +1 -1
- package/dist/esm/utils/recordInteractions.js +31 -13
- package/dist/esm/utils/recordInteractions.js.map +1 -1
- package/dist/esm/utils/resolveFilterTokens.js +46 -3
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/schema.js +1 -1
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +4 -4
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/widgetVisibility.js +46 -1
- package/dist/esm/utils/widgetVisibility.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +4389 -2890
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +500 -75
- package/l10n/en.json +32 -1
- package/l10n/nl.json +32 -1
- package/package.json +1 -1
- package/src/components/CnAiCompanion/CnAiCompanion.vue +23 -0
- package/src/components/CnAppNav/CnAppNav.vue +44 -5
- package/src/components/CnBoardView/CnBoardView.vue +2 -1
- package/src/components/CnCellRenderer/CnCellRenderer.md +1 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +106 -9
- package/src/components/CnCellRenderer/CnGroupNameCell.vue +61 -0
- package/src/components/CnChartWidget/CnChartWidget.vue +39 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -4
- package/src/components/CnDataTable/CnDataTable.md +9 -0
- package/src/components/CnDataTable/CnDataTable.vue +87 -22
- package/src/components/CnDateRangePicker/CnDateRangePicker.vue +44 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +10 -27
- package/src/components/CnFavouriteToggle/CnFavouriteToggle.vue +7 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +204 -5
- package/src/components/CnFollowToggle/CnFollowToggle.vue +118 -13
- package/src/components/CnFormDialog/CnFormDialog.vue +7 -1
- package/src/components/CnFormPage/CnFormPage.vue +4 -1
- package/src/components/CnIndexPage/CnIndexPage.md +36 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +112 -19
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +226 -2
- package/src/components/CnObjectSidebar/CnFilesTab.vue +9 -0
- package/src/components/CnStatWidget/CnStatWidget.vue +52 -2
- package/src/components/CnStatusBadge/CnStatusBadge.vue +6 -2
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
- package/src/composables/__tests__/useObjectPresence.spec.js +24 -0
- package/src/composables/useFormDraft.js +11 -1
- package/src/composables/useListView.js +4 -0
- package/src/composables/useObjectPresence.js +15 -4
- package/src/css/badge.css +31 -0
- package/src/css/kpi-card.css +51 -0
- package/src/css/look-board-detail.css +40 -2
- package/src/css/look-board-index.css +36 -0
- package/src/css/look-board.css +8 -0
- package/src/css/table.css +82 -17
- package/src/index.js +2 -1
- package/src/schemas/app-manifest-v2.schema.json +15 -11
- package/src/store/plugins/interactions.js +20 -10
- package/src/store/useObjectStore.js +17 -0
- package/src/utils/badgeVariants.js +29 -0
- package/src/utils/builtInFormatters.js +15 -7
- package/src/utils/groupAutocomplete.js +75 -0
- package/src/utils/index.js +1 -1
- package/src/utils/lensAvailability.js +86 -0
- package/src/utils/personalLenses.js +13 -6
- package/src/utils/recordInteractions.js +31 -13
- package/src/utils/resolveFilterTokens.js +45 -2
- package/src/utils/schema.js +1 -1
- package/src/utils/sentinelTokens.js +4 -4
- package/src/utils/validateManifestV2.compiled.js +2 -2
- package/src/utils/widgetVisibility.js +45 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useFormDraft.js","sources":["../../../src/composables/useFormDraft.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: AGPL-3.0-or-later\n *\n * useFormDraft — keep what somebody typed when the dialog closes on them.\n *\n * A handler filling a 27-field case type form and losing the tab starts over.\n * This writes the values to `localStorage` as they type and offers them back\n * the next time the same form opens.\n *\n * 🔴 IT NEVER DECIDES TO RESTORE. The draft is offered and the person chooses.\n * Silently refilling a form is how somebody submits last week's answers without\n * noticing they were there, and a form that fills itself is indistinguishable\n * from a form the server prefilled.\n *\n * 🔴 EVERY READ AND WRITE IS WRAPPED. `localStorage` throws in a private\n * window, with site data blocked, and inside some embedded webviews. A draft\n * that cannot be saved is a small loss; a dialog that will not open because\n * saving the draft threw is a large one.\n *\n * WHAT IT DELIBERATELY DOES NOT DO. It does not touch the server. A keystroke\n * autosave against the object API writes an audit row for every half-typed\n * value, and the audit trail is read by people answering \"who changed this\".\n */\n\nimport { translate as t } from '@nextcloud/l10n'\n\n/** How long a draft stays interesting, in milliseconds (7 days). */\nexport const DRAFT_MAX_AGE_MS = (7 * 24 * 60 * 60 * 1000)\n\n/** How long to wait after the last keystroke before writing, in milliseconds. */\nexport const DRAFT_DEBOUNCE_MS = 500\n\n/** The prefix every draft key carries, so a sweep can find them. */\nexport const DRAFT_KEY_PREFIX = 'cn-form-draft'\n\n/**\n * The storage key one form's draft lives under.\n *\n * 🔴 THE USER ID IS PART OF THE KEY. A shared browser profile at a service\n * desk is the ordinary case in a municipality, and a draft keyed without the\n * user hands the next person at the counter what the last one typed.\n *\n * @param {object} parts The identifying parts.\n * @param {string} [parts.appId] The app the form belongs to.\n * @param {string} [parts.schema] The schema being filled in.\n * @param {string} [parts.objectId] The object being edited, or '' when new.\n * @param {string} [parts.userId] Who is typing.\n * @return {string} The storage key.\n */\nexport function draftKey({ appId = '', schema = '', objectId = '', userId = '' } = {}) {\n\treturn [\n\t\tDRAFT_KEY_PREFIX,\n\t\tString(appId || 'app'),\n\t\tString(schema || 'schema'),\n\t\tString(objectId || 'new'),\n\t\tString(userId || 'anonymous'),\n\t].join(':')\n}\n\n/**\n * Read one draft back, or null when there is nothing usable.\n *\n * Returns null for an absent key, unreadable storage, unparseable JSON, a\n * draft older than {@link DRAFT_MAX_AGE_MS}, and a draft older than the object\n * it belongs to. That last one matters: a colleague who saved the record after\n * you started typing has said something more recent than your draft, and\n * offering yours on top would invite you to overwrite them without being told.\n *\n * @param {string} key The storage key.\n * @param {object} [options] Reading options.\n * @param {string} [options.objectUpdated] The object's `updated` timestamp, when it has one.\n * @param {number} [options.now] The current epoch ms, for testing.\n * @return {{values: object, savedAt: number}|null} The draft, or null.\n */\nexport function readDraft(key, { objectUpdated = '', now = Date.now() } = {}) {\n\tlet raw\n\ttry {\n\t\traw = window.localStorage.getItem(key)\n\t} catch {\n\t\t// Blocked or unavailable storage reads as \"no draft\", never as a throw.\n\t\treturn null\n\t}\n\n\tif (!raw) {\n\t\treturn null\n\t}\n\n\tlet parsed\n\ttry {\n\t\tparsed = JSON.parse(raw)\n\t} catch {\n\t\t// A corrupted entry is dropped rather than offered. Nothing here can\n\t\t// repair it, and offering half a form is worse than offering none.\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif (!parsed || typeof parsed !== 'object' || typeof parsed.savedAt !== 'number') {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif ((now - parsed.savedAt) > DRAFT_MAX_AGE_MS) {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif (objectUpdated) {\n\t\tconst updatedAt = Date.parse(String(objectUpdated))\n\t\tif (!Number.isNaN(updatedAt) && updatedAt > parsed.savedAt) {\n\t\t\t// The record moved on after this draft was written.\n\t\t\tclearDraft(key)\n\t\t\treturn null\n\t\t}\n\t}\n\n\tif (!parsed.values || typeof parsed.values !== 'object') {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\treturn { values: parsed.values, savedAt: parsed.savedAt }\n}\n\n/**\n * What the draft indicator says: \"Saving\" while a local write is pending,\n * \"Saved just now\" / \"Saved 3 minutes ago\" once it landed, nothing before that.\n *\n * @param {string} state `idle`, `saving` or `saved`.\n * @param {number} [savedAt] When the last write landed, in epoch ms.\n * @param {number} [now] The current epoch ms, for testing.\n * @return {string} The text, or '' when it says nothing.\n */\nexport function draftIndicatorText(state, savedAt = 0, now = Date.now()) {\n\tif (state === 'saving') {\n\t\treturn t('nextcloud-vue', 'Saving')\n\t}\n\tif (state !== 'saved') {\n\t\treturn ''\n\t}\n\tconst minutes = Math.floor(Math.max(0, now - savedAt) / 60000)\n\tif (!savedAt || minutes < 1) {\n\t\treturn t('nextcloud-vue', 'Saved just now')\n\t}\n\ttry {\n\t\tconst relative = new Intl.RelativeTimeFormat(document.documentElement.lang || 'en', { numeric: 'auto' }).format(-minutes, 'minute')\n\t\treturn t('nextcloud-vue', 'Saved {time}', { time: relative })\n\t} catch {\n\t\treturn t('nextcloud-vue', 'Saved')\n\t}\n}\n\n/**\n * Write one draft.\n *\n * @param {string} key The storage key.\n * @param {object} values The form values.\n * @param {number} [now] The current epoch ms, for testing.\n * @return {boolean} True when it was stored.\n */\nexport function writeDraft(key, values, now = Date.now()) {\n\ttry {\n\t\twindow.localStorage.setItem(\n\t\t\tkey,\n\t\t\tJSON.stringify({ savedAt: now, values: (values || {}) }),\n\t\t)\n\t\treturn true\n\t} catch {\n\t\t// Quota exceeded, or storage blocked. The form keeps working; the\n\t\t// recovery does not exist for this session. Reported to the caller so\n\t\t// the indicator can stay quiet rather than claim \"Saved\".\n\t\treturn false\n\t}\n}\n\n/**\n * Forget one draft.\n *\n * @param {string} key The storage key.\n * @return {void}\n */\nexport function clearDraft(key) {\n\ttry {\n\t\twindow.localStorage.removeItem(key)\n\t} catch {\n\t\t// Nothing to do and nothing worth failing a dialog over.\n\t}\n}\n\n/**\n * The Options API mixin the library's components use.\n *\n * A mixin rather than a Vue 3 composable, because these components are Options\n * API and the library builds for both Vue 2.7 and Vue 3.\n *\n * @return {object} The mixin.\n */\nexport function formDraftMixin() {\n\treturn {\n\t\tdata() {\n\t\t\treturn {\n\t\t\t\t/** The draft offered on open, or null when there is none. */\n\t\t\t\tdraftOffer: null,\n\t\t\t\t/** 'idle' | 'saving' | 'saved' — what the indicator announces. */\n\t\t\t\tdraftState: 'idle',\n\t\t\t\t/** When the last local write landed, in epoch ms. */\n\t\t\t\tdraftSavedAt: 0,\n\t\t\t\t/** Non-reactive handle for the debounce. */\n\t\t\t\tdraftTimeout: null,\n\t\t\t\t/**\n\t\t\t\t * Whether the form's own initial population has happened.\n\t\t\t\t *\n\t\t\t\t * The first change to the values is the component building\n\t\t\t\t * them, not a person typing. See the watcher that reads this.\n\t\t\t\t */\n\t\t\t\tdraftSeeded: false,\n\t\t\t}\n\t\t},\n\n\t\tbeforeUnmount() {\n\t\t\tif (this.draftTimeout) {\n\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\tthis.draftTimeout = null\n\t\t\t}\n\t\t},\n\n\t\tmethods: {\n\t\t\t/**\n\t\t\t * Schedule a draft write, debounced.\n\t\t\t *\n\t\t\t * @param {string} key The storage key.\n\t\t\t * @param {object} values The current form values.\n\t\t\t * @return {void}\n\t\t\t */\n\t\t\tscheduleDraftWrite(key, values) {\n\t\t\t\tif (!key) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tthis.draftState = 'saving'\n\t\t\t\tif (this.draftTimeout) {\n\t\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\t}\n\n\t\t\t\tthis.draftTimeout = setTimeout(() => {\n\t\t\t\t\tthis.draftTimeout = null\n\t\t\t\t\t// 'idle' and not 'saved' when the write failed: an\n\t\t\t\t\t// indicator that says Saved about a draft that is not\n\t\t\t\t\t// stored is the whole class of defect this library has\n\t\t\t\t\t// been paying down, in the one place a user is trusting it.\n\t\t\t\t\tconst stored = writeDraft(key, values)\n\t\t\t\t\tthis.draftState = stored ? 'saved' : 'idle'\n\t\t\t\t\tthis.draftSavedAt = stored ? Date.now() : 0\n\t\t\t\t}, DRAFT_DEBOUNCE_MS)\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Drop the draft and stop announcing anything about it.\n\t\t\t *\n\t\t\t * @param {string} key The storage key.\n\t\t\t * @return {void}\n\t\t\t */\n\t\t\tforgetDraft(key) {\n\t\t\t\tif (this.draftTimeout) {\n\t\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\t\tthis.draftTimeout = null\n\t\t\t\t}\n\n\t\t\t\tclearDraft(key)\n\t\t\t\tthis.draftOffer = null\n\t\t\t\tthis.draftState = 'idle'\n\t\t\t},\n\t\t},\n\t}\n}\n"],"names":["t"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useFormDraft.js","sources":["../../../src/composables/useFormDraft.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>\n * SPDX-License-Identifier: AGPL-3.0-or-later\n *\n * useFormDraft — keep what somebody typed when the dialog closes on them.\n *\n * A handler filling a 27-field case type form and losing the tab starts over.\n * This writes the values to `localStorage` as they type and offers them back\n * the next time the same form opens.\n *\n * 🔴 IT NEVER DECIDES TO RESTORE. The draft is offered and the person chooses.\n * Silently refilling a form is how somebody submits last week's answers without\n * noticing they were there, and a form that fills itself is indistinguishable\n * from a form the server prefilled.\n *\n * 🔴 EVERY READ AND WRITE IS WRAPPED. `localStorage` throws in a private\n * window, with site data blocked, and inside some embedded webviews. A draft\n * that cannot be saved is a small loss; a dialog that will not open because\n * saving the draft threw is a large one.\n *\n * WHAT IT DELIBERATELY DOES NOT DO. It does not touch the server. A keystroke\n * autosave against the object API writes an audit row for every half-typed\n * value, and the audit trail is read by people answering \"who changed this\".\n */\n\nimport { translate as t } from '@nextcloud/l10n'\n\n/** How long a draft stays interesting, in milliseconds (7 days). */\nexport const DRAFT_MAX_AGE_MS = (7 * 24 * 60 * 60 * 1000)\n\n/** How long to wait after the last keystroke before writing, in milliseconds. */\nexport const DRAFT_DEBOUNCE_MS = 500\n\n/** The prefix every draft key carries, so a sweep can find them. */\nexport const DRAFT_KEY_PREFIX = 'cn-form-draft'\n\n/**\n * The storage key one form's draft lives under.\n *\n * 🔴 THE USER ID IS PART OF THE KEY. A shared browser profile at a service\n * desk is the ordinary case in a municipality, and a draft keyed without the\n * user hands the next person at the counter what the last one typed.\n *\n * @param {object} parts The identifying parts.\n * @param {string} [parts.appId] The app the form belongs to.\n * @param {string} [parts.schema] The schema being filled in.\n * @param {string} [parts.objectId] The object being edited, or '' when new.\n * @param {string} [parts.userId] Who is typing.\n * @return {string} The storage key.\n */\nexport function draftKey({ appId = '', schema = '', objectId = '', userId = '' } = {}) {\n\treturn [\n\t\tDRAFT_KEY_PREFIX,\n\t\tString(appId || 'app'),\n\t\tString(schema || 'schema'),\n\t\tString(objectId || 'new'),\n\t\tString(userId || 'anonymous'),\n\t].join(':')\n}\n\n/**\n * Read one draft back, or null when there is nothing usable.\n *\n * Returns null for an absent key, unreadable storage, unparseable JSON, a\n * draft older than {@link DRAFT_MAX_AGE_MS}, and a draft older than the object\n * it belongs to. That last one matters: a colleague who saved the record after\n * you started typing has said something more recent than your draft, and\n * offering yours on top would invite you to overwrite them without being told.\n *\n * @param {string} key The storage key.\n * @param {object} [options] Reading options.\n * @param {string} [options.objectUpdated] The object's `updated` timestamp, when it has one.\n * @param {number} [options.now] The current epoch ms, for testing.\n * @return {{values: object, savedAt: number}|null} The draft, or null.\n */\nexport function readDraft(key, { objectUpdated = '', now = Date.now() } = {}) {\n\tlet raw\n\ttry {\n\t\traw = window.localStorage.getItem(key)\n\t} catch {\n\t\t// Blocked or unavailable storage reads as \"no draft\", never as a throw.\n\t\treturn null\n\t}\n\n\tif (!raw) {\n\t\treturn null\n\t}\n\n\tlet parsed\n\ttry {\n\t\tparsed = JSON.parse(raw)\n\t} catch {\n\t\t// A corrupted entry is dropped rather than offered. Nothing here can\n\t\t// repair it, and offering half a form is worse than offering none.\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif (!parsed || typeof parsed !== 'object' || typeof parsed.savedAt !== 'number') {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif ((now - parsed.savedAt) > DRAFT_MAX_AGE_MS) {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\tif (objectUpdated) {\n\t\tconst updatedAt = Date.parse(String(objectUpdated))\n\t\tif (!Number.isNaN(updatedAt) && updatedAt > parsed.savedAt) {\n\t\t\t// The record moved on after this draft was written.\n\t\t\tclearDraft(key)\n\t\t\treturn null\n\t\t}\n\t}\n\n\tif (!parsed.values || typeof parsed.values !== 'object') {\n\t\tclearDraft(key)\n\t\treturn null\n\t}\n\n\treturn { values: parsed.values, savedAt: parsed.savedAt }\n}\n\n/**\n * What the draft indicator says: \"Saving\" while a local write is pending,\n * \"Saved just now\" / \"Saved 3 minutes ago\" once it landed, nothing before that.\n *\n * After a failed save (`options.saveFailed`) it never says Saving or Saved:\n * next to a server error those words read as if the record had been saved.\n * It then says \"Draft kept on this device\" when a local copy is stored, and\n * nothing otherwise.\n *\n * @param {string} state `idle`, `saving` or `saved`.\n * @param {number} [savedAt] When the last write landed, in epoch ms.\n * @param {number} [now] The current epoch ms, for testing.\n * @param {{saveFailed?: boolean}} [options] `saveFailed`: the form shows a failed save.\n * @return {string} The text, or '' when it says nothing.\n * @spec openspec/changes/cell-labels-and-draft-indicator/specs/cell-labels-and-draft-indicator/spec.md#requirement-the-draft-indicator-never-reads-as-a-server-save-after-a-failed-save\n */\nexport function draftIndicatorText(state, savedAt = 0, now = Date.now(), options = {}) {\n\tif (options && options.saveFailed === true) {\n\t\treturn state === 'saved' ? t('nextcloud-vue', 'Draft kept on this device') : ''\n\t}\n\tif (state === 'saving') {\n\t\treturn t('nextcloud-vue', 'Saving')\n\t}\n\tif (state !== 'saved') {\n\t\treturn ''\n\t}\n\tconst minutes = Math.floor(Math.max(0, now - savedAt) / 60000)\n\tif (!savedAt || minutes < 1) {\n\t\treturn t('nextcloud-vue', 'Saved just now')\n\t}\n\ttry {\n\t\tconst relative = new Intl.RelativeTimeFormat(document.documentElement.lang || 'en', { numeric: 'auto' }).format(-minutes, 'minute')\n\t\treturn t('nextcloud-vue', 'Saved {time}', { time: relative })\n\t} catch {\n\t\treturn t('nextcloud-vue', 'Saved')\n\t}\n}\n\n/**\n * Write one draft.\n *\n * @param {string} key The storage key.\n * @param {object} values The form values.\n * @param {number} [now] The current epoch ms, for testing.\n * @return {boolean} True when it was stored.\n */\nexport function writeDraft(key, values, now = Date.now()) {\n\ttry {\n\t\twindow.localStorage.setItem(\n\t\t\tkey,\n\t\t\tJSON.stringify({ savedAt: now, values: (values || {}) }),\n\t\t)\n\t\treturn true\n\t} catch {\n\t\t// Quota exceeded, or storage blocked. The form keeps working; the\n\t\t// recovery does not exist for this session. Reported to the caller so\n\t\t// the indicator can stay quiet rather than claim \"Saved\".\n\t\treturn false\n\t}\n}\n\n/**\n * Forget one draft.\n *\n * @param {string} key The storage key.\n * @return {void}\n */\nexport function clearDraft(key) {\n\ttry {\n\t\twindow.localStorage.removeItem(key)\n\t} catch {\n\t\t// Nothing to do and nothing worth failing a dialog over.\n\t}\n}\n\n/**\n * The Options API mixin the library's components use.\n *\n * A mixin rather than a Vue 3 composable, because these components are Options\n * API and the library builds for both Vue 2.7 and Vue 3.\n *\n * @return {object} The mixin.\n */\nexport function formDraftMixin() {\n\treturn {\n\t\tdata() {\n\t\t\treturn {\n\t\t\t\t/** The draft offered on open, or null when there is none. */\n\t\t\t\tdraftOffer: null,\n\t\t\t\t/** 'idle' | 'saving' | 'saved' — what the indicator announces. */\n\t\t\t\tdraftState: 'idle',\n\t\t\t\t/** When the last local write landed, in epoch ms. */\n\t\t\t\tdraftSavedAt: 0,\n\t\t\t\t/** Non-reactive handle for the debounce. */\n\t\t\t\tdraftTimeout: null,\n\t\t\t\t/**\n\t\t\t\t * Whether the form's own initial population has happened.\n\t\t\t\t *\n\t\t\t\t * The first change to the values is the component building\n\t\t\t\t * them, not a person typing. See the watcher that reads this.\n\t\t\t\t */\n\t\t\t\tdraftSeeded: false,\n\t\t\t}\n\t\t},\n\n\t\tbeforeUnmount() {\n\t\t\tif (this.draftTimeout) {\n\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\tthis.draftTimeout = null\n\t\t\t}\n\t\t},\n\n\t\tmethods: {\n\t\t\t/**\n\t\t\t * Schedule a draft write, debounced.\n\t\t\t *\n\t\t\t * @param {string} key The storage key.\n\t\t\t * @param {object} values The current form values.\n\t\t\t * @return {void}\n\t\t\t */\n\t\t\tscheduleDraftWrite(key, values) {\n\t\t\t\tif (!key) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tthis.draftState = 'saving'\n\t\t\t\tif (this.draftTimeout) {\n\t\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\t}\n\n\t\t\t\tthis.draftTimeout = setTimeout(() => {\n\t\t\t\t\tthis.draftTimeout = null\n\t\t\t\t\t// 'idle' and not 'saved' when the write failed: an\n\t\t\t\t\t// indicator that says Saved about a draft that is not\n\t\t\t\t\t// stored is the whole class of defect this library has\n\t\t\t\t\t// been paying down, in the one place a user is trusting it.\n\t\t\t\t\tconst stored = writeDraft(key, values)\n\t\t\t\t\tthis.draftState = stored ? 'saved' : 'idle'\n\t\t\t\t\tthis.draftSavedAt = stored ? Date.now() : 0\n\t\t\t\t}, DRAFT_DEBOUNCE_MS)\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Drop the draft and stop announcing anything about it.\n\t\t\t *\n\t\t\t * @param {string} key The storage key.\n\t\t\t * @return {void}\n\t\t\t */\n\t\t\tforgetDraft(key) {\n\t\t\t\tif (this.draftTimeout) {\n\t\t\t\t\tclearTimeout(this.draftTimeout)\n\t\t\t\t\tthis.draftTimeout = null\n\t\t\t\t}\n\n\t\t\t\tclearDraft(key)\n\t\t\t\tthis.draftOffer = null\n\t\t\t\tthis.draftState = 'idle'\n\t\t\t},\n\t\t},\n\t}\n}\n"],"names":["t"],"mappings":"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"}
|
|
@@ -98,6 +98,9 @@ function useListView(objectTypeOrOptions, options) {
|
|
|
98
98
|
// field (a folder pane grouping by it, say) reads them here rather than
|
|
99
99
|
// deriving them from `objects`, which is only the current page.
|
|
100
100
|
const facets = computed(() => objectStore.facets[getType()] || {});
|
|
101
|
+
// Personal-lens reports of the latest response (`{ recent: { available, reason } }`).
|
|
102
|
+
// @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-a-list-response-carries-the-report-of-the-lenses-it-was-asked-for
|
|
103
|
+
const lenses = computed(() => (objectStore.lenses && objectStore.lenses[getType()]) || {});
|
|
101
104
|
// Outcome of the latest `refresh()`: the store's error for it, or null once
|
|
102
105
|
// it succeeded or a later fetch stored new rows. The store keeps the previous
|
|
103
106
|
// rows on a failed fetch, so this is how a consumer tells stale rows from
|
|
@@ -381,6 +384,7 @@ function useListView(objectTypeOrOptions, options) {
|
|
|
381
384
|
loading,
|
|
382
385
|
pagination,
|
|
383
386
|
facets,
|
|
387
|
+
lenses,
|
|
384
388
|
error,
|
|
385
389
|
// Local state
|
|
386
390
|
searchTerm,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useListView.js","sources":["../../../src/composables/useListView.js"],"sourcesContent":["import { computed, isRef, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { useObjectStore } from '../store/index.js'\n\n/**\n * Composable for managing list view state with full objectStore integration.\n *\n * When called with an `objectType` string, connects to the objectStore and handles\n * schema loading, collection fetching, sidebar wiring, and all event handlers\n * automatically. Everything a `CnIndexPage`-based list view needs is returned\n * directly — no additional computed properties or methods required in the component.\n *\n * Backward-compatible: existing `useListView(options)` and `useListView()` calls\n * continue to work without modification.\n *\n * @param {string|Function|object} [objectTypeOrOptions] Object type slug (new API), a getter or ref returning one (the list follows it when it changes), or a legacy options object\n * @param {object} [options] Options (new API only)\n * @param {object|null} [options.objectStore] Custom object store instance (from createObjectStore). When provided, uses this store instead of the default useObjectStore(). Required when the app uses createObjectStore with a custom store ID.\n * @param {object|null} [options.sidebarState] Sidebar state object from `inject('sidebarState')`. When provided, the composable wires and unwires the sidebar automatically on mount/unmount.\n * @param {number} [options.defaultPageSize] Default `_limit` sent to the API\n * @param {number} [options.debounceMs] Search debounce in milliseconds\n * @param {object} [options.defaultSort] Default sort applied on mount e.g. `{ key: 'createdAt', order: 'desc' }`\n * @param {Array<{key: string, order: 'asc'|'desc'}>} [options.defaultSortKeys] Default MULTI-column sort applied on mount (ordered priority list). Takes precedence over `defaultSort` when both are given; used e.g. to restore a sort persisted in the route query.\n * @param {Array<string>|null} [options.defaultVisibleColumns] Initial visible-column key set. Seeds the sidebar's Columns tab with the curated default (e.g. a manifest `columns` list) so toggles add/remove from it. Omit (or `null`) for schema-driven tables where every column starts visible.\n * @param {object} [options.defaultActiveFilters] Initial facet-filter map, e.g. to restore filters persisted in the route query on mount.\n * @param {string} [options.defaultSearchTerm] Initial search term, e.g. to restore `_search` from the route query on mount.\n * @param {object|(() => object)} [options.fixedFilters] A filter map (or getter returning one) merged into every fetch AFTER the user's facet filters, so the fixed entries always win. Used e.g. by `CnIndexPage` to apply a route-param-scoped `pages[].config.filter`. Default `{}` — omitting it is behaviourally identical to before.\n * @param {Array<string>|(() => Array<string>)} [options.extend] OpenRegister `_extend[]` values (or a getter returning them) forwarded on every fetch, e.g. `['calculations']` so a schema's `materialise: false` calculations are evaluated and can be shown as columns. Default `[]` — omitting it sends no `_extend` at all, exactly as before.\n * @return {object} Reactive state and event handlers\n *\n * @example\n * // New API — minimal\n * const { schema, objects, loading, pagination,\n * onSearch, onSort, onFilterChange, onPageChange, refresh } = useListView('client')\n *\n * @example\n * // New API — with sidebar wiring\n * const list = useListView('client', {\n * sidebarState: inject('sidebarState', null),\n * })\n *\n * @example\n * // Legacy API — still works\n * const { searchTerm, filters, onSearchInput, toggleSort } = useListView({\n * objectType: 'client',\n * fetchFn: (type, params) => objectStore.fetchCollection(type, params),\n * })\n */\nexport function useListView(objectTypeOrOptions, options) {\n\t// Backward compat: if first arg is an object or absent, delegate to legacy implementation\n\tif (!objectTypeOrOptions || (typeof objectTypeOrOptions === 'object' && !isRef(objectTypeOrOptions))) {\n\t\treturn useLegacyListView(objectTypeOrOptions || {})\n\t}\n\n\t// ── New API ──────────────────────────────────────────────────────────\n\t// A string, a ref or a getter: a getter lets the page switch object type\n\t// (a folder carrying its own schema) without re-creating the list.\n\tconst typeSource = objectTypeOrOptions\n\tconst isDynamicType = typeof typeSource !== 'string'\n\tconst getType = () => (isRef(typeSource) ? typeSource.value : (typeof typeSource === 'function' ? typeSource() : typeSource))\n\tconst opts = options || {}\n\tconst sidebarState = opts.sidebarState || null\n\n\tconst objectStore = opts.objectStore || useObjectStore()\n\n\t// ── State refs ───────────────────────────────────────────────────────\n\tconst schema = ref(null)\n\tconst searchTerm = ref(opts.defaultSearchTerm || '')\n\t// `sortKeys` is the source of truth (ordered multi-column priority list);\n\t// `sortKey`/`sortOrder` are kept as a primary-entry mirror so every\n\t// existing reader of the pre-multi-sort refs keeps working unchanged.\n\tconst initialSortKeys = Array.isArray(opts.defaultSortKeys) && opts.defaultSortKeys.length > 0\n\t\t? opts.defaultSortKeys\n\t\t: (opts.defaultSort?.key ? [{ key: opts.defaultSort.key, order: opts.defaultSort.order || 'asc' }] : [])\n\tconst sortKeys = ref(initialSortKeys)\n\tconst sortKey = ref(initialSortKeys[0]?.key || null)\n\tconst sortOrder = ref(initialSortKeys[0]?.order || 'asc')\n\tconst activeFilters = ref((opts.defaultActiveFilters && typeof opts.defaultActiveFilters === 'object') ? opts.defaultActiveFilters : {})\n\t// Seed the visible-column set to the configured columns so the sidebar's\n\t// Columns tab reflects the curated default and toggles add/remove from it.\n\t// `null` (no seed) means \"all columns visible\" (schema-driven tables).\n\tconst visibleColumns = ref(opts.defaultVisibleColumns ?? null)\n\tconst pageSize = ref(opts.defaultPageSize || 20)\n\n\t// ── Computed refs from the store ─────────────────────────────────────\n\tconst objects = computed(() => objectStore.collections[getType()] || [])\n\t// True from creation until the mount sequence below has issued its first\n\t// fetch. The store's own flag only goes up once `fetchCollection` runs, and\n\t// `onMounted` awaits `fetchSchema()` first — so for the length of that round\n\t// trip the list was \"not loading\" with zero rows, and every consumer\n\t// rendered its empty state (\"No log entries to show\") before any data had\n\t// been asked for. Folded into `loading` rather than exposed separately so\n\t// existing consumers get the fix without touching their templates.\n\tconst bootstrapping = ref(true)\n\tconst loading = computed(() => bootstrapping.value || objectStore.loading[getType()] || false)\n\tconst pagination = computed(() => objectStore.pagination[getType()] || { total: 0, page: 1, pages: 1, limit: 20 })\n\t// Facets are computed by the platform over the whole query rather than the\n\t// loaded page, so anything that needs the complete set of values for a\n\t// field (a folder pane grouping by it, say) reads them here rather than\n\t// deriving them from `objects`, which is only the current page.\n\tconst facets = computed(() => objectStore.facets[getType()] || {})\n\t// Outcome of the latest `refresh()`: the store's error for it, or null once\n\t// it succeeded or a later fetch stored new rows. The store keeps the previous\n\t// rows on a failed fetch, so this is how a consumer tells stale rows from\n\t// current ones.\n\tconst error = ref(null)\n\tlet refreshSeq = 0\n\tlet pendingRefreshes = 0\n\n\tlet searchTimeout = null\n\n\t// ── Param construction ───────────────────────────────────────────────\n\n\t/**\n\t * Resolve `opts.fixedFilters` to a plain map. Accepts a plain object OR a\n\t * function/getter returning one (so callers can derive it from reactive\n\t * sources — e.g. route params — and have it re-read on every fetch).\n\t *\n\t * @return {object} The fixed-filter map (may be empty).\n\t */\n\tfunction resolveFixedFilters() {\n\t\tconst f = typeof opts.fixedFilters === 'function' ? opts.fixedFilters() : opts.fixedFilters\n\t\treturn (f && typeof f === 'object') ? f : {}\n\t}\n\n\t/**\n\t * Resolve `opts.extend` to a list of non-empty strings.\n\t *\n\t * Accepts an array or a getter returning one, so a caller can re-scope it\n\t * between fetches the way `fixedFilters` allows.\n\t *\n\t * @return {Array<string>} The `_extend[]` values, possibly empty.\n\t */\n\tfunction resolveExtend() {\n\t\tconst e = typeof opts.extend === 'function' ? opts.extend() : opts.extend\n\t\tif (!Array.isArray(e)) {\n\t\t\treturn []\n\t\t}\n\t\treturn e.filter((v) => typeof v === 'string' && v !== '')\n\t}\n\n\t/**\n\t * Build API fetch params from current reactive state.\n\t *\n\t * @param {number} page Page number to request\n\t * @return {object} Params object ready to pass to fetchCollection\n\t */\n\tfunction buildParams(page) {\n\t\tconst params = { _limit: pageSize.value, _page: page }\n\n\t\tif (searchTerm.value) {\n\t\t\tparams._search = searchTerm.value\n\t\t}\n\n\t\tif (sortKeys.value.length > 0) {\n\t\t\t// Object key insertion order = sort priority order, matching\n\t\t\t// OpenRegister's `_order` format exactly (`ObjectsController::\n\t\t\t// normalizeOrderParameter` / `MagicMapper`'s ordering loop). A\n\t\t\t// single active key produces byte-identical output to before.\n\t\t\tparams._order = Object.fromEntries(sortKeys.value.map((k) => [k.key, k.order]))\n\t\t}\n\n\t\tfor (const [key, values] of Object.entries(activeFilters.value)) {\n\t\t\tif (values && values.length > 0) {\n\t\t\t\t// Single-value arrays are unwrapped to scalar params\n\t\t\t\tparams[key] = values.length === 1 ? values[0] : values\n\t\t\t}\n\t\t}\n\n\t\t// Fixed filters (e.g. a route-param-scoped `pages[].config.filter`) are\n\t\t// merged LAST so they always win over a colliding facet `activeFilter`.\n\t\t// Repeated `_extend[]`, serialized by the shared query builder. Set\n\t\t// before the fixed filters so an explicit `_extend` in a filter map\n\t\t// still wins, matching every other key's precedence here.\n\t\tconst extend = resolveExtend()\n\t\tif (extend.length > 0) {\n\t\t\tparams._extend = extend\n\t\t}\n\n\t\tfor (const [key, value] of Object.entries(resolveFixedFilters())) {\n\t\t\tif (value !== undefined && value !== null && value !== '') {\n\t\t\t\tparams[key] = value\n\t\t\t}\n\t\t}\n\n\t\treturn params\n\t}\n\n\t// ── Fetch ────────────────────────────────────────────────────────────\n\n\t/**\n\t * Fetch the collection using current state params and update sidebar facet data.\n\t * Only the most recently started refresh sets `error`, so an older response\n\t * settling late cannot override a newer one's outcome.\n\t *\n\t * @param {number} [page] Page to fetch\n\t * @return {Promise<void>}\n\t */\n\tasync function refresh(page = 1) {\n\t\tconst seq = ++refreshSeq\n\t\t// This call's own result. `errors[getType()]` is shared by every call\n\t\t// for the type and is not cleared on success, so it cannot say which\n\t\t// request failed; it is only the fallback for a store that ignores\n\t\t// `options.outcome`.\n\t\tconst outcome = {}\n\t\tpendingRefreshes++\n\t\ttry {\n\t\t\tawait objectStore.fetchCollection(getType(), buildParams(page), { outcome })\n\t\t} catch (e) {\n\t\t\tif (seq === refreshSeq) {\n\t\t\t\terror.value = e\n\t\t\t}\n\t\t\tthrow e\n\t\t} finally {\n\t\t\tpendingRefreshes--\n\t\t}\n\t\tif (seq === refreshSeq) {\n\t\t\terror.value = ('error' in outcome) ? outcome.error : (objectStore.errors?.[getType()] || null)\n\t\t}\n\t}\n\n\t// A fetch outside `refresh()` (e.g. the live-updates refetch) that stores\n\t// new rows recovers the list from an earlier failure. Rows written while\n\t// a refresh is pending are left to that refresh, which decides `error`.\n\twatch(\n\t\t() => objectStore.collections[getType()],\n\t\t() => {\n\t\t\tif (pendingRefreshes === 0) {\n\t\t\t\terror.value = null\n\t\t\t}\n\t\t},\n\t\t{ flush: 'sync' },\n\t)\n\n\t// ── Event handlers ───────────────────────────────────────────────────\n\n\t/**\n\t * Handle search input. Debounced by `options.debounceMs` (default 300 ms).\n\t *\n\t * @param {string} value New search string\n\t */\n\tfunction onSearch(value) {\n\t\tsearchTerm.value = value\n\t\tclearTimeout(searchTimeout)\n\t\tsearchTimeout = setTimeout(() => refresh(1), opts.debounceMs || 300)\n\t}\n\n\t/**\n\t * Handle sort change. Updates sort state and triggers refresh.\n\t *\n\t * Accepts CnDataTable's extended `sort` event payload: `keys` (the full\n\t * ordered multi-column list) is used when present; a legacy caller\n\t * passing only `{ key, order }` (no `keys`) still works exactly as\n\t * before — it's normalised to a single-entry `sortKeys` list.\n\t *\n\t * @param {{key: string|null, order: string|null, keys?: Array<{key: string, order: string}>}} sort New sort definition.\n\t */\n\tfunction onSort({ key, order, keys }) {\n\t\tconst nextKeys = Array.isArray(keys) ? keys : (key ? [{ key, order: order || 'asc' }] : [])\n\t\tsortKeys.value = nextKeys\n\t\tsortKey.value = nextKeys[0]?.key ?? null\n\t\tsortOrder.value = nextKeys[0]?.order ?? 'asc'\n\t\trefresh(1)\n\t}\n\n\t/**\n\t * Handle filter change for a single key. Empty arrays remove the key.\n\t *\n\t * @param {string} key Filter key (maps to API param name)\n\t * @param {Array} values Selected filter values\n\t */\n\tfunction onFilterChange(key, values) {\n\t\tif (!values || values.length === 0) {\n\t\t\tconst updated = { ...activeFilters.value }\n\t\t\tdelete updated[key]\n\t\t\tactiveFilters.value = updated\n\t\t} else {\n\t\t\tactiveFilters.value = { ...activeFilters.value, [key]: values }\n\t\t}\n\t\trefresh(1)\n\t}\n\n\t/**\n\t * Handle page navigation.\n\t *\n\t * @param {number} page Page number to navigate to\n\t */\n\tfunction onPageChange(page) {\n\t\trefresh(page)\n\t}\n\n\t/**\n\t * Handle page-size change. Resets to page 1.\n\t *\n\t * @param {number} size New page size\n\t */\n\tfunction onPageSizeChange(size) {\n\t\tpageSize.value = size\n\t\trefresh(1)\n\t}\n\n\t// ── Sidebar wiring ───────────────────────────────────────────────────\n\n\tfunction setupSidebar() {\n\t\tif (!sidebarState) {\n\t\t\treturn\n\t\t}\n\t\tsidebarState.active = true\n\t\tsidebarState.schema = schema.value\n\t\tsidebarState.searchValue = searchTerm.value\n\t\tsidebarState.activeFilters = {}\n\t\tsidebarState.onSearch = onSearch\n\t\tsidebarState.onColumnsChange = (cols) => {\n\t\t\tvisibleColumns.value = cols\n\t\t}\n\t\tsidebarState.onFilterChange = ({ key, values }) => onFilterChange(key, values)\n\t}\n\n\tfunction teardownSidebar() {\n\t\tif (!sidebarState) {\n\t\t\treturn\n\t\t}\n\t\tsidebarState.active = false\n\t\tsidebarState.schema = null\n\t\tsidebarState.activeFilters = {}\n\t\tsidebarState.facetData = {}\n\t\tsidebarState.onSearch = null\n\t\tsidebarState.onColumnsChange = null\n\t\tsidebarState.onFilterChange = null\n\t}\n\n\t// Push facet data to sidebar after each store update\n\tif (sidebarState) {\n\t\twatch(\n\t\t\t() => objectStore.facets[getType()],\n\t\t\t(facets) => {\n\t\t\t\tsidebarState.facetData = facets || {}\n\t\t\t},\n\t\t)\n\t}\n\n\t// A dynamic object type that changes starts that type's list from page 1\n\t// with its own schema.\n\tif (isDynamicType) {\n\t\twatch(getType, async (next, prev) => {\n\t\t\tif (!next || next === prev) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tschema.value = null\n\t\t\tschema.value = await objectStore.fetchSchema(next)\n\t\t\tawait refresh(1)\n\t\t})\n\t}\n\n\t// ── Lifecycle ────────────────────────────────────────────────────────\n\n\tonMounted(async () => {\n\t\ttry {\n\t\t\tschema.value = await objectStore.fetchSchema(getType())\n\t\t\tif (sidebarState) {\n\t\t\t\tsetupSidebar()\n\t\t\t}\n\t\t\tawait refresh(1)\n\t\t} finally {\n\t\t\t// Hand the loading state back to the store either way: a schema\n\t\t\t// fetch that rejects must not leave the list spinning forever.\n\t\t\tbootstrapping.value = false\n\t\t}\n\t})\n\n\tonBeforeUnmount(() => {\n\t\tclearTimeout(searchTimeout)\n\t\tteardownSidebar()\n\t})\n\n\t// ── Return value ─────────────────────────────────────────────────────\n\n\treturn {\n\t\t// Store-derived\n\t\tschema,\n\t\tobjects,\n\t\tloading,\n\t\tpagination,\n\t\tfacets,\n\t\terror,\n\t\t// Local state\n\t\tsearchTerm,\n\t\tsortKey,\n\t\tsortOrder,\n\t\tsortKeys,\n\t\tactiveFilters,\n\t\tvisibleColumns,\n\t\tpageSize,\n\t\t// Event handlers\n\t\tonSearch,\n\t\tonSort,\n\t\tonFilterChange,\n\t\tonPageChange,\n\t\tonPageSizeChange,\n\t\t// Explicit fetch\n\t\trefresh,\n\t\t// The query the list sends (search, sort, facet filters, fixed filters),\n\t\t// built from the current state, for an export that must follow the list.\n\t\tbuildParams,\n\t}\n}\n\n// ── Legacy implementation ─────────────────────────────────────────────────────\n\n/**\n * Legacy `useListView(options)` implementation.\n * Preserved verbatim for backward compatibility.\n *\n * @param {object} options Legacy options object\n * @param {string} [options.objectType] The registered object type slug\n * @param {(type: string, params: object) => Promise<Array<object>>} [options.fetchFn] Function to call: (type, params) => Promise<Array>\n * @param {number} [options.debounceMs] Search debounce in milliseconds\n * @param {number} [options.pageSize] Default page size\n * @param {object} [options.defaultSort] Default sort: { key: string, order: 'asc'|'desc' }\n * @return {object} Reactive state and methods\n */\nfunction useLegacyListView(options) {\n\tconst searchTerm = ref('')\n\tconst filters = ref({})\n\tconst sortKey = ref(options.defaultSort?.key || null)\n\tconst sortOrder = ref(options.defaultSort?.order || 'asc')\n\tconst currentPage = ref(1)\n\tconst pageSize = ref(options.pageSize || 20)\n\n\tlet searchTimeout = null\n\n\tfunction buildFetchParams() {\n\t\tconst params = {\n\t\t\t_limit: pageSize.value,\n\t\t\t_page: currentPage.value,\n\t\t}\n\n\t\tif (searchTerm.value) {\n\t\t\tparams._search = searchTerm.value\n\t\t}\n\n\t\tif (sortKey.value) {\n\t\t\tparams._order = { [sortKey.value]: sortOrder.value }\n\t\t}\n\n\t\tfor (const [key, value] of Object.entries(filters.value)) {\n\t\t\tif (value !== null && value !== '' && value !== false) {\n\t\t\t\tparams[key] = value\n\t\t\t}\n\t\t}\n\n\t\treturn params\n\t}\n\n\tasync function fetchData(page) {\n\t\tif (page !== undefined) {\n\t\t\tcurrentPage.value = page\n\t\t}\n\t\tconst params = buildFetchParams()\n\t\tif (options.fetchFn) {\n\t\t\treturn options.fetchFn(options.objectType, params)\n\t\t}\n\t}\n\n\tfunction onSearchInput(value) {\n\t\tsearchTerm.value = value\n\t\tclearTimeout(searchTimeout)\n\t\tsearchTimeout = setTimeout(() => fetchData(1), options.debounceMs || 300)\n\t}\n\n\tfunction toggleSort(key) {\n\t\tif (sortKey.value === key) {\n\t\t\tif (sortOrder.value === 'asc') {\n\t\t\t\tsortOrder.value = 'desc'\n\t\t\t} else {\n\t\t\t\tsortKey.value = null\n\t\t\t\tsortOrder.value = 'asc'\n\t\t\t}\n\t\t} else {\n\t\t\tsortKey.value = key\n\t\t\tsortOrder.value = 'asc'\n\t\t}\n\t\tfetchData(1)\n\t}\n\n\tfunction setFilter(key, value) {\n\t\tfilters.value = { ...filters.value, [key]: value }\n\t\tfetchData(1)\n\t}\n\n\tfunction clearAllFilters() {\n\t\tsearchTerm.value = ''\n\t\tfilters.value = {}\n\t\tsortKey.value = options.defaultSort?.key || null\n\t\tsortOrder.value = options.defaultSort?.order || 'asc'\n\t\tfetchData(1)\n\t}\n\n\tfunction goToPage(page) {\n\t\tcurrentPage.value = page\n\t\tfetchData()\n\t}\n\n\tonBeforeUnmount(() => clearTimeout(searchTimeout))\n\n\treturn {\n\t\tsearchTerm,\n\t\tfilters,\n\t\tsortKey,\n\t\tsortOrder,\n\t\tcurrentPage,\n\t\tpageSize,\n\t\tonSearchInput,\n\t\ttoggleSort,\n\t\tsetFilter,\n\t\tclearAllFilters,\n\t\tgoToPage,\n\t\tfetch: fetchData,\n\t\tbuildFetchParams,\n\t}\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useListView.js","sources":["../../../src/composables/useListView.js"],"sourcesContent":["import { computed, isRef, onBeforeUnmount, onMounted, ref, watch } from 'vue'\nimport { useObjectStore } from '../store/index.js'\n\n/**\n * Composable for managing list view state with full objectStore integration.\n *\n * When called with an `objectType` string, connects to the objectStore and handles\n * schema loading, collection fetching, sidebar wiring, and all event handlers\n * automatically. Everything a `CnIndexPage`-based list view needs is returned\n * directly — no additional computed properties or methods required in the component.\n *\n * Backward-compatible: existing `useListView(options)` and `useListView()` calls\n * continue to work without modification.\n *\n * @param {string|Function|object} [objectTypeOrOptions] Object type slug (new API), a getter or ref returning one (the list follows it when it changes), or a legacy options object\n * @param {object} [options] Options (new API only)\n * @param {object|null} [options.objectStore] Custom object store instance (from createObjectStore). When provided, uses this store instead of the default useObjectStore(). Required when the app uses createObjectStore with a custom store ID.\n * @param {object|null} [options.sidebarState] Sidebar state object from `inject('sidebarState')`. When provided, the composable wires and unwires the sidebar automatically on mount/unmount.\n * @param {number} [options.defaultPageSize] Default `_limit` sent to the API\n * @param {number} [options.debounceMs] Search debounce in milliseconds\n * @param {object} [options.defaultSort] Default sort applied on mount e.g. `{ key: 'createdAt', order: 'desc' }`\n * @param {Array<{key: string, order: 'asc'|'desc'}>} [options.defaultSortKeys] Default MULTI-column sort applied on mount (ordered priority list). Takes precedence over `defaultSort` when both are given; used e.g. to restore a sort persisted in the route query.\n * @param {Array<string>|null} [options.defaultVisibleColumns] Initial visible-column key set. Seeds the sidebar's Columns tab with the curated default (e.g. a manifest `columns` list) so toggles add/remove from it. Omit (or `null`) for schema-driven tables where every column starts visible.\n * @param {object} [options.defaultActiveFilters] Initial facet-filter map, e.g. to restore filters persisted in the route query on mount.\n * @param {string} [options.defaultSearchTerm] Initial search term, e.g. to restore `_search` from the route query on mount.\n * @param {object|(() => object)} [options.fixedFilters] A filter map (or getter returning one) merged into every fetch AFTER the user's facet filters, so the fixed entries always win. Used e.g. by `CnIndexPage` to apply a route-param-scoped `pages[].config.filter`. Default `{}` — omitting it is behaviourally identical to before.\n * @param {Array<string>|(() => Array<string>)} [options.extend] OpenRegister `_extend[]` values (or a getter returning them) forwarded on every fetch, e.g. `['calculations']` so a schema's `materialise: false` calculations are evaluated and can be shown as columns. Default `[]` — omitting it sends no `_extend` at all, exactly as before.\n * @return {object} Reactive state and event handlers\n *\n * @example\n * // New API — minimal\n * const { schema, objects, loading, pagination,\n * onSearch, onSort, onFilterChange, onPageChange, refresh } = useListView('client')\n *\n * @example\n * // New API — with sidebar wiring\n * const list = useListView('client', {\n * sidebarState: inject('sidebarState', null),\n * })\n *\n * @example\n * // Legacy API — still works\n * const { searchTerm, filters, onSearchInput, toggleSort } = useListView({\n * objectType: 'client',\n * fetchFn: (type, params) => objectStore.fetchCollection(type, params),\n * })\n */\nexport function useListView(objectTypeOrOptions, options) {\n\t// Backward compat: if first arg is an object or absent, delegate to legacy implementation\n\tif (!objectTypeOrOptions || (typeof objectTypeOrOptions === 'object' && !isRef(objectTypeOrOptions))) {\n\t\treturn useLegacyListView(objectTypeOrOptions || {})\n\t}\n\n\t// ── New API ──────────────────────────────────────────────────────────\n\t// A string, a ref or a getter: a getter lets the page switch object type\n\t// (a folder carrying its own schema) without re-creating the list.\n\tconst typeSource = objectTypeOrOptions\n\tconst isDynamicType = typeof typeSource !== 'string'\n\tconst getType = () => (isRef(typeSource) ? typeSource.value : (typeof typeSource === 'function' ? typeSource() : typeSource))\n\tconst opts = options || {}\n\tconst sidebarState = opts.sidebarState || null\n\n\tconst objectStore = opts.objectStore || useObjectStore()\n\n\t// ── State refs ───────────────────────────────────────────────────────\n\tconst schema = ref(null)\n\tconst searchTerm = ref(opts.defaultSearchTerm || '')\n\t// `sortKeys` is the source of truth (ordered multi-column priority list);\n\t// `sortKey`/`sortOrder` are kept as a primary-entry mirror so every\n\t// existing reader of the pre-multi-sort refs keeps working unchanged.\n\tconst initialSortKeys = Array.isArray(opts.defaultSortKeys) && opts.defaultSortKeys.length > 0\n\t\t? opts.defaultSortKeys\n\t\t: (opts.defaultSort?.key ? [{ key: opts.defaultSort.key, order: opts.defaultSort.order || 'asc' }] : [])\n\tconst sortKeys = ref(initialSortKeys)\n\tconst sortKey = ref(initialSortKeys[0]?.key || null)\n\tconst sortOrder = ref(initialSortKeys[0]?.order || 'asc')\n\tconst activeFilters = ref((opts.defaultActiveFilters && typeof opts.defaultActiveFilters === 'object') ? opts.defaultActiveFilters : {})\n\t// Seed the visible-column set to the configured columns so the sidebar's\n\t// Columns tab reflects the curated default and toggles add/remove from it.\n\t// `null` (no seed) means \"all columns visible\" (schema-driven tables).\n\tconst visibleColumns = ref(opts.defaultVisibleColumns ?? null)\n\tconst pageSize = ref(opts.defaultPageSize || 20)\n\n\t// ── Computed refs from the store ─────────────────────────────────────\n\tconst objects = computed(() => objectStore.collections[getType()] || [])\n\t// True from creation until the mount sequence below has issued its first\n\t// fetch. The store's own flag only goes up once `fetchCollection` runs, and\n\t// `onMounted` awaits `fetchSchema()` first — so for the length of that round\n\t// trip the list was \"not loading\" with zero rows, and every consumer\n\t// rendered its empty state (\"No log entries to show\") before any data had\n\t// been asked for. Folded into `loading` rather than exposed separately so\n\t// existing consumers get the fix without touching their templates.\n\tconst bootstrapping = ref(true)\n\tconst loading = computed(() => bootstrapping.value || objectStore.loading[getType()] || false)\n\tconst pagination = computed(() => objectStore.pagination[getType()] || { total: 0, page: 1, pages: 1, limit: 20 })\n\t// Facets are computed by the platform over the whole query rather than the\n\t// loaded page, so anything that needs the complete set of values for a\n\t// field (a folder pane grouping by it, say) reads them here rather than\n\t// deriving them from `objects`, which is only the current page.\n\tconst facets = computed(() => objectStore.facets[getType()] || {})\n\t// Personal-lens reports of the latest response (`{ recent: { available, reason } }`).\n\t// @spec openspec/changes/lens-says-why-it-is-empty/specs/personal-lens-availability/spec.md#requirement-a-list-response-carries-the-report-of-the-lenses-it-was-asked-for\n\tconst lenses = computed(() => (objectStore.lenses && objectStore.lenses[getType()]) || {})\n\t// Outcome of the latest `refresh()`: the store's error for it, or null once\n\t// it succeeded or a later fetch stored new rows. The store keeps the previous\n\t// rows on a failed fetch, so this is how a consumer tells stale rows from\n\t// current ones.\n\tconst error = ref(null)\n\tlet refreshSeq = 0\n\tlet pendingRefreshes = 0\n\n\tlet searchTimeout = null\n\n\t// ── Param construction ───────────────────────────────────────────────\n\n\t/**\n\t * Resolve `opts.fixedFilters` to a plain map. Accepts a plain object OR a\n\t * function/getter returning one (so callers can derive it from reactive\n\t * sources — e.g. route params — and have it re-read on every fetch).\n\t *\n\t * @return {object} The fixed-filter map (may be empty).\n\t */\n\tfunction resolveFixedFilters() {\n\t\tconst f = typeof opts.fixedFilters === 'function' ? opts.fixedFilters() : opts.fixedFilters\n\t\treturn (f && typeof f === 'object') ? f : {}\n\t}\n\n\t/**\n\t * Resolve `opts.extend` to a list of non-empty strings.\n\t *\n\t * Accepts an array or a getter returning one, so a caller can re-scope it\n\t * between fetches the way `fixedFilters` allows.\n\t *\n\t * @return {Array<string>} The `_extend[]` values, possibly empty.\n\t */\n\tfunction resolveExtend() {\n\t\tconst e = typeof opts.extend === 'function' ? opts.extend() : opts.extend\n\t\tif (!Array.isArray(e)) {\n\t\t\treturn []\n\t\t}\n\t\treturn e.filter((v) => typeof v === 'string' && v !== '')\n\t}\n\n\t/**\n\t * Build API fetch params from current reactive state.\n\t *\n\t * @param {number} page Page number to request\n\t * @return {object} Params object ready to pass to fetchCollection\n\t */\n\tfunction buildParams(page) {\n\t\tconst params = { _limit: pageSize.value, _page: page }\n\n\t\tif (searchTerm.value) {\n\t\t\tparams._search = searchTerm.value\n\t\t}\n\n\t\tif (sortKeys.value.length > 0) {\n\t\t\t// Object key insertion order = sort priority order, matching\n\t\t\t// OpenRegister's `_order` format exactly (`ObjectsController::\n\t\t\t// normalizeOrderParameter` / `MagicMapper`'s ordering loop). A\n\t\t\t// single active key produces byte-identical output to before.\n\t\t\tparams._order = Object.fromEntries(sortKeys.value.map((k) => [k.key, k.order]))\n\t\t}\n\n\t\tfor (const [key, values] of Object.entries(activeFilters.value)) {\n\t\t\tif (values && values.length > 0) {\n\t\t\t\t// Single-value arrays are unwrapped to scalar params\n\t\t\t\tparams[key] = values.length === 1 ? values[0] : values\n\t\t\t}\n\t\t}\n\n\t\t// Fixed filters (e.g. a route-param-scoped `pages[].config.filter`) are\n\t\t// merged LAST so they always win over a colliding facet `activeFilter`.\n\t\t// Repeated `_extend[]`, serialized by the shared query builder. Set\n\t\t// before the fixed filters so an explicit `_extend` in a filter map\n\t\t// still wins, matching every other key's precedence here.\n\t\tconst extend = resolveExtend()\n\t\tif (extend.length > 0) {\n\t\t\tparams._extend = extend\n\t\t}\n\n\t\tfor (const [key, value] of Object.entries(resolveFixedFilters())) {\n\t\t\tif (value !== undefined && value !== null && value !== '') {\n\t\t\t\tparams[key] = value\n\t\t\t}\n\t\t}\n\n\t\treturn params\n\t}\n\n\t// ── Fetch ────────────────────────────────────────────────────────────\n\n\t/**\n\t * Fetch the collection using current state params and update sidebar facet data.\n\t * Only the most recently started refresh sets `error`, so an older response\n\t * settling late cannot override a newer one's outcome.\n\t *\n\t * @param {number} [page] Page to fetch\n\t * @return {Promise<void>}\n\t */\n\tasync function refresh(page = 1) {\n\t\tconst seq = ++refreshSeq\n\t\t// This call's own result. `errors[getType()]` is shared by every call\n\t\t// for the type and is not cleared on success, so it cannot say which\n\t\t// request failed; it is only the fallback for a store that ignores\n\t\t// `options.outcome`.\n\t\tconst outcome = {}\n\t\tpendingRefreshes++\n\t\ttry {\n\t\t\tawait objectStore.fetchCollection(getType(), buildParams(page), { outcome })\n\t\t} catch (e) {\n\t\t\tif (seq === refreshSeq) {\n\t\t\t\terror.value = e\n\t\t\t}\n\t\t\tthrow e\n\t\t} finally {\n\t\t\tpendingRefreshes--\n\t\t}\n\t\tif (seq === refreshSeq) {\n\t\t\terror.value = ('error' in outcome) ? outcome.error : (objectStore.errors?.[getType()] || null)\n\t\t}\n\t}\n\n\t// A fetch outside `refresh()` (e.g. the live-updates refetch) that stores\n\t// new rows recovers the list from an earlier failure. Rows written while\n\t// a refresh is pending are left to that refresh, which decides `error`.\n\twatch(\n\t\t() => objectStore.collections[getType()],\n\t\t() => {\n\t\t\tif (pendingRefreshes === 0) {\n\t\t\t\terror.value = null\n\t\t\t}\n\t\t},\n\t\t{ flush: 'sync' },\n\t)\n\n\t// ── Event handlers ───────────────────────────────────────────────────\n\n\t/**\n\t * Handle search input. Debounced by `options.debounceMs` (default 300 ms).\n\t *\n\t * @param {string} value New search string\n\t */\n\tfunction onSearch(value) {\n\t\tsearchTerm.value = value\n\t\tclearTimeout(searchTimeout)\n\t\tsearchTimeout = setTimeout(() => refresh(1), opts.debounceMs || 300)\n\t}\n\n\t/**\n\t * Handle sort change. Updates sort state and triggers refresh.\n\t *\n\t * Accepts CnDataTable's extended `sort` event payload: `keys` (the full\n\t * ordered multi-column list) is used when present; a legacy caller\n\t * passing only `{ key, order }` (no `keys`) still works exactly as\n\t * before — it's normalised to a single-entry `sortKeys` list.\n\t *\n\t * @param {{key: string|null, order: string|null, keys?: Array<{key: string, order: string}>}} sort New sort definition.\n\t */\n\tfunction onSort({ key, order, keys }) {\n\t\tconst nextKeys = Array.isArray(keys) ? keys : (key ? [{ key, order: order || 'asc' }] : [])\n\t\tsortKeys.value = nextKeys\n\t\tsortKey.value = nextKeys[0]?.key ?? null\n\t\tsortOrder.value = nextKeys[0]?.order ?? 'asc'\n\t\trefresh(1)\n\t}\n\n\t/**\n\t * Handle filter change for a single key. Empty arrays remove the key.\n\t *\n\t * @param {string} key Filter key (maps to API param name)\n\t * @param {Array} values Selected filter values\n\t */\n\tfunction onFilterChange(key, values) {\n\t\tif (!values || values.length === 0) {\n\t\t\tconst updated = { ...activeFilters.value }\n\t\t\tdelete updated[key]\n\t\t\tactiveFilters.value = updated\n\t\t} else {\n\t\t\tactiveFilters.value = { ...activeFilters.value, [key]: values }\n\t\t}\n\t\trefresh(1)\n\t}\n\n\t/**\n\t * Handle page navigation.\n\t *\n\t * @param {number} page Page number to navigate to\n\t */\n\tfunction onPageChange(page) {\n\t\trefresh(page)\n\t}\n\n\t/**\n\t * Handle page-size change. Resets to page 1.\n\t *\n\t * @param {number} size New page size\n\t */\n\tfunction onPageSizeChange(size) {\n\t\tpageSize.value = size\n\t\trefresh(1)\n\t}\n\n\t// ── Sidebar wiring ───────────────────────────────────────────────────\n\n\tfunction setupSidebar() {\n\t\tif (!sidebarState) {\n\t\t\treturn\n\t\t}\n\t\tsidebarState.active = true\n\t\tsidebarState.schema = schema.value\n\t\tsidebarState.searchValue = searchTerm.value\n\t\tsidebarState.activeFilters = {}\n\t\tsidebarState.onSearch = onSearch\n\t\tsidebarState.onColumnsChange = (cols) => {\n\t\t\tvisibleColumns.value = cols\n\t\t}\n\t\tsidebarState.onFilterChange = ({ key, values }) => onFilterChange(key, values)\n\t}\n\n\tfunction teardownSidebar() {\n\t\tif (!sidebarState) {\n\t\t\treturn\n\t\t}\n\t\tsidebarState.active = false\n\t\tsidebarState.schema = null\n\t\tsidebarState.activeFilters = {}\n\t\tsidebarState.facetData = {}\n\t\tsidebarState.onSearch = null\n\t\tsidebarState.onColumnsChange = null\n\t\tsidebarState.onFilterChange = null\n\t}\n\n\t// Push facet data to sidebar after each store update\n\tif (sidebarState) {\n\t\twatch(\n\t\t\t() => objectStore.facets[getType()],\n\t\t\t(facets) => {\n\t\t\t\tsidebarState.facetData = facets || {}\n\t\t\t},\n\t\t)\n\t}\n\n\t// A dynamic object type that changes starts that type's list from page 1\n\t// with its own schema.\n\tif (isDynamicType) {\n\t\twatch(getType, async (next, prev) => {\n\t\t\tif (!next || next === prev) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tschema.value = null\n\t\t\tschema.value = await objectStore.fetchSchema(next)\n\t\t\tawait refresh(1)\n\t\t})\n\t}\n\n\t// ── Lifecycle ────────────────────────────────────────────────────────\n\n\tonMounted(async () => {\n\t\ttry {\n\t\t\tschema.value = await objectStore.fetchSchema(getType())\n\t\t\tif (sidebarState) {\n\t\t\t\tsetupSidebar()\n\t\t\t}\n\t\t\tawait refresh(1)\n\t\t} finally {\n\t\t\t// Hand the loading state back to the store either way: a schema\n\t\t\t// fetch that rejects must not leave the list spinning forever.\n\t\t\tbootstrapping.value = false\n\t\t}\n\t})\n\n\tonBeforeUnmount(() => {\n\t\tclearTimeout(searchTimeout)\n\t\tteardownSidebar()\n\t})\n\n\t// ── Return value ─────────────────────────────────────────────────────\n\n\treturn {\n\t\t// Store-derived\n\t\tschema,\n\t\tobjects,\n\t\tloading,\n\t\tpagination,\n\t\tfacets,\n\t\tlenses,\n\t\terror,\n\t\t// Local state\n\t\tsearchTerm,\n\t\tsortKey,\n\t\tsortOrder,\n\t\tsortKeys,\n\t\tactiveFilters,\n\t\tvisibleColumns,\n\t\tpageSize,\n\t\t// Event handlers\n\t\tonSearch,\n\t\tonSort,\n\t\tonFilterChange,\n\t\tonPageChange,\n\t\tonPageSizeChange,\n\t\t// Explicit fetch\n\t\trefresh,\n\t\t// The query the list sends (search, sort, facet filters, fixed filters),\n\t\t// built from the current state, for an export that must follow the list.\n\t\tbuildParams,\n\t}\n}\n\n// ── Legacy implementation ─────────────────────────────────────────────────────\n\n/**\n * Legacy `useListView(options)` implementation.\n * Preserved verbatim for backward compatibility.\n *\n * @param {object} options Legacy options object\n * @param {string} [options.objectType] The registered object type slug\n * @param {(type: string, params: object) => Promise<Array<object>>} [options.fetchFn] Function to call: (type, params) => Promise<Array>\n * @param {number} [options.debounceMs] Search debounce in milliseconds\n * @param {number} [options.pageSize] Default page size\n * @param {object} [options.defaultSort] Default sort: { key: string, order: 'asc'|'desc' }\n * @return {object} Reactive state and methods\n */\nfunction useLegacyListView(options) {\n\tconst searchTerm = ref('')\n\tconst filters = ref({})\n\tconst sortKey = ref(options.defaultSort?.key || null)\n\tconst sortOrder = ref(options.defaultSort?.order || 'asc')\n\tconst currentPage = ref(1)\n\tconst pageSize = ref(options.pageSize || 20)\n\n\tlet searchTimeout = null\n\n\tfunction buildFetchParams() {\n\t\tconst params = {\n\t\t\t_limit: pageSize.value,\n\t\t\t_page: currentPage.value,\n\t\t}\n\n\t\tif (searchTerm.value) {\n\t\t\tparams._search = searchTerm.value\n\t\t}\n\n\t\tif (sortKey.value) {\n\t\t\tparams._order = { [sortKey.value]: sortOrder.value }\n\t\t}\n\n\t\tfor (const [key, value] of Object.entries(filters.value)) {\n\t\t\tif (value !== null && value !== '' && value !== false) {\n\t\t\t\tparams[key] = value\n\t\t\t}\n\t\t}\n\n\t\treturn params\n\t}\n\n\tasync function fetchData(page) {\n\t\tif (page !== undefined) {\n\t\t\tcurrentPage.value = page\n\t\t}\n\t\tconst params = buildFetchParams()\n\t\tif (options.fetchFn) {\n\t\t\treturn options.fetchFn(options.objectType, params)\n\t\t}\n\t}\n\n\tfunction onSearchInput(value) {\n\t\tsearchTerm.value = value\n\t\tclearTimeout(searchTimeout)\n\t\tsearchTimeout = setTimeout(() => fetchData(1), options.debounceMs || 300)\n\t}\n\n\tfunction toggleSort(key) {\n\t\tif (sortKey.value === key) {\n\t\t\tif (sortOrder.value === 'asc') {\n\t\t\t\tsortOrder.value = 'desc'\n\t\t\t} else {\n\t\t\t\tsortKey.value = null\n\t\t\t\tsortOrder.value = 'asc'\n\t\t\t}\n\t\t} else {\n\t\t\tsortKey.value = key\n\t\t\tsortOrder.value = 'asc'\n\t\t}\n\t\tfetchData(1)\n\t}\n\n\tfunction setFilter(key, value) {\n\t\tfilters.value = { ...filters.value, [key]: value }\n\t\tfetchData(1)\n\t}\n\n\tfunction clearAllFilters() {\n\t\tsearchTerm.value = ''\n\t\tfilters.value = {}\n\t\tsortKey.value = options.defaultSort?.key || null\n\t\tsortOrder.value = options.defaultSort?.order || 'asc'\n\t\tfetchData(1)\n\t}\n\n\tfunction goToPage(page) {\n\t\tcurrentPage.value = page\n\t\tfetchData()\n\t}\n\n\tonBeforeUnmount(() => clearTimeout(searchTimeout))\n\n\treturn {\n\t\tsearchTerm,\n\t\tfilters,\n\t\tsortKey,\n\t\tsortOrder,\n\t\tcurrentPage,\n\t\tpageSize,\n\t\tonSearchInput,\n\t\ttoggleSort,\n\t\tsetFilter,\n\t\tclearAllFilters,\n\t\tgoToPage,\n\t\tfetch: fetchData,\n\t\tbuildFetchParams,\n\t}\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getCurrentUser } from '@nextcloud/auth';
|
|
1
|
+
import { getCurrentUser, getRequestToken } from '@nextcloud/auth';
|
|
2
2
|
import axios__default from '@nextcloud/axios';
|
|
3
3
|
import { generateUrl } from '@nextcloud/router';
|
|
4
4
|
import { ref, computed, watch, getCurrentScope, onScopeDispose, unref } from 'vue';
|
|
@@ -202,7 +202,15 @@ function useObjectPresence(register, schema, objectUuid, options = {}) {
|
|
|
202
202
|
* the browser and survives the navigation, which is the only mechanism
|
|
203
203
|
* that does.
|
|
204
204
|
*
|
|
205
|
+
* 🔴 THE TOKEN RIDES IN THE BODY. A beacon cannot set headers, so the
|
|
206
|
+
* `requesttoken` header axios adds is missing, and the departure route
|
|
207
|
+
* keeps Nextcloud's CSRF check. Nextcloud also reads the token from a
|
|
208
|
+
* POST field, so it goes in a form body. Without it the beacon answers
|
|
209
|
+
* 412; before OpenRegister registered POST on `/presence` it answered 405
|
|
210
|
+
* (seen on cloud.conduction.nl on every dossiq case page).
|
|
211
|
+
*
|
|
205
212
|
* @return {void}
|
|
213
|
+
* @spec openspec/changes/no-calls-to-missing-apps-and-presence-beacon/specs/object-presence/spec.md#requirement-a-closing-tab-departs-through-a-route-that-exists
|
|
206
214
|
*/
|
|
207
215
|
function beaconDepart() {
|
|
208
216
|
const url = presenceUrl();
|
|
@@ -210,9 +218,12 @@ function useObjectPresence(register, schema, objectUuid, options = {}) {
|
|
|
210
218
|
return
|
|
211
219
|
}
|
|
212
220
|
try {
|
|
213
|
-
// No DELETE from a beacon
|
|
214
|
-
//
|
|
215
|
-
|
|
221
|
+
// No DELETE from a beacon, it is always a POST, so the server's
|
|
222
|
+
// POST departure route is used, marked `_method=DELETE`. Both reach
|
|
223
|
+
// one implementation.
|
|
224
|
+
const body = new FormData();
|
|
225
|
+
body.append('requesttoken', getRequestToken() || '');
|
|
226
|
+
navigator.sendBeacon?.(url.replace(/\/presence$/, '/presence?_method=DELETE'), body);
|
|
216
227
|
} catch {
|
|
217
228
|
// Best effort.
|
|
218
229
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useObjectPresence.js","sources":["../../../src/composables/useObjectPresence.js"],"sourcesContent":["// SPDX-License-Identifier: EUPL-1.2\n// Copyright (C) 2026 Conduction B.V.\n\nimport { getCurrentUser } from '@nextcloud/auth'\nimport axios from '@nextcloud/axios'\nimport { generateUrl } from '@nextcloud/router'\nimport { computed, getCurrentScope, onScopeDispose, ref, unref, watch } from 'vue'\nimport { buildObjectKey } from '../store/liveUpdates/eventKeys.js'\nimport { getLiveUpdates } from '../store/liveUpdates/transport.js'\n\n/**\n * How long the server believes a reader after their last beat, in seconds.\n *\n * Mirrored from `PresenceService::WINDOW_SECONDS` and used ONLY as the\n * fallback pace when a server does not answer with its own. The server's\n * `beatSeconds` wins whenever it is present, because the window is its\n * decision and a client pacing itself to a stale copy of it drops off the\n * list while its user is still reading.\n */\nconst FALLBACK_BEAT_SECONDS = 30\n\n/**\n * useObjectPresence — who else has this object open.\n *\n * 🔴 A HEARTBEAT, NOT A CONNECTION. notify_push tells the server nothing about\n * who is looking at what: a socket can be open while the tab showing this\n * object closed ten minutes ago, and a socket can drop while the reader is\n * still there. So the client says \"still here\" on a timer and the server stops\n * believing it after its window. Missing two beats reads as gone, which\n * survives a lost socket where connection tracking does not.\n *\n * 🔑 THE LIST COMES FROM TWO PLACES, AND THAT IS DELIBERATE. Every beat answers\n * the current list, so the component is correct even on an instance with no\n * notify_push at all — it simply learns at beat pace. The push is the\n * OPTIMISATION: it carries the new list on the object's own channel the moment\n * somebody arrives or leaves. Relying on the push alone would make presence a\n * feature that silently does nothing wherever notify_push is not installed,\n * which is most development instances and some production ones.\n *\n * 🔑 DEPARTURE IS SENT TWICE, ON PURPOSE. `tryOnScopeDispose` covers a route\n * change and an unmount; `beforeunload` with `sendBeacon` covers a closed tab,\n * which fires no Vue hook at all. Without the beacon a closed tab lingers for a\n * whole window on everybody else's screen; without the dispose a route change\n * does. Both are cheap and the server is idempotent about it.\n *\n * @param {string|import('vue').Ref<string>} register OpenRegister register slug.\n * @param {string|import('vue').Ref<string>} schema OpenRegister schema slug.\n * @param {string|import('vue').Ref<string>} objectUuid The object being read.\n * @param {object} [options] Optional config.\n * @param {boolean|import('vue').Ref<boolean>|(() => boolean)} [options.enabled] Set false to make the\n * whole thing inert. A ref or getter is read on every use, so a consumer whose answer changes\n * after mount turns the subscription on when it flips.\n * @return {{\n * others: import('vue').ComputedRef<Array<object>>,\n * count: import('vue').ComputedRef<number>,\n * active: import('vue').Ref<boolean>,\n * depart: () => Promise<void>\n * }} The other readers, and a way to leave early.\n */\nexport function useObjectPresence(register, schema, objectUuid, options = {}) {\n\tconst present = ref([])\n\tconst active = ref(false)\n\tlet beatTimer = null\n\tlet subscription = null\n\tlet beatSeconds = FALLBACK_BEAT_SECONDS\n\n\t// A value, a ref or a getter, like the address parts below: a consumer whose\n\t// answer changes after mount (widgets that arrive late) has to be able to say so.\n\tconst enabled = () => unref(typeof options.enabled === 'function' ? options.enabled() : options.enabled) !== false\n\n\t/**\n\t * Read an address part that may be a value, a ref OR a getter.\n\t *\n\t * 🔑 GETTERS MATTER HERE. A detail page reuses one widget host across a\n\t * route change, so the record under a mounted consumer changes. A consumer\n\t * that could only pass a VALUE would leave this beating on the record the\n\t * reader left — the shape that puts a stranger's avatar on your page — and\n\t * `unref` alone does not call a function.\n\t *\n\t * @param {string|import('vue').Ref<string>|(() => string)} source A value, a ref or a getter.\n\t * @return {string} The part.\n\t */\n\tfunction read(source) {\n\t\treturn String((typeof source === 'function' ? source() : unref(source)) || '')\n\t}\n\n\tconst readUuid = () => read(objectUuid)\n\n\t/**\n\t * The presence endpoint of the object currently being read.\n\t *\n\t * Named `presenceUrl` rather than `endpoint` because\n\t * `tests/packaging/api-fetches-are-url-prefixed` recognises module-local\n\t * URL builders BY NAME, alongside `objectsUrl` and `agentsUrl`. A generic\n\t * `endpoint` would have to be exempted globally, which would blanket every\n\t * function of that name in the library.\n\t *\n\t * @return {string} The url, or '' when the object is not resolved yet.\n\t */\n\tfunction presenceUrl() {\n\t\tconst r = read(register)\n\t\tconst s = read(schema)\n\t\tconst i = readUuid()\n\t\tif (!r || !s || !i) {\n\t\t\treturn ''\n\t\t}\n\t\treturn generateUrl(`/apps/openregister/api/objects/${encodeURIComponent(r)}/${encodeURIComponent(s)}/${encodeURIComponent(i)}/presence`)\n\t}\n\n\t/**\n\t * Everybody present except the viewer.\n\t *\n\t * The server already leaves the caller out of the beat's answer, but a\n\t * PUSH carries the whole list — it is one payload for every recipient and\n\t * cannot be personalised. Filtering here is what stops a reader seeing\n\t * their own avatar appear the moment somebody else arrives.\n\t */\n\tconst others = computed(() => {\n\t\tconst me = getCurrentUser()?.uid\n\t\treturn present.value.filter((entry) => !me || entry?.user !== me)\n\t})\n\n\tconst count = computed(() => others.value.length)\n\n\t/**\n\t * Replace the list with whatever the server just said.\n\t *\n\t * @param {Array<object>} list The readers.\n\t * @return {void}\n\t */\n\tfunction adopt(list) {\n\t\tpresent.value = Array.isArray(list) ? list : []\n\t}\n\n\t/**\n\t * Say we are still here, and take the answer.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync function beat() {\n\t\tconst url = presenceUrl()\n\t\tif (!url || !enabled()) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst { data } = await axios.put(url)\n\t\t\tadopt(data?.present)\n\t\t\tactive.value = true\n\t\t\t// The server's pace, when it offers one. See FALLBACK_BEAT_SECONDS.\n\t\t\tif (Number.isFinite(data?.beatSeconds) && data.beatSeconds > 0) {\n\t\t\t\tbeatSeconds = data.beatSeconds\n\t\t\t}\n\t\t} catch {\n\t\t\t// A failed beat is NOT an empty list. The reader drops off other\n\t\t\t// people's screens in one window either way, and clearing our own\n\t\t\t// list here would tell this reader they are alone on a page two\n\t\t\t// colleagues are looking at.\n\t\t\tactive.value = false\n\t\t}\n\t}\n\n\t/**\n\t * Say we have gone.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync function depart() {\n\t\tstop()\n\t\tawait departFrom(readUuid())\n\t\tpresent.value = []\n\t\tactive.value = false\n\t}\n\n\t/**\n\t * Leave one specific record, which may no longer be the current one.\n\t *\n\t * Takes the uuid rather than reading it, because the watcher calls this\n\t * for the record just LEFT, after the address has already moved on.\n\t *\n\t * @param {string} uuid The record to leave.\n\t * @return {Promise<void>}\n\t */\n\tasync function departFrom(uuid) {\n\t\tconst r = read(register)\n\t\tconst s = read(schema)\n\t\tif (!r || !s || !uuid) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tawait axios.delete(generateUrl(`/apps/openregister/api/objects/${encodeURIComponent(r)}/${encodeURIComponent(s)}/${encodeURIComponent(uuid)}/presence`))\n\t\t} catch {\n\t\t\t// Best effort: the window expires us anyway.\n\t\t}\n\t}\n\n\t/**\n\t * Tell the server we are gone from a tab that is closing.\n\t *\n\t * `sendBeacon` because `beforeunload` gives no time for a promise: an\n\t * axios call from here is cancelled with the page. A beacon is queued by\n\t * the browser and survives the navigation, which is the only mechanism\n\t * that does.\n\t *\n\t * @return {void}\n\t */\n\tfunction beaconDepart() {\n\t\tconst url = presenceUrl()\n\t\tif (!url || !active.value) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\t// No DELETE from a beacon — it is always a POST — so the server's\n\t\t\t// own POST release path is used. Both reach one implementation.\n\t\t\tnavigator.sendBeacon?.(url.replace(/\\/presence$/, '/presence?_method=DELETE'))\n\t\t} catch {\n\t\t\t// Best effort.\n\t\t}\n\t}\n\n\t/**\n\t * Begin beating and listen for other people's arrivals.\n\t *\n\t * @return {void}\n\t */\n\tfunction start() {\n\t\tif (!enabled() || beatTimer) {\n\t\t\treturn\n\t\t}\n\n\t\tbeat()\n\t\tbeatTimer = setInterval(beat, beatSeconds * 1000)\n\n\t\ttry {\n\t\t\tconst live = getLiveUpdates()\n\t\t\tsubscription = live.subscribe(\n\t\t\t\tbuildObjectKey(readUuid()),\n\t\t\t\t(event, body) => {\n\t\t\t\t\t// ONLY the presence event. The same channel carries create,\n\t\t\t\t\t// update and delete, and adopting a lifecycle payload's\n\t\t\t\t\t// absent `present` key would blank the list on every save.\n\t\t\t\t\tif (body?.action === 'presence') {\n\t\t\t\t\t\tadopt(body?.present)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{ isObject: true },\n\t\t\t)\n\t\t} catch {\n\t\t\t// No transport is not an error: the beats still answer the list,\n\t\t\t// and presence simply updates at beat pace.\n\t\t\tsubscription = null\n\t\t}\n\t}\n\n\t/**\n\t * Stop beating and listening.\n\t *\n\t * @return {void}\n\t */\n\tfunction stop() {\n\t\tif (beatTimer) {\n\t\t\tclearInterval(beatTimer)\n\t\t\tbeatTimer = null\n\t\t}\n\t\tif (subscription) {\n\t\t\ttry {\n\t\t\t\tgetLiveUpdates().unsubscribe(subscription)\n\t\t\t} catch {\n\t\t\t\t// Already gone.\n\t\t\t}\n\t\t\tsubscription = null\n\t\t}\n\t}\n\n\t// 🔴 STARTED HERE, NOT IN `onMounted`. `onMounted` only fires inside a\n\t// COMPONENT: called from a plain `effectScope`, or from a composable a\n\t// consumer wires up outside setup, it warns and never runs, and presence\n\t// would be a feature that silently does nothing in exactly the places\n\t// somebody would try it first. `tryOnScopeDispose` below works in both, so\n\t// the pairing is scope-based at both ends rather than half component and\n\t// half scope.\n\t//\n\t// Beating before mount is also the behaviour we want: a reader has the\n\t// object open the moment its page is being built, not one frame later.\n\twindow.addEventListener('beforeunload', beaconDepart)\n\tstart()\n\n\t// 🔴 THE RECORD CAN CHANGE UNDER A MOUNTED CONSUMER. A detail page reuses\n\t// one widget host across a route change, so without this the beats would go\n\t// on naming the record the reader has left: they would appear present on a\n\t// page they closed, and absent on the one they are reading. Depart the old\n\t// one first, so the people still on it are told, rather than waiting a whole\n\t// window to find out.\n\twatch(\n\t\t() => readUuid(),\n\t\t(next, previous) => {\n\t\t\tif (next === previous) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tstop()\n\t\t\tif (previous) {\n\t\t\t\tdepartFrom(previous)\n\t\t\t}\n\t\t\tpresent.value = []\n\t\t\tactive.value = false\n\t\t\tstart()\n\t\t},\n\t)\n\n\t// A consumer may only learn it needs presence AFTER mount — a detail page\n\t// whose `widgets` arrive with the manifest reads `false` here on the first\n\t// pass. Without this the beat never starts for it, and the row it gates is\n\t// dropped permanently rather than until somebody arrives.\n\twatch(\n\t\t() => enabled(),\n\t\t(on, was) => {\n\t\t\tif (on === was) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (on) {\n\t\t\t\tstart()\n\t\t\t} else {\n\t\t\t\tdepart()\n\t\t\t}\n\t\t},\n\t)\n\n\t// 🔴 VUE'S OWN `onScopeDispose`, NOT `@vueuse/core`'s `tryOnScopeDispose`.\n\t// Measured in this repo's jest on 2026-09-18: a `tryOnScopeDispose`\n\t// callback registered inside `effectScope().run()` does NOT fire on\n\t// `scope.stop()`, while Vue's own does. The cause is the dual-package\n\t// hazard — vueuse resolves its own copy of Vue and asks a different\n\t// `getCurrentScope()` — so the registration silently goes nowhere.\n\t//\n\t// A departure that never fires is the exact silent no-op this whole\n\t// feature is built to avoid: the reader stays on everybody's list for a\n\t// full window after they navigate away. One fewer indirection also means\n\t// one fewer thing that can resolve to the wrong instance.\n\t//\n\t// Guarded, because a consumer may legitimately call this outside a scope;\n\t// there it simply has no automatic cleanup and relies on the beacon and\n\t// the server's window, which is what the guard makes explicit rather than\n\t// leaving to a warning nobody reads.\n\tif (getCurrentScope()) {\n\t\tonScopeDispose(() => {\n\t\t\twindow.removeEventListener('beforeunload', beaconDepart)\n\t\t\t// Fire and forget: the scope is gone, there is nobody to await it.\n\t\t\tdepart()\n\t\t})\n\t}\n\n\treturn { others, count, active, depart }\n}\n"],"names":["axios"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useObjectPresence.js","sources":["../../../src/composables/useObjectPresence.js"],"sourcesContent":["// SPDX-License-Identifier: EUPL-1.2\n// Copyright (C) 2026 Conduction B.V.\n\nimport { getCurrentUser, getRequestToken } from '@nextcloud/auth'\nimport axios from '@nextcloud/axios'\nimport { generateUrl } from '@nextcloud/router'\nimport { computed, getCurrentScope, onScopeDispose, ref, unref, watch } from 'vue'\nimport { buildObjectKey } from '../store/liveUpdates/eventKeys.js'\nimport { getLiveUpdates } from '../store/liveUpdates/transport.js'\n\n/**\n * How long the server believes a reader after their last beat, in seconds.\n *\n * Mirrored from `PresenceService::WINDOW_SECONDS` and used ONLY as the\n * fallback pace when a server does not answer with its own. The server's\n * `beatSeconds` wins whenever it is present, because the window is its\n * decision and a client pacing itself to a stale copy of it drops off the\n * list while its user is still reading.\n */\nconst FALLBACK_BEAT_SECONDS = 30\n\n/**\n * useObjectPresence — who else has this object open.\n *\n * 🔴 A HEARTBEAT, NOT A CONNECTION. notify_push tells the server nothing about\n * who is looking at what: a socket can be open while the tab showing this\n * object closed ten minutes ago, and a socket can drop while the reader is\n * still there. So the client says \"still here\" on a timer and the server stops\n * believing it after its window. Missing two beats reads as gone, which\n * survives a lost socket where connection tracking does not.\n *\n * 🔑 THE LIST COMES FROM TWO PLACES, AND THAT IS DELIBERATE. Every beat answers\n * the current list, so the component is correct even on an instance with no\n * notify_push at all — it simply learns at beat pace. The push is the\n * OPTIMISATION: it carries the new list on the object's own channel the moment\n * somebody arrives or leaves. Relying on the push alone would make presence a\n * feature that silently does nothing wherever notify_push is not installed,\n * which is most development instances and some production ones.\n *\n * 🔑 DEPARTURE IS SENT TWICE, ON PURPOSE. `tryOnScopeDispose` covers a route\n * change and an unmount; `beforeunload` with `sendBeacon` covers a closed tab,\n * which fires no Vue hook at all. Without the beacon a closed tab lingers for a\n * whole window on everybody else's screen; without the dispose a route change\n * does. Both are cheap and the server is idempotent about it.\n *\n * @param {string|import('vue').Ref<string>} register OpenRegister register slug.\n * @param {string|import('vue').Ref<string>} schema OpenRegister schema slug.\n * @param {string|import('vue').Ref<string>} objectUuid The object being read.\n * @param {object} [options] Optional config.\n * @param {boolean|import('vue').Ref<boolean>|(() => boolean)} [options.enabled] Set false to make the\n * whole thing inert. A ref or getter is read on every use, so a consumer whose answer changes\n * after mount turns the subscription on when it flips.\n * @return {{\n * others: import('vue').ComputedRef<Array<object>>,\n * count: import('vue').ComputedRef<number>,\n * active: import('vue').Ref<boolean>,\n * depart: () => Promise<void>\n * }} The other readers, and a way to leave early.\n */\nexport function useObjectPresence(register, schema, objectUuid, options = {}) {\n\tconst present = ref([])\n\tconst active = ref(false)\n\tlet beatTimer = null\n\tlet subscription = null\n\tlet beatSeconds = FALLBACK_BEAT_SECONDS\n\n\t// A value, a ref or a getter, like the address parts below: a consumer whose\n\t// answer changes after mount (widgets that arrive late) has to be able to say so.\n\tconst enabled = () => unref(typeof options.enabled === 'function' ? options.enabled() : options.enabled) !== false\n\n\t/**\n\t * Read an address part that may be a value, a ref OR a getter.\n\t *\n\t * 🔑 GETTERS MATTER HERE. A detail page reuses one widget host across a\n\t * route change, so the record under a mounted consumer changes. A consumer\n\t * that could only pass a VALUE would leave this beating on the record the\n\t * reader left — the shape that puts a stranger's avatar on your page — and\n\t * `unref` alone does not call a function.\n\t *\n\t * @param {string|import('vue').Ref<string>|(() => string)} source A value, a ref or a getter.\n\t * @return {string} The part.\n\t */\n\tfunction read(source) {\n\t\treturn String((typeof source === 'function' ? source() : unref(source)) || '')\n\t}\n\n\tconst readUuid = () => read(objectUuid)\n\n\t/**\n\t * The presence endpoint of the object currently being read.\n\t *\n\t * Named `presenceUrl` rather than `endpoint` because\n\t * `tests/packaging/api-fetches-are-url-prefixed` recognises module-local\n\t * URL builders BY NAME, alongside `objectsUrl` and `agentsUrl`. A generic\n\t * `endpoint` would have to be exempted globally, which would blanket every\n\t * function of that name in the library.\n\t *\n\t * @return {string} The url, or '' when the object is not resolved yet.\n\t */\n\tfunction presenceUrl() {\n\t\tconst r = read(register)\n\t\tconst s = read(schema)\n\t\tconst i = readUuid()\n\t\tif (!r || !s || !i) {\n\t\t\treturn ''\n\t\t}\n\t\treturn generateUrl(`/apps/openregister/api/objects/${encodeURIComponent(r)}/${encodeURIComponent(s)}/${encodeURIComponent(i)}/presence`)\n\t}\n\n\t/**\n\t * Everybody present except the viewer.\n\t *\n\t * The server already leaves the caller out of the beat's answer, but a\n\t * PUSH carries the whole list — it is one payload for every recipient and\n\t * cannot be personalised. Filtering here is what stops a reader seeing\n\t * their own avatar appear the moment somebody else arrives.\n\t */\n\tconst others = computed(() => {\n\t\tconst me = getCurrentUser()?.uid\n\t\treturn present.value.filter((entry) => !me || entry?.user !== me)\n\t})\n\n\tconst count = computed(() => others.value.length)\n\n\t/**\n\t * Replace the list with whatever the server just said.\n\t *\n\t * @param {Array<object>} list The readers.\n\t * @return {void}\n\t */\n\tfunction adopt(list) {\n\t\tpresent.value = Array.isArray(list) ? list : []\n\t}\n\n\t/**\n\t * Say we are still here, and take the answer.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync function beat() {\n\t\tconst url = presenceUrl()\n\t\tif (!url || !enabled()) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst { data } = await axios.put(url)\n\t\t\tadopt(data?.present)\n\t\t\tactive.value = true\n\t\t\t// The server's pace, when it offers one. See FALLBACK_BEAT_SECONDS.\n\t\t\tif (Number.isFinite(data?.beatSeconds) && data.beatSeconds > 0) {\n\t\t\t\tbeatSeconds = data.beatSeconds\n\t\t\t}\n\t\t} catch {\n\t\t\t// A failed beat is NOT an empty list. The reader drops off other\n\t\t\t// people's screens in one window either way, and clearing our own\n\t\t\t// list here would tell this reader they are alone on a page two\n\t\t\t// colleagues are looking at.\n\t\t\tactive.value = false\n\t\t}\n\t}\n\n\t/**\n\t * Say we have gone.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync function depart() {\n\t\tstop()\n\t\tawait departFrom(readUuid())\n\t\tpresent.value = []\n\t\tactive.value = false\n\t}\n\n\t/**\n\t * Leave one specific record, which may no longer be the current one.\n\t *\n\t * Takes the uuid rather than reading it, because the watcher calls this\n\t * for the record just LEFT, after the address has already moved on.\n\t *\n\t * @param {string} uuid The record to leave.\n\t * @return {Promise<void>}\n\t */\n\tasync function departFrom(uuid) {\n\t\tconst r = read(register)\n\t\tconst s = read(schema)\n\t\tif (!r || !s || !uuid) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tawait axios.delete(generateUrl(`/apps/openregister/api/objects/${encodeURIComponent(r)}/${encodeURIComponent(s)}/${encodeURIComponent(uuid)}/presence`))\n\t\t} catch {\n\t\t\t// Best effort: the window expires us anyway.\n\t\t}\n\t}\n\n\t/**\n\t * Tell the server we are gone from a tab that is closing.\n\t *\n\t * `sendBeacon` because `beforeunload` gives no time for a promise: an\n\t * axios call from here is cancelled with the page. A beacon is queued by\n\t * the browser and survives the navigation, which is the only mechanism\n\t * that does.\n\t *\n\t * 🔴 THE TOKEN RIDES IN THE BODY. A beacon cannot set headers, so the\n\t * `requesttoken` header axios adds is missing, and the departure route\n\t * keeps Nextcloud's CSRF check. Nextcloud also reads the token from a\n\t * POST field, so it goes in a form body. Without it the beacon answers\n\t * 412; before OpenRegister registered POST on `/presence` it answered 405\n\t * (seen on cloud.conduction.nl on every dossiq case page).\n\t *\n\t * @return {void}\n\t * @spec openspec/changes/no-calls-to-missing-apps-and-presence-beacon/specs/object-presence/spec.md#requirement-a-closing-tab-departs-through-a-route-that-exists\n\t */\n\tfunction beaconDepart() {\n\t\tconst url = presenceUrl()\n\t\tif (!url || !active.value) {\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\t// No DELETE from a beacon, it is always a POST, so the server's\n\t\t\t// POST departure route is used, marked `_method=DELETE`. Both reach\n\t\t\t// one implementation.\n\t\t\tconst body = new FormData()\n\t\t\tbody.append('requesttoken', getRequestToken() || '')\n\t\t\tnavigator.sendBeacon?.(url.replace(/\\/presence$/, '/presence?_method=DELETE'), body)\n\t\t} catch {\n\t\t\t// Best effort.\n\t\t}\n\t}\n\n\t/**\n\t * Begin beating and listen for other people's arrivals.\n\t *\n\t * @return {void}\n\t */\n\tfunction start() {\n\t\tif (!enabled() || beatTimer) {\n\t\t\treturn\n\t\t}\n\n\t\tbeat()\n\t\tbeatTimer = setInterval(beat, beatSeconds * 1000)\n\n\t\ttry {\n\t\t\tconst live = getLiveUpdates()\n\t\t\tsubscription = live.subscribe(\n\t\t\t\tbuildObjectKey(readUuid()),\n\t\t\t\t(event, body) => {\n\t\t\t\t\t// ONLY the presence event. The same channel carries create,\n\t\t\t\t\t// update and delete, and adopting a lifecycle payload's\n\t\t\t\t\t// absent `present` key would blank the list on every save.\n\t\t\t\t\tif (body?.action === 'presence') {\n\t\t\t\t\t\tadopt(body?.present)\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\t{ isObject: true },\n\t\t\t)\n\t\t} catch {\n\t\t\t// No transport is not an error: the beats still answer the list,\n\t\t\t// and presence simply updates at beat pace.\n\t\t\tsubscription = null\n\t\t}\n\t}\n\n\t/**\n\t * Stop beating and listening.\n\t *\n\t * @return {void}\n\t */\n\tfunction stop() {\n\t\tif (beatTimer) {\n\t\t\tclearInterval(beatTimer)\n\t\t\tbeatTimer = null\n\t\t}\n\t\tif (subscription) {\n\t\t\ttry {\n\t\t\t\tgetLiveUpdates().unsubscribe(subscription)\n\t\t\t} catch {\n\t\t\t\t// Already gone.\n\t\t\t}\n\t\t\tsubscription = null\n\t\t}\n\t}\n\n\t// 🔴 STARTED HERE, NOT IN `onMounted`. `onMounted` only fires inside a\n\t// COMPONENT: called from a plain `effectScope`, or from a composable a\n\t// consumer wires up outside setup, it warns and never runs, and presence\n\t// would be a feature that silently does nothing in exactly the places\n\t// somebody would try it first. `tryOnScopeDispose` below works in both, so\n\t// the pairing is scope-based at both ends rather than half component and\n\t// half scope.\n\t//\n\t// Beating before mount is also the behaviour we want: a reader has the\n\t// object open the moment its page is being built, not one frame later.\n\twindow.addEventListener('beforeunload', beaconDepart)\n\tstart()\n\n\t// 🔴 THE RECORD CAN CHANGE UNDER A MOUNTED CONSUMER. A detail page reuses\n\t// one widget host across a route change, so without this the beats would go\n\t// on naming the record the reader has left: they would appear present on a\n\t// page they closed, and absent on the one they are reading. Depart the old\n\t// one first, so the people still on it are told, rather than waiting a whole\n\t// window to find out.\n\twatch(\n\t\t() => readUuid(),\n\t\t(next, previous) => {\n\t\t\tif (next === previous) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tstop()\n\t\t\tif (previous) {\n\t\t\t\tdepartFrom(previous)\n\t\t\t}\n\t\t\tpresent.value = []\n\t\t\tactive.value = false\n\t\t\tstart()\n\t\t},\n\t)\n\n\t// A consumer may only learn it needs presence AFTER mount — a detail page\n\t// whose `widgets` arrive with the manifest reads `false` here on the first\n\t// pass. Without this the beat never starts for it, and the row it gates is\n\t// dropped permanently rather than until somebody arrives.\n\twatch(\n\t\t() => enabled(),\n\t\t(on, was) => {\n\t\t\tif (on === was) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (on) {\n\t\t\t\tstart()\n\t\t\t} else {\n\t\t\t\tdepart()\n\t\t\t}\n\t\t},\n\t)\n\n\t// 🔴 VUE'S OWN `onScopeDispose`, NOT `@vueuse/core`'s `tryOnScopeDispose`.\n\t// Measured in this repo's jest on 2026-09-18: a `tryOnScopeDispose`\n\t// callback registered inside `effectScope().run()` does NOT fire on\n\t// `scope.stop()`, while Vue's own does. The cause is the dual-package\n\t// hazard — vueuse resolves its own copy of Vue and asks a different\n\t// `getCurrentScope()` — so the registration silently goes nowhere.\n\t//\n\t// A departure that never fires is the exact silent no-op this whole\n\t// feature is built to avoid: the reader stays on everybody's list for a\n\t// full window after they navigate away. One fewer indirection also means\n\t// one fewer thing that can resolve to the wrong instance.\n\t//\n\t// Guarded, because a consumer may legitimately call this outside a scope;\n\t// there it simply has no automatic cleanup and relies on the beacon and\n\t// the server's window, which is what the guard makes explicit rather than\n\t// leaving to a warning nobody reads.\n\tif (getCurrentScope()) {\n\t\tonScopeDispose(() => {\n\t\t\twindow.removeEventListener('beforeunload', beaconDepart)\n\t\t\t// Fire and forget: the scope is gone, there is nobody to await it.\n\t\t\tdepart()\n\t\t})\n\t}\n\n\treturn { others, count, active, depart }\n}\n"],"names":["axios"],"mappings":"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"}
|
package/dist/esm/index.js
CHANGED
|
@@ -28,6 +28,7 @@ export { registerTranslations } from './l10n/index.js';
|
|
|
28
28
|
export { CnHttpError, cnFetch, cnFetchJson, configureCnFetch } from './utils/cnFetch.js';
|
|
29
29
|
export { isLockedByCurrentUser, isObjectLocked, lockHolder, readLockPayload, resolveObjectLock } from './utils/objectLock.js';
|
|
30
30
|
export { dispatchObjectsChanged } from './utils/objectSignals.js';
|
|
31
|
+
export { lensUnavailableText, readLensReports } from './utils/lensAvailability.js';
|
|
31
32
|
export { validateManifest, validateManifestV2 } from './utils/validateManifest.js';
|
|
32
33
|
export { clearResolveCache, resolveManifestSentinels } from './utils/resolveManifestSentinels.js';
|
|
33
34
|
export { clearRouteSentinelWarnings, resolveRouteSentinels } from './utils/resolveRouteSentinels.js';
|
|
@@ -457,7 +458,7 @@ export { columnsFromSchema, fieldsFromSchema, filtersFromSchema, formatValue, va
|
|
|
457
458
|
export { findRepeatingWrite, journeyItemTargets } from './utils/journeyRepeatingWrite.js';
|
|
458
459
|
export { DYNAMIC_KEY_PREFIX, EXTENDS_FORM_KEY, PREFILL_KEY, splitDynamicFormData, usesArrayValues, valueArrayFor, valueRecordsFor } from './utils/dynamicProperties.js';
|
|
459
460
|
export { SchemaBreakingChangeError, SchemaHasObjectsError, deleteSchema, describeSchemaChange, saveSchema } from './utils/schemaApi.js';
|
|
460
|
-
export { filterWidgetsByVisibility, getCurrentUserGroups, getCurrentUserId, isWidgetVisible, resetVisibilityCache } from './utils/widgetVisibility.js';
|
|
461
|
+
export { filterWidgetsByVisibility, getCurrentUserGroups, getCurrentUserId, isWidgetVisible, peekCurrentUserGroups, resetVisibilityCache } from './utils/widgetVisibility.js';
|
|
461
462
|
export { safeHref, safeImageSrc, safeSvgPath } from './utils/safeHref.js';
|
|
462
463
|
export { followLinkClick, isModifiedClick, openRowTarget, resolveHref } from './utils/linkNavigation.js';
|
|
463
464
|
export { isNewTabHandled, isRowMiddleClick } from './utils/rowAuxClick.js';
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/esm/l10n/en.json.js
CHANGED
|
@@ -113,6 +113,7 @@ var translations = {
|
|
|
113
113
|
"Saved just now": "Saved just now",
|
|
114
114
|
"Saved {time}": "Saved {time}",
|
|
115
115
|
Saved: "Saved",
|
|
116
|
+
"Draft kept on this device": "Draft kept on this device",
|
|
116
117
|
Publish: "Publish",
|
|
117
118
|
"Edit {title} (draft)": "Edit {title} (draft)",
|
|
118
119
|
"Also search inside files": "Also search inside files",
|
|
@@ -452,6 +453,12 @@ var translations = {
|
|
|
452
453
|
"NC Activity is currently unavailable.": "NC Activity is currently unavailable.",
|
|
453
454
|
"Could not load activity.": "Could not load activity.",
|
|
454
455
|
Today: "Today",
|
|
456
|
+
"Last 8 hours": "Last 8 hours",
|
|
457
|
+
"Last 24 hours": "Last 24 hours",
|
|
458
|
+
"Last 7 days": "Last 7 days",
|
|
459
|
+
"Last 30 days": "Last 30 days",
|
|
460
|
+
"Last 90 days": "Last 90 days",
|
|
461
|
+
"Custom range": "Custom range",
|
|
455
462
|
Yesterday: "Yesterday",
|
|
456
463
|
Earlier: "Earlier",
|
|
457
464
|
System: "System",
|
|
@@ -1289,7 +1296,19 @@ var translations = {
|
|
|
1289
1296
|
"Test environment": "Test environment",
|
|
1290
1297
|
"Acceptance environment": "Acceptance environment",
|
|
1291
1298
|
"About {label}": "About {label}",
|
|
1292
|
-
"Could not load the form for this item.": "Could not load the form for this item."
|
|
1299
|
+
"Could not load the form for this item.": "Could not load the form for this item.",
|
|
1300
|
+
"Drop to add": "Drop to add",
|
|
1301
|
+
"Or drag files onto this list.": "Or drag files onto this list.",
|
|
1302
|
+
"This server does not keep track of what you open.": "This server does not keep track of what you open.",
|
|
1303
|
+
"Log in to see what you opened recently.": "Log in to see what you opened recently.",
|
|
1304
|
+
"Your recent items are not available right now.": "Your recent items are not available right now.",
|
|
1305
|
+
yesterday: "yesterday",
|
|
1306
|
+
tomorrow: "tomorrow",
|
|
1307
|
+
"Filter by {column}": "Filter by {column}",
|
|
1308
|
+
"Filter by {column}, active": "Filter by {column}, active",
|
|
1309
|
+
"Turn notifications off": "Turn notifications off",
|
|
1310
|
+
"Turn notifications on": "Turn notifications on",
|
|
1311
|
+
"Could not change notifications.": "Could not change notifications."
|
|
1293
1312
|
};
|
|
1294
1313
|
var plurals = {
|
|
1295
1314
|
"%n year": [
|
|
@@ -1343,6 +1362,18 @@ var plurals = {
|
|
|
1343
1362
|
"You see {count} capability of {total}.": [
|
|
1344
1363
|
"You see {count} capability of {total}.",
|
|
1345
1364
|
"You see {count} capabilities of {total}."
|
|
1365
|
+
],
|
|
1366
|
+
"%n file added": [
|
|
1367
|
+
"%n file added",
|
|
1368
|
+
"%n files added"
|
|
1369
|
+
],
|
|
1370
|
+
"{count} day ago": [
|
|
1371
|
+
"{count} day ago",
|
|
1372
|
+
"{count} days ago"
|
|
1373
|
+
],
|
|
1374
|
+
"in {count} day": [
|
|
1375
|
+
"in {count} day",
|
|
1376
|
+
"in {count} days"
|
|
1346
1377
|
]
|
|
1347
1378
|
};
|
|
1348
1379
|
var en = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|
package/dist/esm/l10n/nl.json.js
CHANGED
|
@@ -113,6 +113,7 @@ var translations = {
|
|
|
113
113
|
"Saved just now": "Zojuist opgeslagen",
|
|
114
114
|
"Saved {time}": "Opgeslagen {time}",
|
|
115
115
|
Saved: "Opgeslagen",
|
|
116
|
+
"Draft kept on this device": "Concept bewaard op dit apparaat",
|
|
116
117
|
"Edit {title} (draft)": "{title} bewerken (concept)",
|
|
117
118
|
"Also search inside files": "Zoek ook in bestanden",
|
|
118
119
|
"File matches are limited to the best 50.": "Bestandstreffers zijn beperkt tot de beste 50.",
|
|
@@ -451,6 +452,12 @@ var translations = {
|
|
|
451
452
|
"NC Activity is currently unavailable.": "NC Activiteit is momenteel niet beschikbaar.",
|
|
452
453
|
"Could not load activity.": "Kon activiteit niet laden.",
|
|
453
454
|
Today: "Vandaag",
|
|
455
|
+
"Last 8 hours": "Laatste 8 uur",
|
|
456
|
+
"Last 24 hours": "Laatste 24 uur",
|
|
457
|
+
"Last 7 days": "Laatste 7 dagen",
|
|
458
|
+
"Last 30 days": "Laatste 30 dagen",
|
|
459
|
+
"Last 90 days": "Laatste 90 dagen",
|
|
460
|
+
"Custom range": "Aangepaste periode",
|
|
454
461
|
Yesterday: "Gisteren",
|
|
455
462
|
Earlier: "Eerder",
|
|
456
463
|
System: "Systeem",
|
|
@@ -1340,7 +1347,19 @@ var translations = {
|
|
|
1340
1347
|
"Test environment": "Testomgeving",
|
|
1341
1348
|
"Acceptance environment": "Acceptatieomgeving",
|
|
1342
1349
|
"About {label}": "Over {label}",
|
|
1343
|
-
"Could not load the form for this item.": "Kon het formulier voor dit item niet laden."
|
|
1350
|
+
"Could not load the form for this item.": "Kon het formulier voor dit item niet laden.",
|
|
1351
|
+
"Drop to add": "Laat los om toe te voegen",
|
|
1352
|
+
"Or drag files onto this list.": "Of sleep bestanden op deze lijst.",
|
|
1353
|
+
"This server does not keep track of what you open.": "Deze server houdt niet bij welke items je opent.",
|
|
1354
|
+
"Log in to see what you opened recently.": "Log in om te zien wat je onlangs opende.",
|
|
1355
|
+
"Your recent items are not available right now.": "Je recente items zijn nu niet beschikbaar.",
|
|
1356
|
+
yesterday: "gisteren",
|
|
1357
|
+
tomorrow: "morgen",
|
|
1358
|
+
"Filter by {column}": "Filteren op {column}",
|
|
1359
|
+
"Filter by {column}, active": "Filteren op {column}, actief",
|
|
1360
|
+
"Turn notifications off": "Meldingen uitzetten",
|
|
1361
|
+
"Turn notifications on": "Meldingen aanzetten",
|
|
1362
|
+
"Could not change notifications.": "Meldingen konden niet worden gewijzigd."
|
|
1344
1363
|
};
|
|
1345
1364
|
var plurals = {
|
|
1346
1365
|
"%n year": [
|
|
@@ -1394,6 +1413,18 @@ var plurals = {
|
|
|
1394
1413
|
"You see {count} capability of {total}.": [
|
|
1395
1414
|
"Je ziet {count} functionaliteit van {total}.",
|
|
1396
1415
|
"Je ziet {count} functionaliteiten van {total}."
|
|
1416
|
+
],
|
|
1417
|
+
"%n file added": [
|
|
1418
|
+
"%n bestand toegevoegd",
|
|
1419
|
+
"%n bestanden toegevoegd"
|
|
1420
|
+
],
|
|
1421
|
+
"{count} day ago": [
|
|
1422
|
+
"{count} dag geleden",
|
|
1423
|
+
"{count} dagen geleden"
|
|
1424
|
+
],
|
|
1425
|
+
"in {count} day": [
|
|
1426
|
+
"over {count} dag",
|
|
1427
|
+
"over {count} dagen"
|
|
1397
1428
|
]
|
|
1398
1429
|
};
|
|
1399
1430
|
var nl = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"nl.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"nl.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|