@conduction/nextcloud-vue 2.106.0 → 2.107.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/CnActionDialog/CnActionDialog.vue.js +9 -0
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js.map +1 -0
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +338 -0
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -0
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +249 -0
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -0
- package/dist/esm/components/CnAdvancedFormDialog/CnPropertiesTab.vue2.js +2 -2
- package/dist/esm/components/CnAdvancedFormDialog/CnPropertiesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +145 -138
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +102 -2
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +83 -6
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +4 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
- package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +389 -20
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +68 -35
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +22 -2
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +69 -5
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js +38 -5
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +7 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +8 -0
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js.map +1 -0
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +297 -0
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -0
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js +24 -0
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -0
- package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue.js +9 -0
- package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue.js.map +1 -0
- package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue2.js +95 -0
- package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue2.js.map +1 -0
- package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue3.js +123 -0
- package/dist/esm/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue3.js.map +1 -0
- package/dist/esm/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue.js +1 -1
- package/dist/esm/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue2.js +13 -5
- package/dist/esm/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue3.js +3 -1
- package/dist/esm/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewShareFields/CnSavedViewShareFields.vue.js +1 -1
- package/dist/esm/components/CnSavedViewShareFields/CnSavedViewShareFields.vue2.js +49 -23
- package/dist/esm/components/CnSavedViewShareFields/CnSavedViewShareFields.vue2.js.map +1 -1
- package/dist/esm/components/CnSavedViewShareFields/CnSavedViewShareFields.vue3.js +74 -27
- package/dist/esm/components/CnSavedViewShareFields/CnSavedViewShareFields.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue.js +0 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js +18 -413
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +27 -214
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue.js +9 -0
- package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue.js.map +1 -0
- package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue2.js +190 -0
- package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue2.js.map +1 -0
- package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue3.js +270 -0
- package/dist/esm/components/CnSavedViewsPanel/CnSavedViewsPanel.vue3.js.map +1 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +56 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +56 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/mixins/savedViewsList.js +235 -0
- package/dist/esm/mixins/savedViewsList.js.map +1 -0
- package/dist/esm/nextcloud-vue.css +348 -108
- package/dist/esm/utils/actionsDispatcher.js +2 -2
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/builtInModals.js +39 -0
- package/dist/esm/utils/builtInModals.js.map +1 -0
- package/dist/esm/utils/mapMarkerColor.js +121 -0
- package/dist/esm/utils/mapMarkerColor.js.map +1 -0
- package/dist/esm/utils/mergeManifestDelta.js +4 -0
- package/dist/esm/utils/mergeManifestDelta.js.map +1 -1
- package/dist/esm/utils/savedViewHelpers.js +59 -7
- package/dist/esm/utils/savedViewHelpers.js.map +1 -1
- package/dist/esm/utils/savedViewMenu.js +154 -0
- package/dist/esm/utils/savedViewMenu.js.map +1 -0
- package/dist/esm/utils/searchGroupSharees.js +50 -15
- package/dist/esm/utils/searchGroupSharees.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +13351 -10855
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +348 -108
- package/l10n/en.json +56 -1
- package/l10n/nl.json +56 -1
- package/package.json +1 -1
- package/src/components/CnActionDialog/CnActionDialog.vue +545 -0
- package/src/components/CnActionDialog/index.js +12 -0
- package/src/components/CnAppNav/CnAppNav.vue +17 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +102 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +86 -7
- package/src/components/CnIndexPage/CnIndexPage.vue +409 -23
- package/src/components/CnIndexPage/useSelfFetchList.js +21 -1
- package/src/components/CnIndexSidebar/CnIndexSidebar.md +2 -0
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +91 -1
- package/src/components/CnMapWidget/CnMapWidget.vue +16 -1
- package/src/components/CnPartyPicker/CnPartyPicker.vue +317 -0
- package/src/components/CnPartyPicker/index.js +11 -0
- package/src/components/CnSavedViewEditDialog/CnSavedViewEditDialog.vue +192 -0
- package/src/components/CnSavedViewEditDialog/index.js +11 -0
- package/src/components/CnSavedViewShareDialog/CnSavedViewShareDialog.vue +21 -6
- package/src/components/CnSavedViewShareFields/CnSavedViewShareFields.vue +130 -37
- package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +36 -599
- package/src/components/CnSavedViewsPanel/CnSavedViewsPanel.vue +459 -0
- package/src/components/CnSavedViewsPanel/index.js +11 -0
- package/src/components/index.js +4 -0
- package/src/css/index-page.css +16 -0
- package/src/css/look-board-index.css +5 -3
- package/src/css/look-board.css +18 -3
- package/src/index.js +4 -0
- package/src/mixins/savedViewsList.js +231 -0
- package/src/schemas/app-manifest-v2.schema.json +98 -3
- package/src/utils/actionsDispatcher.js +2 -2
- package/src/utils/builtInModals.js +35 -0
- package/src/utils/mapMarkerColor.js +117 -0
- package/src/utils/mergeManifestDelta.js +4 -0
- package/src/utils/savedViewHelpers.js +58 -6
- package/src/utils/savedViewMenu.js +150 -0
- package/src/utils/searchGroupSharees.js +49 -14
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mergeManifestDelta.js","sources":["../../../src/utils/mergeManifestDelta.js"],"sourcesContent":["/**\n * Keyed structural merge for app manifests (the `delta` merge mode).\n *\n * Unlike a plain deep-merge (which replaces arrays wholesale), this merges\n * the manifest's identity-bearing arrays BY KEY: `pages[]` by `page.id`,\n * `widgets[]` by `widget.id`, `menu[]` by `menu.id`, and a menu entry's nested\n * `children[]` by child `id`. That lets a stored delta patch a single page,\n * widget, or nested menu child — and lets a backend `/api/manifest` override\n * ADD children to an existing menu group (e.g. one nav entry per case type\n * under a \"Cases\" group) without resending or clobbering the group's other\n * children. It also lets a built app inherit later changes to its base manifest.\n *\n * Semantics (per the manifest-delta-merge capability):\n * - Plain objects merge recursively; scalars and non-keyed arrays are\n * replaced (delta wins) — preserving today's deep-merge behaviour.\n * - In a keyed array, a delta entry whose key MATCHES a base entry is merged\n * into it; a delta entry whose key is NEW is appended.\n * - A delta entry of the form `{ \"<key>\": \"x\", \"$op\": \"remove\" }` deletes the\n * matching base entry. `$op:\"remove\"` targeting a key not present in the\n * base is an ORPHAN: it is skipped, warned, and its path recorded.\n * - An optional `__order` object on the parent maps an array name to an id\n * sequence (`{ \"widgets\": [\"w2\", \"w1\"] }`) and reorders the merged entries\n * to it; unlisted entries keep their relative order after the listed ones.\n * Using an object (not a bare array) keeps it unambiguous when a parent\n * holds more than one keyed array (the manifest has both `pages` and\n * `menu`).\n *\n * The function is pure and Vue-free so it can be unit-tested in isolation and\n * reused by OpenBuilt (client-side preview + a server-side port).\n *\n * @module utils/mergeManifestDelta\n */\n\n/**\n * Map of array property name → the field that identifies its entries.\n * Only arrays listed here merge by key; every other array replaces.\n *\n * @type {Readonly<Record<string, string>>}\n */\nexport const KEYED_ARRAYS = Object.freeze({\n\tpages: 'id',\n\twidgets: 'id',\n\tmenu: 'id',\n\t// A menu entry's nested nav children merge by child `id` too, so a delta\n\t// (or a backend `/api/manifest` override) can add/patch/remove individual\n\t// children of a group without replacing the whole `children[]` array.\n\tchildren: 'id',\n})\n\n/** Reserved delta markers — never part of a base manifest. */\nexport const DELTA_REMOVE = 'remove'\nconst ORDER_KEY = '__order'\nconst OP_KEY = '$op'\n\n/**\n * Apply a keyed structural delta to a base manifest.\n *\n * @param {object} base The base manifest (bundled manifest or stub).\n * @param {object} delta The delta payload to apply.\n * @return {{ manifest: object, orphanedDeltaPaths: string[] }}\n * `manifest` is a new merged object (inputs are never mutated);\n * `orphanedDeltaPaths` lists the paths of delta entries that targeted a\n * missing base entry and were therefore skipped.\n */\nexport function mergeManifestDelta(base, delta) {\n\tconst orphans = []\n\tconst manifest = mergeValue(base, delta, '', orphans)\n\treturn { manifest, orphanedDeltaPaths: orphans }\n}\n\n/**\n * Recursively merge `delta` onto `base` at `path`, collecting orphan paths.\n *\n * @param {unknown} base Base value.\n * @param {unknown} delta Delta value (takes precedence).\n * @param {string} path Current JSON-ish path (for orphan reporting).\n * @param {string[]} orphans Accumulator for orphaned delta paths.\n * @return {unknown} Merged value.\n */\nfunction mergeValue(base, delta, path, orphans) {\n\t// Delta absent → keep base. Base absent / scalar mismatch → delta wins.\n\tif (delta === undefined) {\n\t\treturn clone(base)\n\t}\n\tif (!isPlainObject(base) || !isPlainObject(delta)) {\n\t\treturn clone(delta)\n\t}\n\n\tconst out = { ...clone(base) }\n\tfor (const key of Object.keys(delta)) {\n\t\tif (key === ORDER_KEY) {\n\t\t\tcontinue\n\t\t}\n\t\tconst childPath = path ? `${path}/${key}` : key\n\t\tconst baseChild = base[key]\n\t\tconst deltaChild = delta[key]\n\n\t\tif (Array.isArray(deltaChild) && Array.isArray(baseChild) && KEYED_ARRAYS[key]) {\n\t\t\tout[key] = mergeKeyedArray(baseChild, deltaChild, KEYED_ARRAYS[key], childPath, orphans)\n\t\t} else if (isPlainObject(deltaChild) && isPlainObject(baseChild)) {\n\t\t\tout[key] = mergeValue(baseChild, deltaChild, childPath, orphans)\n\t\t} else {\n\t\t\tout[key] = clone(deltaChild)\n\t\t}\n\t}\n\n\t// Apply `__order` last so a reorder-only delta (which carries no copy of\n\t// the array itself) still reorders the base array.\n\tconst orderMap = isPlainObject(delta[ORDER_KEY]) ? delta[ORDER_KEY] : {}\n\tfor (const [arrKey, seq] of Object.entries(orderMap)) {\n\t\tif (Array.isArray(seq) && Array.isArray(out[arrKey]) && KEYED_ARRAYS[arrKey]) {\n\t\t\tout[arrKey] = applyOrder(out[arrKey], seq, KEYED_ARRAYS[arrKey])\n\t\t}\n\t}\n\treturn out\n}\n\n/**\n * Merge two arrays of keyed entries.\n *\n * @param {object[]} baseArr Base array.\n * @param {object[]} deltaArr Delta array.\n * @param {string} keyField Identity field name (e.g. \"id\").\n * @param {string} path Current path (for orphan reporting).\n * @param {string[]} orphans Accumulator for orphaned delta paths.\n * @return {object[]} Merged array (ordering, if any, is applied by the caller).\n */\nfunction mergeKeyedArray(baseArr, deltaArr, keyField, path, orphans) {\n\t// Start from a keyed map of the base entries, preserving order.\n\tconst merged = baseArr.map((entry) => clone(entry))\n\tconst indexByKey = new Map()\n\tmerged.forEach((entry, i) => {\n\t\tif (isPlainObject(entry) && entry[keyField] !== undefined) {\n\t\t\tindexByKey.set(entry[keyField], i)\n\t\t}\n\t})\n\n\tfor (const deltaEntry of deltaArr) {\n\t\tif (!isPlainObject(deltaEntry)) {\n\t\t\tcontinue\n\t\t}\n\t\tconst key = deltaEntry[keyField]\n\t\tconst op = deltaEntry[OP_KEY]\n\t\tconst entryPath = `${path}/${key}`\n\n\t\tif (op === DELTA_REMOVE) {\n\t\t\tif (indexByKey.has(key)) {\n\t\t\t\tmerged[indexByKey.get(key)] = undefined // tombstone; compacted below\n\t\t\t} else {\n\t\t\t\torphans.push(entryPath)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif (indexByKey.has(key)) {\n\t\t\t// Patch an existing entry (recurse so nested widgets[] merge AND a\n\t\t\t// nested __order both apply). Keep __order for the recursion —\n\t\t\t// mergeValue consumes it and never copies it into the output; only\n\t\t\t// $op is meaningless past this point.\n\t\t\tconst i = indexByKey.get(key)\n\t\t\tmerged[i] = mergeValue(merged[i], stripOp(deltaEntry), entryPath, orphans)\n\t\t} else {\n\t\t\t// New key → append as an addition.\n\t\t\tmerged.push(clone(stripMarkers(deltaEntry)))\n\t\t}\n\t}\n\n\treturn merged.filter((e) => e !== undefined)\n}\n\n/**\n * Reorder entries to the given key sequence; unlisted entries follow in their\n * original relative order.\n *\n * @param {object[]} entries Merged entries.\n * @param {string[]} order Desired key sequence.\n * @param {string} keyField Identity field name.\n * @return {object[]} Reordered entries.\n */\nfunction applyOrder(entries, order, keyField) {\n\tconst byKey = new Map(entries.map((e) => [e && e[keyField], e]))\n\tconst result = []\n\tconst used = new Set()\n\tfor (const key of order) {\n\t\tif (byKey.has(key)) {\n\t\t\tresult.push(byKey.get(key))\n\t\t\tused.add(key)\n\t\t}\n\t}\n\tfor (const e of entries) {\n\t\tif (!used.has(e && e[keyField])) {\n\t\t\tresult.push(e)\n\t\t}\n\t}\n\treturn result\n}\n\n/**\n * Strip all delta-only markers from an entry before it lands in the manifest.\n *\n * @param {object} entry A delta array entry, possibly carrying the `$op` and\n * `__order` markers.\n * @return {object} A shallow copy with both markers removed.\n */\nfunction stripMarkers(entry) {\n\tconst out = { ...entry }\n\tdelete out[OP_KEY]\n\tdelete out[ORDER_KEY]\n\treturn out\n}\n\n/**\n * Strip only `$op`, preserving `__order` for a recursive merge to consume.\n *\n * @param {object} entry A delta array entry, possibly carrying the `$op` marker.\n * @return {object} A shallow copy with `$op` removed and `__order` intact.\n */\nfunction stripOp(entry) {\n\tconst out = { ...entry }\n\tdelete out[OP_KEY]\n\treturn out\n}\n\nfunction isPlainObject(value) {\n\treturn value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\n/**\n * Structured clone via JSON (manifests are plain JSON — no cycles/functions).\n *\n * @param {unknown} value Any manifest fragment. `undefined` and primitives are\n * returned as-is; objects and arrays are deep-copied.\n * @return {unknown} The deep copy, or `value` itself when it is not an object.\n */\nfunction clone(value) {\n\tif (value === undefined) {\n\t\treturn undefined\n\t}\n\tif (value === null || typeof value !== 'object') {\n\t\treturn value\n\t}\n\treturn JSON.parse(JSON.stringify(value))\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mergeManifestDelta.js","sources":["../../../src/utils/mergeManifestDelta.js"],"sourcesContent":["/**\n * Keyed structural merge for app manifests (the `delta` merge mode).\n *\n * Unlike a plain deep-merge (which replaces arrays wholesale), this merges\n * the manifest's identity-bearing arrays BY KEY: `pages[]` by `page.id`,\n * `widgets[]` by `widget.id`, `menu[]` by `menu.id`, and a menu entry's nested\n * `children[]` by child `id`. That lets a stored delta patch a single page,\n * widget, or nested menu child — and lets a backend `/api/manifest` override\n * ADD children to an existing menu group (e.g. one nav entry per case type\n * under a \"Cases\" group) without resending or clobbering the group's other\n * children. It also lets a built app inherit later changes to its base manifest.\n *\n * Semantics (per the manifest-delta-merge capability):\n * - Plain objects merge recursively; scalars and non-keyed arrays are\n * replaced (delta wins) — preserving today's deep-merge behaviour.\n * - In a keyed array, a delta entry whose key MATCHES a base entry is merged\n * into it; a delta entry whose key is NEW is appended.\n * - A delta entry of the form `{ \"<key>\": \"x\", \"$op\": \"remove\" }` deletes the\n * matching base entry. `$op:\"remove\"` targeting a key not present in the\n * base is an ORPHAN: it is skipped, warned, and its path recorded.\n * - An optional `__order` object on the parent maps an array name to an id\n * sequence (`{ \"widgets\": [\"w2\", \"w1\"] }`) and reorders the merged entries\n * to it; unlisted entries keep their relative order after the listed ones.\n * Using an object (not a bare array) keeps it unambiguous when a parent\n * holds more than one keyed array (the manifest has both `pages` and\n * `menu`).\n *\n * The function is pure and Vue-free so it can be unit-tested in isolation and\n * reused by OpenBuilt (client-side preview + a server-side port).\n *\n * @module utils/mergeManifestDelta\n */\n\n/**\n * Map of array property name → the field that identifies its entries.\n * Only arrays listed here merge by key; every other array replaces.\n *\n * @type {Readonly<Record<string, string>>}\n */\nexport const KEYED_ARRAYS = Object.freeze({\n\tpages: 'id',\n\twidgets: 'id',\n\tmenu: 'id',\n\t// A menu entry's nested nav children merge by child `id` too, so a delta\n\t// (or a backend `/api/manifest` override) can add/patch/remove individual\n\t// children of a group without replacing the whole `children[]` array.\n\tchildren: 'id',\n\t// An index page's default views merge by `slug`, so the per-user delta\n\t// (`/api/manifest`) can add a user's own defaults (dossiq's case type\n\t// pins) without wiping the defaults the page ships.\n\tdefaultSavedViews: 'slug',\n})\n\n/** Reserved delta markers — never part of a base manifest. */\nexport const DELTA_REMOVE = 'remove'\nconst ORDER_KEY = '__order'\nconst OP_KEY = '$op'\n\n/**\n * Apply a keyed structural delta to a base manifest.\n *\n * @param {object} base The base manifest (bundled manifest or stub).\n * @param {object} delta The delta payload to apply.\n * @return {{ manifest: object, orphanedDeltaPaths: string[] }}\n * `manifest` is a new merged object (inputs are never mutated);\n * `orphanedDeltaPaths` lists the paths of delta entries that targeted a\n * missing base entry and were therefore skipped.\n */\nexport function mergeManifestDelta(base, delta) {\n\tconst orphans = []\n\tconst manifest = mergeValue(base, delta, '', orphans)\n\treturn { manifest, orphanedDeltaPaths: orphans }\n}\n\n/**\n * Recursively merge `delta` onto `base` at `path`, collecting orphan paths.\n *\n * @param {unknown} base Base value.\n * @param {unknown} delta Delta value (takes precedence).\n * @param {string} path Current JSON-ish path (for orphan reporting).\n * @param {string[]} orphans Accumulator for orphaned delta paths.\n * @return {unknown} Merged value.\n */\nfunction mergeValue(base, delta, path, orphans) {\n\t// Delta absent → keep base. Base absent / scalar mismatch → delta wins.\n\tif (delta === undefined) {\n\t\treturn clone(base)\n\t}\n\tif (!isPlainObject(base) || !isPlainObject(delta)) {\n\t\treturn clone(delta)\n\t}\n\n\tconst out = { ...clone(base) }\n\tfor (const key of Object.keys(delta)) {\n\t\tif (key === ORDER_KEY) {\n\t\t\tcontinue\n\t\t}\n\t\tconst childPath = path ? `${path}/${key}` : key\n\t\tconst baseChild = base[key]\n\t\tconst deltaChild = delta[key]\n\n\t\tif (Array.isArray(deltaChild) && Array.isArray(baseChild) && KEYED_ARRAYS[key]) {\n\t\t\tout[key] = mergeKeyedArray(baseChild, deltaChild, KEYED_ARRAYS[key], childPath, orphans)\n\t\t} else if (isPlainObject(deltaChild) && isPlainObject(baseChild)) {\n\t\t\tout[key] = mergeValue(baseChild, deltaChild, childPath, orphans)\n\t\t} else {\n\t\t\tout[key] = clone(deltaChild)\n\t\t}\n\t}\n\n\t// Apply `__order` last so a reorder-only delta (which carries no copy of\n\t// the array itself) still reorders the base array.\n\tconst orderMap = isPlainObject(delta[ORDER_KEY]) ? delta[ORDER_KEY] : {}\n\tfor (const [arrKey, seq] of Object.entries(orderMap)) {\n\t\tif (Array.isArray(seq) && Array.isArray(out[arrKey]) && KEYED_ARRAYS[arrKey]) {\n\t\t\tout[arrKey] = applyOrder(out[arrKey], seq, KEYED_ARRAYS[arrKey])\n\t\t}\n\t}\n\treturn out\n}\n\n/**\n * Merge two arrays of keyed entries.\n *\n * @param {object[]} baseArr Base array.\n * @param {object[]} deltaArr Delta array.\n * @param {string} keyField Identity field name (e.g. \"id\").\n * @param {string} path Current path (for orphan reporting).\n * @param {string[]} orphans Accumulator for orphaned delta paths.\n * @return {object[]} Merged array (ordering, if any, is applied by the caller).\n */\nfunction mergeKeyedArray(baseArr, deltaArr, keyField, path, orphans) {\n\t// Start from a keyed map of the base entries, preserving order.\n\tconst merged = baseArr.map((entry) => clone(entry))\n\tconst indexByKey = new Map()\n\tmerged.forEach((entry, i) => {\n\t\tif (isPlainObject(entry) && entry[keyField] !== undefined) {\n\t\t\tindexByKey.set(entry[keyField], i)\n\t\t}\n\t})\n\n\tfor (const deltaEntry of deltaArr) {\n\t\tif (!isPlainObject(deltaEntry)) {\n\t\t\tcontinue\n\t\t}\n\t\tconst key = deltaEntry[keyField]\n\t\tconst op = deltaEntry[OP_KEY]\n\t\tconst entryPath = `${path}/${key}`\n\n\t\tif (op === DELTA_REMOVE) {\n\t\t\tif (indexByKey.has(key)) {\n\t\t\t\tmerged[indexByKey.get(key)] = undefined // tombstone; compacted below\n\t\t\t} else {\n\t\t\t\torphans.push(entryPath)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif (indexByKey.has(key)) {\n\t\t\t// Patch an existing entry (recurse so nested widgets[] merge AND a\n\t\t\t// nested __order both apply). Keep __order for the recursion —\n\t\t\t// mergeValue consumes it and never copies it into the output; only\n\t\t\t// $op is meaningless past this point.\n\t\t\tconst i = indexByKey.get(key)\n\t\t\tmerged[i] = mergeValue(merged[i], stripOp(deltaEntry), entryPath, orphans)\n\t\t} else {\n\t\t\t// New key → append as an addition.\n\t\t\tmerged.push(clone(stripMarkers(deltaEntry)))\n\t\t}\n\t}\n\n\treturn merged.filter((e) => e !== undefined)\n}\n\n/**\n * Reorder entries to the given key sequence; unlisted entries follow in their\n * original relative order.\n *\n * @param {object[]} entries Merged entries.\n * @param {string[]} order Desired key sequence.\n * @param {string} keyField Identity field name.\n * @return {object[]} Reordered entries.\n */\nfunction applyOrder(entries, order, keyField) {\n\tconst byKey = new Map(entries.map((e) => [e && e[keyField], e]))\n\tconst result = []\n\tconst used = new Set()\n\tfor (const key of order) {\n\t\tif (byKey.has(key)) {\n\t\t\tresult.push(byKey.get(key))\n\t\t\tused.add(key)\n\t\t}\n\t}\n\tfor (const e of entries) {\n\t\tif (!used.has(e && e[keyField])) {\n\t\t\tresult.push(e)\n\t\t}\n\t}\n\treturn result\n}\n\n/**\n * Strip all delta-only markers from an entry before it lands in the manifest.\n *\n * @param {object} entry A delta array entry, possibly carrying the `$op` and\n * `__order` markers.\n * @return {object} A shallow copy with both markers removed.\n */\nfunction stripMarkers(entry) {\n\tconst out = { ...entry }\n\tdelete out[OP_KEY]\n\tdelete out[ORDER_KEY]\n\treturn out\n}\n\n/**\n * Strip only `$op`, preserving `__order` for a recursive merge to consume.\n *\n * @param {object} entry A delta array entry, possibly carrying the `$op` marker.\n * @return {object} A shallow copy with `$op` removed and `__order` intact.\n */\nfunction stripOp(entry) {\n\tconst out = { ...entry }\n\tdelete out[OP_KEY]\n\treturn out\n}\n\nfunction isPlainObject(value) {\n\treturn value !== null && typeof value === 'object' && !Array.isArray(value)\n}\n\n/**\n * Structured clone via JSON (manifests are plain JSON — no cycles/functions).\n *\n * @param {unknown} value Any manifest fragment. `undefined` and primitives are\n * returned as-is; objects and arrays are deep-copied.\n * @return {unknown} The deep copy, or `value` itself when it is not an object.\n */\nfunction clone(value) {\n\tif (value === undefined) {\n\t\treturn undefined\n\t}\n\tif (value === null || typeof value !== 'object') {\n\t\treturn value\n\t}\n\treturn JSON.parse(JSON.stringify(value))\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -187,22 +187,30 @@ function buildViewCreatePayload({ name, description, isPublic, isDefault, state,
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
|
-
* Clean an audience list: keep `{ group, mode }`
|
|
191
|
-
* coerce any mode but `write`
|
|
190
|
+
* Clean an audience list: keep `{ group, mode }` and `{ user, mode }` entries
|
|
191
|
+
* that name exactly one target, drop repeats, and coerce any mode but `write`
|
|
192
|
+
* to `read`.
|
|
192
193
|
*
|
|
193
194
|
* @param {unknown} sharedWith The audience as given.
|
|
194
|
-
* @return {Array<{group
|
|
195
|
+
* @return {Array<{group?: string, user?: string, mode: 'read'|'write'}>} The cleaned list.
|
|
196
|
+
* @spec openspec/changes/saved-views-manage-and-defaults/tasks.md#task-5
|
|
195
197
|
*/
|
|
196
198
|
function normalizeSharedWith(sharedWith) {
|
|
197
199
|
const seen = new Set();
|
|
198
200
|
const out = [];
|
|
199
201
|
for (const entry of Array.isArray(sharedWith) ? sharedWith : []) {
|
|
200
202
|
const group = entry && typeof entry.group === 'string' ? entry.group : '';
|
|
201
|
-
|
|
203
|
+
const user = entry && typeof entry.user === 'string' ? entry.user : '';
|
|
204
|
+
if ((group === '') === (user === '')) {
|
|
202
205
|
continue
|
|
203
206
|
}
|
|
204
|
-
|
|
205
|
-
|
|
207
|
+
const key = user !== '' ? `user:${user}` : `group:${group}`;
|
|
208
|
+
if (seen.has(key)) {
|
|
209
|
+
continue
|
|
210
|
+
}
|
|
211
|
+
seen.add(key);
|
|
212
|
+
const mode = entry.mode === 'write' ? 'write' : 'read';
|
|
213
|
+
out.push(user !== '' ? { user, mode } : { group, mode });
|
|
206
214
|
}
|
|
207
215
|
return out
|
|
208
216
|
}
|
|
@@ -351,5 +359,49 @@ function isOwnView(view, currentUserId) {
|
|
|
351
359
|
return !!view && !!currentUserId && view.owner === currentUserId
|
|
352
360
|
}
|
|
353
361
|
|
|
354
|
-
|
|
362
|
+
/**
|
|
363
|
+
* The default views an index page declares (`config.defaultSavedViews`), as
|
|
364
|
+
* read-only views the saved-views control lists. They are never stored: the
|
|
365
|
+
* manifest installs them, so every user has the same set and none can delete
|
|
366
|
+
* one. `seeded: true` puts them in the control's default group; the query
|
|
367
|
+
* keeps its `@`-tokens, which resolve when a default is applied.
|
|
368
|
+
*
|
|
369
|
+
* @param {Array<object>|null|undefined} defaults The declared entries `{ slug, name, filters, search?, sort?, label? }`.
|
|
370
|
+
* @param {{ scope?: string, register?: string, schema?: object|string|null }} [page] The page, as `savedViewScope` takes it.
|
|
371
|
+
* @return {Array<object>} The views, in declaration order; entries without a slug or name are left out.
|
|
372
|
+
* @spec openspec/changes/saved-views-manage-and-defaults/tasks.md#task-4
|
|
373
|
+
*/
|
|
374
|
+
function defaultFilterViews(defaults, page = {}) {
|
|
375
|
+
if (!Array.isArray(defaults)) {
|
|
376
|
+
return []
|
|
377
|
+
}
|
|
378
|
+
return defaults
|
|
379
|
+
.filter((entry) => entry && typeof entry.slug === 'string' && entry.slug !== '' && typeof entry.name === 'string' && entry.name !== '')
|
|
380
|
+
.map((entry) => {
|
|
381
|
+
const { query } = buildViewCreatePayload({
|
|
382
|
+
name: entry.name,
|
|
383
|
+
state: {
|
|
384
|
+
filters: (entry.filters && typeof entry.filters === 'object' && !Array.isArray(entry.filters)) ? entry.filters : {},
|
|
385
|
+
search: typeof entry.search === 'string' ? entry.search : '',
|
|
386
|
+
sortKeys: Array.isArray(entry.sort) ? entry.sort : [],
|
|
387
|
+
},
|
|
388
|
+
scope: savedViewScope(page),
|
|
389
|
+
register: page.register,
|
|
390
|
+
schema: page.schema,
|
|
391
|
+
});
|
|
392
|
+
return {
|
|
393
|
+
id: `default:${entry.slug}`,
|
|
394
|
+
slug: entry.slug,
|
|
395
|
+
name: entry.name,
|
|
396
|
+
owner: '',
|
|
397
|
+
seeded: true,
|
|
398
|
+
isPublic: false,
|
|
399
|
+
labels: typeof entry.label === 'string' && entry.label !== '' ? [entry.label] : [],
|
|
400
|
+
'@self': { access: 'read' },
|
|
401
|
+
query,
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export { buildRouteQueryFromViewState, buildViewCreatePayload, defaultFilterViews, extractViewState, extractViewStateFromRouteQuery, isOwnView, normalizeSharedWith, savedViewScope, schemaSlug, viewAccess, viewMatchesScope };
|
|
355
407
|
//# sourceMappingURL=savedViewHelpers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"savedViewHelpers.js","sources":["../../../src/utils/savedViewHelpers.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 Conduction B.V.\n// SPDX-License-Identifier: EUPL-1.2\n\n/**\n * Pure helpers for CnIndexPage's saved-views feature (saved-views-ui).\n *\n * OpenRegister ships a saved-search Views backend\n * (`/apps/openregister/api/views`, ViewsController + ViewService); these\n * helpers translate between the three state shapes involved:\n *\n * 1. **Route query** — CnIndexPage's deep-link contract: every `$route.query`\n * key NOT starting with `_` is a fetch filter (see\n * `useSelfFetchList.resolveQueryFilters`); `_`-prefixed keys are reserved\n * list params. Search is `_search` and sort is `_order`, the JSON-encoded\n * ordered array `[{ key, order }, …]` — the one spelling CnIndexPage,\n * listNavigation, CnPageRenderer and OpenRegister's object API all agree\n * on. This module writes NO spelling of its own.\n * 2. **View state** — the normalized internal object\n * `{ filters, search, sortKeys, sortKey, sortOrder }`, where `sortKeys` is\n * the truth and the last two mirror its first entry, exactly as\n * `useListView` holds the same state.\n * 3. **OR View payload** — the `POST/PUT /api/views` body whose opaque\n * `query` field round-trips through OpenRegister unchanged.\n *\n * All functions are pure and defensive: malformed input degrades to the\n * empty/default state, never throws.\n *\n * @module utils/savedViewHelpers\n */\n\nimport { parseSortKeys } from './routeFilters.js'\n\n/** The default (empty) view state. @return {object} A fresh empty state. */\nfunction emptyState() {\n\treturn { filters: {}, search: '', sortKeys: [], sortKey: null, sortOrder: 'asc' }\n}\n\n/**\n * Coerce anything into an ordered sort-key list.\n *\n * The single defensive entry point, and the whole of the stored-shape\n * compatibility: a view saved before sorts could be chained holds `sort` as one\n * `{ key, order }` object, and that must keep opening exactly as it does today.\n *\n * @param {unknown} value An array, a single `{ key, order }`, or anything else.\n * @return {Array<{key: string, order: 'asc'|'desc'}>} The normalized list.\n */\nfunction normalizeSortKeys(value) {\n\tconst list = Array.isArray(value) ? value : [value]\n\tconst seen = new Set()\n\tconst keys = []\n\tfor (const entry of list) {\n\t\tif (!entry || typeof entry !== 'object' || typeof entry.key !== 'string' || entry.key === '') {\n\t\t\tcontinue\n\t\t}\n\t\tif (seen.has(entry.key)) {\n\t\t\tcontinue\n\t\t}\n\t\tseen.add(entry.key)\n\t\tkeys.push({ key: entry.key, order: entry.order === 'desc' ? 'desc' : 'asc' })\n\t}\n\treturn keys\n}\n\n/**\n * Write a sort onto a state, keeping the single-key mirror in step.\n *\n * @param {object} state The state to write onto (mutated and returned).\n * @param {Array<{key: string, order: 'asc'|'desc'}>} keys The normalized sort.\n * @return {object} The same state.\n */\nfunction withSort(state, keys) {\n\tstate.sortKeys = keys\n\tstate.sortKey = keys.length > 0 ? keys[0].key : null\n\tstate.sortOrder = keys.length > 0 ? keys[0].order : 'asc'\n\treturn state\n}\n\n/**\n * The sort a caller handed in, whichever of the two shapes they hold.\n *\n * @param {object} src A view state, possibly carrying only `sortKey`/`sortOrder`.\n * @return {Array<{key: string, order: 'asc'|'desc'}>} The normalized sort.\n */\nfunction sortKeysOf(src) {\n\tif (src.sortKeys !== undefined && src.sortKeys !== null) {\n\t\treturn normalizeSortKeys(src.sortKeys)\n\t}\n\treturn src.sortKey ? normalizeSortKeys({ key: src.sortKey, order: src.sortOrder }) : []\n}\n\n/**\n * Extract the current view state from a `$route.query`-shaped object.\n *\n * Non-underscore-prefixed keys become `filters` entries (values passed\n * through as-is, including arrays from `?status[]=a&status[]=b` deep\n * links). Reserved keys map onto the dedicated state fields: `_search` and\n * `_order`. Other reserved keys (`_page`, `_limit`, …) are ignored —\n * pagination is never part of a saved view.\n *\n * @param {object|null|undefined} query The `$route.query` object.\n * @return {{ filters: object, search: string, sortKeys: Array<{key: string, order: string}>, sortKey: ?string, sortOrder: string }} The normalized view state.\n */\nexport function extractViewStateFromRouteQuery(query) {\n\tconst state = emptyState()\n\tif (!query || typeof query !== 'object' || Array.isArray(query)) {\n\t\treturn state\n\t}\n\tfor (const [key, value] of Object.entries(query)) {\n\t\tif (key.startsWith('_')) {\n\t\t\tcontinue\n\t\t}\n\t\tif (value === undefined || value === null || value === '') {\n\t\t\tcontinue\n\t\t}\n\t\tstate.filters[key] = value\n\t}\n\tif (typeof query._search === 'string' && query._search !== '') {\n\t\tstate.search = query._search\n\t}\n\treturn withSort(state, parseSortKeys(query._order))\n}\n\n/**\n * Build a fresh route-query object that APPLIES a view state.\n *\n * Spreads `state.filters` (skipping null/undefined/empty-string/empty-array\n * values, matching what `CnIndexPage.persistViewStateToRoute` skips — the two\n * write the same query and must agree on what an absent filter looks like),\n * then sets the reserved keys only when meaningful: `_search` for a non-empty\n * search term, `_order` for an active sort. Deliberately omits `_page` —\n * applying a view is a full state replace that implicitly resets pagination.\n *\n * @param {{ filters?: object, search?: string, sortKeys?: Array<{key: string, order: string}>, sortKey?: ?string, sortOrder?: string }} state The view state to serialize.\n * @return {object} A plain object suitable for `$router.replace({ query })`.\n */\nexport function buildRouteQueryFromViewState(state) {\n\tconst query = {}\n\tconst src = (state && typeof state === 'object') ? state : {}\n\tconst filters = (src.filters && typeof src.filters === 'object' && !Array.isArray(src.filters)) ? src.filters : {}\n\tfor (const [key, value] of Object.entries(filters)) {\n\t\tif (value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0)) {\n\t\t\tcontinue\n\t\t}\n\t\tquery[key] = value\n\t}\n\tif (typeof src.search === 'string' && src.search !== '') {\n\t\tquery._search = src.search\n\t}\n\tconst keys = sortKeysOf(src)\n\tif (keys.length > 0) {\n\t\tquery._order = JSON.stringify(keys)\n\t}\n\treturn query\n}\n\n/**\n * Build the exact `POST /apps/openregister/api/views` (and PUT) request\n * body for persisting a view. The `query` field is OpenRegister's opaque\n * JSON blob — ViewsController stores `$data['query']` verbatim when it is\n * an array/object, so the shape below round-trips unchanged and\n * {@link extractViewState} is its inverse.\n *\n * @param {object} options Payload options.\n * @param {string} options.name Required view name.\n * @param {string} [options.description] Optional description.\n * @param {boolean} [options.isPublic] Share the view with other users.\n * @param {boolean} [options.isDefault] Mark as the user's default view.\n * @param {{ filters?: object, search?: string, sortKeys?: Array<{key: string, order: string}>, sortKey?: ?string, sortOrder?: string }} options.state The view state to persist.\n * @param {string} [options.scope] The pages this view belongs to, from {@link savedViewScope}; omitted when empty.\n * @param {string} [options.register] The page's register, written as OpenRegister's own `registers` list.\n * @param {object|string} [options.schema] The page's schema (slug or object), written as OpenRegister's own `schemas` list.\n * @param {Array<{group: string, mode: string}>} [options.sharedWith] Groups to share with; omitted from the body when empty.\n * @param {object} [options.presentation] How the view shows (OpenRegister's shape); omitted from the body for a table, which is the default.\n * @return {object} The request body for the OR views API.\n */\nexport function buildViewCreatePayload({ name, description, isPublic, isDefault, state, scope, register, schema, sharedWith, presentation } = {}) {\n\tconst src = (state && typeof state === 'object') ? state : {}\n\tconst keys = sortKeysOf(src)\n\tconst query = {\n\t\tfilters: (src.filters && typeof src.filters === 'object' && !Array.isArray(src.filters)) ? src.filters : {},\n\t\tsearch: (typeof src.search === 'string') ? src.search : '',\n\t\t// `null` rather than `[]` for no sort: every view stored before sorts\n\t\t// could be chained says `null`, and one spelling for \"no sort\" is worth\n\t\t// more than the symmetry.\n\t\tsort: keys.length > 0 ? keys : null,\n\t}\n\tif (typeof scope === 'string' && scope !== '') {\n\t\tquery.scope = scope\n\t}\n\tif (typeof register === 'string' && register !== '') {\n\t\tquery.registers = [register]\n\t}\n\tconst slug = schemaSlug(schema)\n\tif (slug !== '') {\n\t\tquery.schemas = [slug]\n\t}\n\tconst body = {\n\t\tname,\n\t\tdescription: description || '',\n\t\tisPublic: !!isPublic,\n\t\tisDefault: !!isDefault,\n\t\tquery,\n\t}\n\tif (presentation && typeof presentation === 'object' && (presentation.viewType === 'kanban' || presentation.viewType === 'calendar')) {\n\t\tbody.presentation = presentation\n\t}\n\tconst audience = normalizeSharedWith(sharedWith)\n\tif (audience.length > 0) {\n\t\tbody.sharedWith = audience\n\t}\n\treturn body\n}\n\n/**\n * Clean an audience list: keep `{ group, mode }` entries with a group id, and\n * coerce any mode but `write` to `read`.\n *\n * @param {unknown} sharedWith The audience as given.\n * @return {Array<{group: string, mode: 'read'|'write'}>} The cleaned list.\n */\nexport function normalizeSharedWith(sharedWith) {\n\tconst seen = new Set()\n\tconst out = []\n\tfor (const entry of Array.isArray(sharedWith) ? sharedWith : []) {\n\t\tconst group = entry && typeof entry.group === 'string' ? entry.group : ''\n\t\tif (group === '' || seen.has(group)) {\n\t\t\tcontinue\n\t\t}\n\t\tseen.add(group)\n\t\tout.push({ group, mode: entry.mode === 'write' ? 'write' : 'read' })\n\t}\n\treturn out\n}\n\n/**\n * What the current user may do with a view: `owner`, `write` or `read`.\n *\n * The server decides: `@self.access` wins. A view without it (an older\n * OpenRegister) counts as `owner` when its `owner` is the current user and as\n * `read` otherwise.\n *\n * @param {object|null|undefined} view The View API object.\n * @param {string|null|undefined} currentUserId The signed-in NC user id.\n * @return {'owner'|'write'|'read'} The access.\n */\nexport function viewAccess(view, currentUserId) {\n\tconst access = view && view['@self'] && view['@self'].access\n\tif (access === 'owner' || access === 'write' || access === 'read') {\n\t\treturn access\n\t}\n\treturn isOwnView(view, currentUserId) ? 'owner' : 'read'\n}\n\n/**\n * The slug of a schema given as a slug or as a schema object.\n *\n * @param {object|string|null|undefined} schema The page's `schema` prop.\n * @return {string} The slug, or the empty string.\n */\nexport function schemaSlug(schema) {\n\tif (typeof schema === 'string') {\n\t\treturn schema\n\t}\n\tif (schema && typeof schema === 'object' && typeof schema.slug === 'string') {\n\t\treturn schema.slug\n\t}\n\treturn ''\n}\n\n/**\n * The key naming which pages share a view.\n *\n * A view is a set of filters over one schema's fields, so by default every\n * page over the same register and schema shares it and no other page sees\n * it: three case indexes share their views, the organisations index does\n * not. `savedViewsScope` on a page overrides that, for pages that should\n * share views although they read different sources, or keep them apart\n * although they read the same one.\n *\n * @param {object} page The page.\n * @param {string} [page.scope] The explicit `savedViewsScope`, when set.\n * @param {string} [page.register] The page's register.\n * @param {object|string} [page.schema] The page's schema, slug or object.\n * @return {string} The scope key, or the empty string for a page with no source.\n */\nexport function savedViewScope({ scope, register, schema } = {}) {\n\tif (typeof scope === 'string' && scope !== '') {\n\t\treturn scope\n\t}\n\tconst slug = schemaSlug(schema)\n\tif (typeof register === 'string' && register !== '' && slug !== '') {\n\t\treturn `${register}/${slug}`\n\t}\n\treturn ''\n}\n\n/**\n * Whether a saved view belongs on a page.\n *\n * A view saved with a `scope` is shown where the scope matches. A view that\n * names `schemas` (OpenRegister's own search UI writes those) is shown on a\n * page over one of them. A view that says neither, saved before views were\n * scoped, is shown everywhere as it always was: hiding it would lose the\n * view, and nothing can tell which page it was made on.\n *\n * @param {object|null|undefined} view The View API object.\n * @param {object} page The page, as {@link savedViewScope} takes it.\n * @return {boolean} True when the view is shown on this page.\n */\nexport function viewMatchesScope(view, page = {}) {\n\tconst query = view && typeof view === 'object' ? view.query : null\n\tif (!query || typeof query !== 'object' || Array.isArray(query)) {\n\t\treturn true\n\t}\n\tif (typeof query.scope === 'string' && query.scope !== '') {\n\t\treturn query.scope === savedViewScope(page)\n\t}\n\tif (Array.isArray(query.schemas) && query.schemas.length > 0) {\n\t\tconst slug = schemaSlug(page.schema)\n\t\tif (slug === '' || !query.schemas.map(String).includes(slug)) {\n\t\t\treturn false\n\t\t}\n\t\tif (Array.isArray(query.registers) && query.registers.length > 0 && page.register) {\n\t\t\treturn query.registers.map(String).includes(String(page.register))\n\t\t}\n\t\treturn true\n\t}\n\treturn true\n}\n\n/**\n * Normalize a View API object (as returned by `GET /api/views`) back into\n * the internal view state. Accepts either a full view object (reads its\n * `query` property) or a raw query blob directly. Every field degrades\n * defensively: missing/null/malformed `query`, non-object `filters`,\n * malformed `sort` all yield the empty/default state — never throws.\n *\n * @param {object|null|undefined} view The View API object (or its raw `query` blob).\n * @return {{ filters: object, search: string, sortKeys: Array<{key: string, order: string}>, sortKey: ?string, sortOrder: string }} The normalized view state.\n */\nexport function extractViewState(view) {\n\tconst state = emptyState()\n\tif (!view || typeof view !== 'object' || Array.isArray(view)) {\n\t\treturn state\n\t}\n\t// Full view object (has a `query` key) or a raw query blob itself.\n\tconst query = (Object.hasOwn(view, 'query')) ? view.query : view\n\tif (!query || typeof query !== 'object' || Array.isArray(query)) {\n\t\treturn state\n\t}\n\tif (query.filters && typeof query.filters === 'object' && !Array.isArray(query.filters)) {\n\t\tfor (const [key, value] of Object.entries(query.filters)) {\n\t\t\tif (value === undefined || value === null || value === '') {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tstate.filters[key] = value\n\t\t}\n\t}\n\tif (typeof query.search === 'string') {\n\t\tstate.search = query.search\n\t}\n\treturn withSort(state, normalizeSortKeys(query.sort))\n}\n\n/**\n * Whether the given view is owned by the given user. Drives the delete\n * affordance in CnSavedViewsControl — OpenRegister scopes destroy() to the\n * owner server-side (a foreign id 404s), so this only hides the button for\n * views the API would refuse to delete anyway.\n *\n * @param {object|null|undefined} view The View API object.\n * @param {string|null|undefined} currentUserId The signed-in NC user id.\n * @return {boolean} True when the view belongs to the current user.\n */\nexport function isOwnView(view, currentUserId) {\n\treturn !!view && !!currentUserId && view.owner === currentUserId\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"savedViewHelpers.js","sources":["../../../src/utils/savedViewHelpers.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 Conduction B.V.\n// SPDX-License-Identifier: EUPL-1.2\n\n/**\n * Pure helpers for CnIndexPage's saved-views feature (saved-views-ui).\n *\n * OpenRegister ships a saved-search Views backend\n * (`/apps/openregister/api/views`, ViewsController + ViewService); these\n * helpers translate between the three state shapes involved:\n *\n * 1. **Route query** — CnIndexPage's deep-link contract: every `$route.query`\n * key NOT starting with `_` is a fetch filter (see\n * `useSelfFetchList.resolveQueryFilters`); `_`-prefixed keys are reserved\n * list params. Search is `_search` and sort is `_order`, the JSON-encoded\n * ordered array `[{ key, order }, …]` — the one spelling CnIndexPage,\n * listNavigation, CnPageRenderer and OpenRegister's object API all agree\n * on. This module writes NO spelling of its own.\n * 2. **View state** — the normalized internal object\n * `{ filters, search, sortKeys, sortKey, sortOrder }`, where `sortKeys` is\n * the truth and the last two mirror its first entry, exactly as\n * `useListView` holds the same state.\n * 3. **OR View payload** — the `POST/PUT /api/views` body whose opaque\n * `query` field round-trips through OpenRegister unchanged.\n *\n * All functions are pure and defensive: malformed input degrades to the\n * empty/default state, never throws.\n *\n * @module utils/savedViewHelpers\n */\n\nimport { parseSortKeys } from './routeFilters.js'\n\n/** The default (empty) view state. @return {object} A fresh empty state. */\nfunction emptyState() {\n\treturn { filters: {}, search: '', sortKeys: [], sortKey: null, sortOrder: 'asc' }\n}\n\n/**\n * Coerce anything into an ordered sort-key list.\n *\n * The single defensive entry point, and the whole of the stored-shape\n * compatibility: a view saved before sorts could be chained holds `sort` as one\n * `{ key, order }` object, and that must keep opening exactly as it does today.\n *\n * @param {unknown} value An array, a single `{ key, order }`, or anything else.\n * @return {Array<{key: string, order: 'asc'|'desc'}>} The normalized list.\n */\nfunction normalizeSortKeys(value) {\n\tconst list = Array.isArray(value) ? value : [value]\n\tconst seen = new Set()\n\tconst keys = []\n\tfor (const entry of list) {\n\t\tif (!entry || typeof entry !== 'object' || typeof entry.key !== 'string' || entry.key === '') {\n\t\t\tcontinue\n\t\t}\n\t\tif (seen.has(entry.key)) {\n\t\t\tcontinue\n\t\t}\n\t\tseen.add(entry.key)\n\t\tkeys.push({ key: entry.key, order: entry.order === 'desc' ? 'desc' : 'asc' })\n\t}\n\treturn keys\n}\n\n/**\n * Write a sort onto a state, keeping the single-key mirror in step.\n *\n * @param {object} state The state to write onto (mutated and returned).\n * @param {Array<{key: string, order: 'asc'|'desc'}>} keys The normalized sort.\n * @return {object} The same state.\n */\nfunction withSort(state, keys) {\n\tstate.sortKeys = keys\n\tstate.sortKey = keys.length > 0 ? keys[0].key : null\n\tstate.sortOrder = keys.length > 0 ? keys[0].order : 'asc'\n\treturn state\n}\n\n/**\n * The sort a caller handed in, whichever of the two shapes they hold.\n *\n * @param {object} src A view state, possibly carrying only `sortKey`/`sortOrder`.\n * @return {Array<{key: string, order: 'asc'|'desc'}>} The normalized sort.\n */\nfunction sortKeysOf(src) {\n\tif (src.sortKeys !== undefined && src.sortKeys !== null) {\n\t\treturn normalizeSortKeys(src.sortKeys)\n\t}\n\treturn src.sortKey ? normalizeSortKeys({ key: src.sortKey, order: src.sortOrder }) : []\n}\n\n/**\n * Extract the current view state from a `$route.query`-shaped object.\n *\n * Non-underscore-prefixed keys become `filters` entries (values passed\n * through as-is, including arrays from `?status[]=a&status[]=b` deep\n * links). Reserved keys map onto the dedicated state fields: `_search` and\n * `_order`. Other reserved keys (`_page`, `_limit`, …) are ignored —\n * pagination is never part of a saved view.\n *\n * @param {object|null|undefined} query The `$route.query` object.\n * @return {{ filters: object, search: string, sortKeys: Array<{key: string, order: string}>, sortKey: ?string, sortOrder: string }} The normalized view state.\n */\nexport function extractViewStateFromRouteQuery(query) {\n\tconst state = emptyState()\n\tif (!query || typeof query !== 'object' || Array.isArray(query)) {\n\t\treturn state\n\t}\n\tfor (const [key, value] of Object.entries(query)) {\n\t\tif (key.startsWith('_')) {\n\t\t\tcontinue\n\t\t}\n\t\tif (value === undefined || value === null || value === '') {\n\t\t\tcontinue\n\t\t}\n\t\tstate.filters[key] = value\n\t}\n\tif (typeof query._search === 'string' && query._search !== '') {\n\t\tstate.search = query._search\n\t}\n\treturn withSort(state, parseSortKeys(query._order))\n}\n\n/**\n * Build a fresh route-query object that APPLIES a view state.\n *\n * Spreads `state.filters` (skipping null/undefined/empty-string/empty-array\n * values, matching what `CnIndexPage.persistViewStateToRoute` skips — the two\n * write the same query and must agree on what an absent filter looks like),\n * then sets the reserved keys only when meaningful: `_search` for a non-empty\n * search term, `_order` for an active sort. Deliberately omits `_page` —\n * applying a view is a full state replace that implicitly resets pagination.\n *\n * @param {{ filters?: object, search?: string, sortKeys?: Array<{key: string, order: string}>, sortKey?: ?string, sortOrder?: string }} state The view state to serialize.\n * @return {object} A plain object suitable for `$router.replace({ query })`.\n */\nexport function buildRouteQueryFromViewState(state) {\n\tconst query = {}\n\tconst src = (state && typeof state === 'object') ? state : {}\n\tconst filters = (src.filters && typeof src.filters === 'object' && !Array.isArray(src.filters)) ? src.filters : {}\n\tfor (const [key, value] of Object.entries(filters)) {\n\t\tif (value === undefined || value === null || value === '' || (Array.isArray(value) && value.length === 0)) {\n\t\t\tcontinue\n\t\t}\n\t\tquery[key] = value\n\t}\n\tif (typeof src.search === 'string' && src.search !== '') {\n\t\tquery._search = src.search\n\t}\n\tconst keys = sortKeysOf(src)\n\tif (keys.length > 0) {\n\t\tquery._order = JSON.stringify(keys)\n\t}\n\treturn query\n}\n\n/**\n * Build the exact `POST /apps/openregister/api/views` (and PUT) request\n * body for persisting a view. The `query` field is OpenRegister's opaque\n * JSON blob — ViewsController stores `$data['query']` verbatim when it is\n * an array/object, so the shape below round-trips unchanged and\n * {@link extractViewState} is its inverse.\n *\n * @param {object} options Payload options.\n * @param {string} options.name Required view name.\n * @param {string} [options.description] Optional description.\n * @param {boolean} [options.isPublic] Share the view with other users.\n * @param {boolean} [options.isDefault] Mark as the user's default view.\n * @param {{ filters?: object, search?: string, sortKeys?: Array<{key: string, order: string}>, sortKey?: ?string, sortOrder?: string }} options.state The view state to persist.\n * @param {string} [options.scope] The pages this view belongs to, from {@link savedViewScope}; omitted when empty.\n * @param {string} [options.register] The page's register, written as OpenRegister's own `registers` list.\n * @param {object|string} [options.schema] The page's schema (slug or object), written as OpenRegister's own `schemas` list.\n * @param {Array<{group: string, mode: string}>} [options.sharedWith] Groups to share with; omitted from the body when empty.\n * @param {object} [options.presentation] How the view shows (OpenRegister's shape); omitted from the body for a table, which is the default.\n * @return {object} The request body for the OR views API.\n */\nexport function buildViewCreatePayload({ name, description, isPublic, isDefault, state, scope, register, schema, sharedWith, presentation } = {}) {\n\tconst src = (state && typeof state === 'object') ? state : {}\n\tconst keys = sortKeysOf(src)\n\tconst query = {\n\t\tfilters: (src.filters && typeof src.filters === 'object' && !Array.isArray(src.filters)) ? src.filters : {},\n\t\tsearch: (typeof src.search === 'string') ? src.search : '',\n\t\t// `null` rather than `[]` for no sort: every view stored before sorts\n\t\t// could be chained says `null`, and one spelling for \"no sort\" is worth\n\t\t// more than the symmetry.\n\t\tsort: keys.length > 0 ? keys : null,\n\t}\n\tif (typeof scope === 'string' && scope !== '') {\n\t\tquery.scope = scope\n\t}\n\tif (typeof register === 'string' && register !== '') {\n\t\tquery.registers = [register]\n\t}\n\tconst slug = schemaSlug(schema)\n\tif (slug !== '') {\n\t\tquery.schemas = [slug]\n\t}\n\tconst body = {\n\t\tname,\n\t\tdescription: description || '',\n\t\tisPublic: !!isPublic,\n\t\tisDefault: !!isDefault,\n\t\tquery,\n\t}\n\tif (presentation && typeof presentation === 'object' && (presentation.viewType === 'kanban' || presentation.viewType === 'calendar')) {\n\t\tbody.presentation = presentation\n\t}\n\tconst audience = normalizeSharedWith(sharedWith)\n\tif (audience.length > 0) {\n\t\tbody.sharedWith = audience\n\t}\n\treturn body\n}\n\n/**\n * Clean an audience list: keep `{ group, mode }` and `{ user, mode }` entries\n * that name exactly one target, drop repeats, and coerce any mode but `write`\n * to `read`.\n *\n * @param {unknown} sharedWith The audience as given.\n * @return {Array<{group?: string, user?: string, mode: 'read'|'write'}>} The cleaned list.\n * @spec openspec/changes/saved-views-manage-and-defaults/tasks.md#task-5\n */\nexport function normalizeSharedWith(sharedWith) {\n\tconst seen = new Set()\n\tconst out = []\n\tfor (const entry of Array.isArray(sharedWith) ? sharedWith : []) {\n\t\tconst group = entry && typeof entry.group === 'string' ? entry.group : ''\n\t\tconst user = entry && typeof entry.user === 'string' ? entry.user : ''\n\t\tif ((group === '') === (user === '')) {\n\t\t\tcontinue\n\t\t}\n\t\tconst key = user !== '' ? `user:${user}` : `group:${group}`\n\t\tif (seen.has(key)) {\n\t\t\tcontinue\n\t\t}\n\t\tseen.add(key)\n\t\tconst mode = entry.mode === 'write' ? 'write' : 'read'\n\t\tout.push(user !== '' ? { user, mode } : { group, mode })\n\t}\n\treturn out\n}\n\n/**\n * What the current user may do with a view: `owner`, `write` or `read`.\n *\n * The server decides: `@self.access` wins. A view without it (an older\n * OpenRegister) counts as `owner` when its `owner` is the current user and as\n * `read` otherwise.\n *\n * @param {object|null|undefined} view The View API object.\n * @param {string|null|undefined} currentUserId The signed-in NC user id.\n * @return {'owner'|'write'|'read'} The access.\n */\nexport function viewAccess(view, currentUserId) {\n\tconst access = view && view['@self'] && view['@self'].access\n\tif (access === 'owner' || access === 'write' || access === 'read') {\n\t\treturn access\n\t}\n\treturn isOwnView(view, currentUserId) ? 'owner' : 'read'\n}\n\n/**\n * The slug of a schema given as a slug or as a schema object.\n *\n * @param {object|string|null|undefined} schema The page's `schema` prop.\n * @return {string} The slug, or the empty string.\n */\nexport function schemaSlug(schema) {\n\tif (typeof schema === 'string') {\n\t\treturn schema\n\t}\n\tif (schema && typeof schema === 'object' && typeof schema.slug === 'string') {\n\t\treturn schema.slug\n\t}\n\treturn ''\n}\n\n/**\n * The key naming which pages share a view.\n *\n * A view is a set of filters over one schema's fields, so by default every\n * page over the same register and schema shares it and no other page sees\n * it: three case indexes share their views, the organisations index does\n * not. `savedViewsScope` on a page overrides that, for pages that should\n * share views although they read different sources, or keep them apart\n * although they read the same one.\n *\n * @param {object} page The page.\n * @param {string} [page.scope] The explicit `savedViewsScope`, when set.\n * @param {string} [page.register] The page's register.\n * @param {object|string} [page.schema] The page's schema, slug or object.\n * @return {string} The scope key, or the empty string for a page with no source.\n */\nexport function savedViewScope({ scope, register, schema } = {}) {\n\tif (typeof scope === 'string' && scope !== '') {\n\t\treturn scope\n\t}\n\tconst slug = schemaSlug(schema)\n\tif (typeof register === 'string' && register !== '' && slug !== '') {\n\t\treturn `${register}/${slug}`\n\t}\n\treturn ''\n}\n\n/**\n * Whether a saved view belongs on a page.\n *\n * A view saved with a `scope` is shown where the scope matches. A view that\n * names `schemas` (OpenRegister's own search UI writes those) is shown on a\n * page over one of them. A view that says neither, saved before views were\n * scoped, is shown everywhere as it always was: hiding it would lose the\n * view, and nothing can tell which page it was made on.\n *\n * @param {object|null|undefined} view The View API object.\n * @param {object} page The page, as {@link savedViewScope} takes it.\n * @return {boolean} True when the view is shown on this page.\n */\nexport function viewMatchesScope(view, page = {}) {\n\tconst query = view && typeof view === 'object' ? view.query : null\n\tif (!query || typeof query !== 'object' || Array.isArray(query)) {\n\t\treturn true\n\t}\n\tif (typeof query.scope === 'string' && query.scope !== '') {\n\t\treturn query.scope === savedViewScope(page)\n\t}\n\tif (Array.isArray(query.schemas) && query.schemas.length > 0) {\n\t\tconst slug = schemaSlug(page.schema)\n\t\tif (slug === '' || !query.schemas.map(String).includes(slug)) {\n\t\t\treturn false\n\t\t}\n\t\tif (Array.isArray(query.registers) && query.registers.length > 0 && page.register) {\n\t\t\treturn query.registers.map(String).includes(String(page.register))\n\t\t}\n\t\treturn true\n\t}\n\treturn true\n}\n\n/**\n * Normalize a View API object (as returned by `GET /api/views`) back into\n * the internal view state. Accepts either a full view object (reads its\n * `query` property) or a raw query blob directly. Every field degrades\n * defensively: missing/null/malformed `query`, non-object `filters`,\n * malformed `sort` all yield the empty/default state — never throws.\n *\n * @param {object|null|undefined} view The View API object (or its raw `query` blob).\n * @return {{ filters: object, search: string, sortKeys: Array<{key: string, order: string}>, sortKey: ?string, sortOrder: string }} The normalized view state.\n */\nexport function extractViewState(view) {\n\tconst state = emptyState()\n\tif (!view || typeof view !== 'object' || Array.isArray(view)) {\n\t\treturn state\n\t}\n\t// Full view object (has a `query` key) or a raw query blob itself.\n\tconst query = (Object.hasOwn(view, 'query')) ? view.query : view\n\tif (!query || typeof query !== 'object' || Array.isArray(query)) {\n\t\treturn state\n\t}\n\tif (query.filters && typeof query.filters === 'object' && !Array.isArray(query.filters)) {\n\t\tfor (const [key, value] of Object.entries(query.filters)) {\n\t\t\tif (value === undefined || value === null || value === '') {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tstate.filters[key] = value\n\t\t}\n\t}\n\tif (typeof query.search === 'string') {\n\t\tstate.search = query.search\n\t}\n\treturn withSort(state, normalizeSortKeys(query.sort))\n}\n\n/**\n * Whether the given view is owned by the given user. Drives the delete\n * affordance in CnSavedViewsControl — OpenRegister scopes destroy() to the\n * owner server-side (a foreign id 404s), so this only hides the button for\n * views the API would refuse to delete anyway.\n *\n * @param {object|null|undefined} view The View API object.\n * @param {string|null|undefined} currentUserId The signed-in NC user id.\n * @return {boolean} True when the view belongs to the current user.\n */\nexport function isOwnView(view, currentUserId) {\n\treturn !!view && !!currentUserId && view.owner === currentUserId\n}\n\n/**\n * The default views an index page declares (`config.defaultSavedViews`), as\n * read-only views the saved-views control lists. They are never stored: the\n * manifest installs them, so every user has the same set and none can delete\n * one. `seeded: true` puts them in the control's default group; the query\n * keeps its `@`-tokens, which resolve when a default is applied.\n *\n * @param {Array<object>|null|undefined} defaults The declared entries `{ slug, name, filters, search?, sort?, label? }`.\n * @param {{ scope?: string, register?: string, schema?: object|string|null }} [page] The page, as `savedViewScope` takes it.\n * @return {Array<object>} The views, in declaration order; entries without a slug or name are left out.\n * @spec openspec/changes/saved-views-manage-and-defaults/tasks.md#task-4\n */\nexport function defaultFilterViews(defaults, page = {}) {\n\tif (!Array.isArray(defaults)) {\n\t\treturn []\n\t}\n\treturn defaults\n\t\t.filter((entry) => entry && typeof entry.slug === 'string' && entry.slug !== '' && typeof entry.name === 'string' && entry.name !== '')\n\t\t.map((entry) => {\n\t\t\tconst { query } = buildViewCreatePayload({\n\t\t\t\tname: entry.name,\n\t\t\t\tstate: {\n\t\t\t\t\tfilters: (entry.filters && typeof entry.filters === 'object' && !Array.isArray(entry.filters)) ? entry.filters : {},\n\t\t\t\t\tsearch: typeof entry.search === 'string' ? entry.search : '',\n\t\t\t\t\tsortKeys: Array.isArray(entry.sort) ? entry.sort : [],\n\t\t\t\t},\n\t\t\t\tscope: savedViewScope(page),\n\t\t\t\tregister: page.register,\n\t\t\t\tschema: page.schema,\n\t\t\t})\n\t\t\treturn {\n\t\t\t\tid: `default:${entry.slug}`,\n\t\t\t\tslug: entry.slug,\n\t\t\t\tname: entry.name,\n\t\t\t\towner: '',\n\t\t\t\tseeded: true,\n\t\t\t\tisPublic: false,\n\t\t\t\tlabels: typeof entry.label === 'string' && entry.label !== '' ? [entry.label] : [],\n\t\t\t\t'@self': { access: 'read' },\n\t\t\t\tquery,\n\t\t\t}\n\t\t})\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { translate } from '@nextcloud/l10n';
|
|
2
|
+
|
|
3
|
+
// SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
4
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Saved views a user added to the app menu.
|
|
9
|
+
*
|
|
10
|
+
* Per user, in the app's preferences (`cn_saved_view_menu`), as a list of
|
|
11
|
+
* `{ id, page, name }`: the view id (or `default:<slug>`), the route name of
|
|
12
|
+
* the index page it filters, and the name to show. The view itself is not
|
|
13
|
+
* touched, so a view shared with the user can be added like an own one, and
|
|
14
|
+
* removing it from the menu never deletes it.
|
|
15
|
+
*
|
|
16
|
+
* A menu entry links to its page with `?savedView=<id>`; the page applies
|
|
17
|
+
* that view. The entries form the "My views" group at the bottom of the menu.
|
|
18
|
+
*
|
|
19
|
+
* @spec openspec/changes/saved-views-manage-and-defaults/tasks.md#task-7
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** The preference key the list is stored under. */
|
|
23
|
+
const SAVED_VIEW_MENU_KEY = 'cn_saved_view_menu';
|
|
24
|
+
|
|
25
|
+
/** The id of the "My views" caption. */
|
|
26
|
+
const SAVED_VIEW_MENU_CAPTION = 'saved-views-caption';
|
|
27
|
+
|
|
28
|
+
/** The id prefix of a menu entry made from a saved view. */
|
|
29
|
+
const SAVED_VIEW_MENU_PREFIX = 'saved-view-';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The stored list, cleaned: entries with an id and a page, no repeats.
|
|
33
|
+
*
|
|
34
|
+
* @param {unknown} entries The stored value.
|
|
35
|
+
* @return {Array<{id: string, page: string, name: string}>} The list.
|
|
36
|
+
*/
|
|
37
|
+
function normalizeSavedViewMenu(entries) {
|
|
38
|
+
const seen = new Set();
|
|
39
|
+
const out = [];
|
|
40
|
+
for (const e of Array.isArray(entries) ? entries : []) {
|
|
41
|
+
const id = e && (typeof e.id === 'string' || typeof e.id === 'number') ? String(e.id) : '';
|
|
42
|
+
const page = e && typeof e.page === 'string' ? e.page : '';
|
|
43
|
+
if (id === '' || page === '' || seen.has(`${page}|${id}`)) {
|
|
44
|
+
continue
|
|
45
|
+
}
|
|
46
|
+
seen.add(`${page}|${id}`);
|
|
47
|
+
out.push({ id, page, name: e && typeof e.name === 'string' && e.name !== '' ? e.name : id });
|
|
48
|
+
}
|
|
49
|
+
return out
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Add an entry, or remove it when it is already there.
|
|
54
|
+
*
|
|
55
|
+
* @param {Array<object>} entries The current list.
|
|
56
|
+
* @param {{id: string, page: string, name: string}} entry The view.
|
|
57
|
+
* @return {Array<{id: string, page: string, name: string}>} The new list.
|
|
58
|
+
*/
|
|
59
|
+
function toggleSavedViewMenuEntry(entries, entry) {
|
|
60
|
+
const list = normalizeSavedViewMenu(entries);
|
|
61
|
+
const id = String(entry.id);
|
|
62
|
+
const present = list.some((e) => e.id === id && e.page === entry.page);
|
|
63
|
+
if (present) {
|
|
64
|
+
return list.filter((e) => !(e.id === id && e.page === entry.page))
|
|
65
|
+
}
|
|
66
|
+
return normalizeSavedViewMenu([...list, { id, page: entry.page, name: entry.name }])
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Bring one page's entries in line with the views that page can show:
|
|
71
|
+
* current names, and no entry for a view that was deleted or is no longer
|
|
72
|
+
* shared with the user. Other pages' entries are left alone.
|
|
73
|
+
*
|
|
74
|
+
* @param {Array<object>} entries The current list.
|
|
75
|
+
* @param {string} page The page's route name.
|
|
76
|
+
* @param {Array<object>} views The views the page lists (defaults included).
|
|
77
|
+
* @return {Array<{id: string, page: string, name: string}>} The new list.
|
|
78
|
+
*/
|
|
79
|
+
function syncSavedViewMenu(entries, page, views) {
|
|
80
|
+
const byId = new Map((Array.isArray(views) ? views : []).map((v) => [String(v.id || v.slug), v]));
|
|
81
|
+
return normalizeSavedViewMenu(entries)
|
|
82
|
+
.filter((e) => e.page !== page || byId.has(e.id))
|
|
83
|
+
.map((e) => (e.page === page ? { ...e, name: byId.get(e.id).name || e.name } : e))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The id of the line the "My views" group shows while it is empty. */
|
|
87
|
+
const SAVED_VIEW_MENU_EMPTY = 'saved-views-empty';
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The manifest menu with the user's saved views in it, as the standard "My
|
|
91
|
+
* views" group at the bottom of the menu: a caption and one entry per view,
|
|
92
|
+
* after every main entry and so just above the footer entries (Help,
|
|
93
|
+
* Advanced), which CnAppNav draws apart. Every app gets the same group in the
|
|
94
|
+
* same place (Ruben's design rule, 19 Oct); it replaces app-specific groups
|
|
95
|
+
* such as dossiq's "My case types".
|
|
96
|
+
*
|
|
97
|
+
* With `showEmpty`, an app that offers saved views shows the group also while
|
|
98
|
+
* the user pinned none, with one line saying how to add one, so the group can
|
|
99
|
+
* be found.
|
|
100
|
+
*
|
|
101
|
+
* @param {Array<object>} menu The manifest's `menu`.
|
|
102
|
+
* @param {Array<object>} entries The user's list.
|
|
103
|
+
* @param {{showEmpty?: boolean}} [options] Options.
|
|
104
|
+
* @return {Array<object>} A new menu, in manifest order; the given one when there is nothing to add.
|
|
105
|
+
* @spec openspec/changes/my-views-menu-section/specs/app-nav/spec.md#requirement-every-app-menu-ends-with-my-views
|
|
106
|
+
*/
|
|
107
|
+
function withSavedViewMenu(menu, entries, options = {}) {
|
|
108
|
+
const list = normalizeSavedViewMenu(entries);
|
|
109
|
+
if (!Array.isArray(menu) || (list.length === 0 && options.showEmpty !== true)) {
|
|
110
|
+
return menu
|
|
111
|
+
}
|
|
112
|
+
const sorted = menu.map((item, index) => ({ item, index })).sort((a, b) => byOrder(a.item, b.item) || a.index - b.index).map(({ item }) => item);
|
|
113
|
+
const group = [
|
|
114
|
+
{ id: SAVED_VIEW_MENU_CAPTION, type: 'caption', label: translate('nextcloud-vue', 'My views') },
|
|
115
|
+
...(list.length > 0
|
|
116
|
+
? list.map((e) => ({
|
|
117
|
+
id: `${SAVED_VIEW_MENU_PREFIX}${e.id}`,
|
|
118
|
+
label: e.name,
|
|
119
|
+
route: e.page,
|
|
120
|
+
query: { savedView: e.id },
|
|
121
|
+
icon: 'FilterOutline',
|
|
122
|
+
}))
|
|
123
|
+
: [{ id: SAVED_VIEW_MENU_EMPTY, type: 'hint', label: translate('nextcloud-vue', 'Add a view to the menu from Filters') }]),
|
|
124
|
+
];
|
|
125
|
+
// Orders after the last ordered entry, so CnAppNav's own sort keeps the
|
|
126
|
+
// group at the bottom.
|
|
127
|
+
const orders = sorted.map((item) => item && item.order).filter((order) => typeof order === 'number');
|
|
128
|
+
if (orders.length > 0) {
|
|
129
|
+
const start = Math.max(...orders);
|
|
130
|
+
group.forEach((item, k) => {
|
|
131
|
+
item.order = start + k + 1;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return [...sorted, ...group]
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Manifest menu order: entries with an `order` first, by it; the rest keep their place.
|
|
139
|
+
*
|
|
140
|
+
* @param {object} a First entry.
|
|
141
|
+
* @param {object} b Second entry.
|
|
142
|
+
* @return {number}
|
|
143
|
+
*/
|
|
144
|
+
function byOrder(a, b) {
|
|
145
|
+
const aHas = typeof a?.order === 'number';
|
|
146
|
+
const bHas = typeof b?.order === 'number';
|
|
147
|
+
if (aHas !== bHas) {
|
|
148
|
+
return aHas ? -1 : 1
|
|
149
|
+
}
|
|
150
|
+
return aHas ? a.order - b.order : 0
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { SAVED_VIEW_MENU_CAPTION, SAVED_VIEW_MENU_EMPTY, SAVED_VIEW_MENU_KEY, SAVED_VIEW_MENU_PREFIX, normalizeSavedViewMenu, syncSavedViewMenu, toggleSavedViewMenuEntry, withSavedViewMenu };
|
|
154
|
+
//# sourceMappingURL=savedViewMenu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"savedViewMenu.js","sources":["../../../src/utils/savedViewMenu.js"],"sourcesContent":["// SPDX-FileCopyrightText: 2026 Conduction B.V.\n// SPDX-License-Identifier: EUPL-1.2\n\nimport { translate as t } from '@nextcloud/l10n'\n\n/**\n * Saved views a user added to the app menu.\n *\n * Per user, in the app's preferences (`cn_saved_view_menu`), as a list of\n * `{ id, page, name }`: the view id (or `default:<slug>`), the route name of\n * the index page it filters, and the name to show. The view itself is not\n * touched, so a view shared with the user can be added like an own one, and\n * removing it from the menu never deletes it.\n *\n * A menu entry links to its page with `?savedView=<id>`; the page applies\n * that view. The entries form the \"My views\" group at the bottom of the menu.\n *\n * @spec openspec/changes/saved-views-manage-and-defaults/tasks.md#task-7\n */\n\n/** The preference key the list is stored under. */\nexport const SAVED_VIEW_MENU_KEY = 'cn_saved_view_menu'\n\n/** The id of the \"My views\" caption. */\nexport const SAVED_VIEW_MENU_CAPTION = 'saved-views-caption'\n\n/** The id prefix of a menu entry made from a saved view. */\nexport const SAVED_VIEW_MENU_PREFIX = 'saved-view-'\n\n/**\n * The stored list, cleaned: entries with an id and a page, no repeats.\n *\n * @param {unknown} entries The stored value.\n * @return {Array<{id: string, page: string, name: string}>} The list.\n */\nexport function normalizeSavedViewMenu(entries) {\n\tconst seen = new Set()\n\tconst out = []\n\tfor (const e of Array.isArray(entries) ? entries : []) {\n\t\tconst id = e && (typeof e.id === 'string' || typeof e.id === 'number') ? String(e.id) : ''\n\t\tconst page = e && typeof e.page === 'string' ? e.page : ''\n\t\tif (id === '' || page === '' || seen.has(`${page}|${id}`)) {\n\t\t\tcontinue\n\t\t}\n\t\tseen.add(`${page}|${id}`)\n\t\tout.push({ id, page, name: e && typeof e.name === 'string' && e.name !== '' ? e.name : id })\n\t}\n\treturn out\n}\n\n/**\n * Add an entry, or remove it when it is already there.\n *\n * @param {Array<object>} entries The current list.\n * @param {{id: string, page: string, name: string}} entry The view.\n * @return {Array<{id: string, page: string, name: string}>} The new list.\n */\nexport function toggleSavedViewMenuEntry(entries, entry) {\n\tconst list = normalizeSavedViewMenu(entries)\n\tconst id = String(entry.id)\n\tconst present = list.some((e) => e.id === id && e.page === entry.page)\n\tif (present) {\n\t\treturn list.filter((e) => !(e.id === id && e.page === entry.page))\n\t}\n\treturn normalizeSavedViewMenu([...list, { id, page: entry.page, name: entry.name }])\n}\n\n/**\n * Bring one page's entries in line with the views that page can show:\n * current names, and no entry for a view that was deleted or is no longer\n * shared with the user. Other pages' entries are left alone.\n *\n * @param {Array<object>} entries The current list.\n * @param {string} page The page's route name.\n * @param {Array<object>} views The views the page lists (defaults included).\n * @return {Array<{id: string, page: string, name: string}>} The new list.\n */\nexport function syncSavedViewMenu(entries, page, views) {\n\tconst byId = new Map((Array.isArray(views) ? views : []).map((v) => [String(v.id || v.slug), v]))\n\treturn normalizeSavedViewMenu(entries)\n\t\t.filter((e) => e.page !== page || byId.has(e.id))\n\t\t.map((e) => (e.page === page ? { ...e, name: byId.get(e.id).name || e.name } : e))\n}\n\n/** The id of the line the \"My views\" group shows while it is empty. */\nexport const SAVED_VIEW_MENU_EMPTY = 'saved-views-empty'\n\n/**\n * The manifest menu with the user's saved views in it, as the standard \"My\n * views\" group at the bottom of the menu: a caption and one entry per view,\n * after every main entry and so just above the footer entries (Help,\n * Advanced), which CnAppNav draws apart. Every app gets the same group in the\n * same place (Ruben's design rule, 19 Oct); it replaces app-specific groups\n * such as dossiq's \"My case types\".\n *\n * With `showEmpty`, an app that offers saved views shows the group also while\n * the user pinned none, with one line saying how to add one, so the group can\n * be found.\n *\n * @param {Array<object>} menu The manifest's `menu`.\n * @param {Array<object>} entries The user's list.\n * @param {{showEmpty?: boolean}} [options] Options.\n * @return {Array<object>} A new menu, in manifest order; the given one when there is nothing to add.\n * @spec openspec/changes/my-views-menu-section/specs/app-nav/spec.md#requirement-every-app-menu-ends-with-my-views\n */\nexport function withSavedViewMenu(menu, entries, options = {}) {\n\tconst list = normalizeSavedViewMenu(entries)\n\tif (!Array.isArray(menu) || (list.length === 0 && options.showEmpty !== true)) {\n\t\treturn menu\n\t}\n\tconst sorted = menu.map((item, index) => ({ item, index })).sort((a, b) => byOrder(a.item, b.item) || a.index - b.index).map(({ item }) => item)\n\tconst group = [\n\t\t{ id: SAVED_VIEW_MENU_CAPTION, type: 'caption', label: t('nextcloud-vue', 'My views') },\n\t\t...(list.length > 0\n\t\t\t? list.map((e) => ({\n\t\t\t\t\tid: `${SAVED_VIEW_MENU_PREFIX}${e.id}`,\n\t\t\t\t\tlabel: e.name,\n\t\t\t\t\troute: e.page,\n\t\t\t\t\tquery: { savedView: e.id },\n\t\t\t\t\ticon: 'FilterOutline',\n\t\t\t\t}))\n\t\t\t: [{ id: SAVED_VIEW_MENU_EMPTY, type: 'hint', label: t('nextcloud-vue', 'Add a view to the menu from Filters') }]),\n\t]\n\t// Orders after the last ordered entry, so CnAppNav's own sort keeps the\n\t// group at the bottom.\n\tconst orders = sorted.map((item) => item && item.order).filter((order) => typeof order === 'number')\n\tif (orders.length > 0) {\n\t\tconst start = Math.max(...orders)\n\t\tgroup.forEach((item, k) => {\n\t\t\titem.order = start + k + 1\n\t\t})\n\t}\n\treturn [...sorted, ...group]\n}\n\n/**\n * Manifest menu order: entries with an `order` first, by it; the rest keep their place.\n *\n * @param {object} a First entry.\n * @param {object} b Second entry.\n * @return {number}\n */\nfunction byOrder(a, b) {\n\tconst aHas = typeof a?.order === 'number'\n\tconst bHas = typeof b?.order === 'number'\n\tif (aHas !== bHas) {\n\t\treturn aHas ? -1 : 1\n\t}\n\treturn aHas ? a.order - b.order : 0\n}\n"],"names":["t"],"mappings":"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"}
|
|
@@ -13,33 +13,38 @@ import { generateOcsUrl } from '@nextcloud/router';
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Search the sharee API for
|
|
17
|
-
* does an instance where sharing is off
|
|
16
|
+
* Search the sharee API for the given share types. Fails soft: any error
|
|
17
|
+
* answers `[]`, and so does an instance where sharing is off.
|
|
18
18
|
*
|
|
19
|
-
* @param {string}
|
|
20
|
-
* @param {
|
|
21
|
-
* @param {number}
|
|
22
|
-
* @return {Promise<Array<{id: string, label: string}>>}
|
|
19
|
+
* @param {string} query The search term.
|
|
20
|
+
* @param {Array<number>} shareTypes Nextcloud share types (0 user, 1 group).
|
|
21
|
+
* @param {number} limit Max results per type.
|
|
22
|
+
* @return {Promise<Array<{id: string, kind: 'user'|'group', label: string}>>} The sharees, exact matches first.
|
|
23
23
|
*/
|
|
24
|
-
async function
|
|
25
|
-
const { limit = 25 } = options;
|
|
24
|
+
async function searchSharees(query, shareTypes, limit) {
|
|
26
25
|
try {
|
|
27
26
|
const response = await axios__default.get(generateOcsUrl('apps/files_sharing/api/v1/sharees'), {
|
|
28
27
|
headers: { 'OCS-APIRequest': 'true', Accept: 'application/json' },
|
|
29
|
-
params: { search: query || '', itemType: 'file', shareType:
|
|
28
|
+
params: { search: query || '', itemType: 'file', shareType: shareTypes, perPage: limit, lookup: false, format: 'json' },
|
|
30
29
|
});
|
|
31
30
|
const body = response && response.data;
|
|
32
31
|
const data = body && body.ocs ? body.ocs.data : body;
|
|
33
|
-
const
|
|
32
|
+
const exact = (data && data.exact) || {};
|
|
33
|
+
const lists = [
|
|
34
|
+
...(exact.users || []).map((e) => ['user', e]),
|
|
35
|
+
...(exact.groups || []).map((e) => ['group', e]),
|
|
36
|
+
...((data && data.users) || []).map((e) => ['user', e]),
|
|
37
|
+
...((data && data.groups) || []).map((e) => ['group', e]),
|
|
38
|
+
];
|
|
34
39
|
const seen = new Set();
|
|
35
40
|
const out = [];
|
|
36
|
-
for (const entry of lists) {
|
|
41
|
+
for (const [kind, entry] of lists) {
|
|
37
42
|
const id = entry && entry.value ? entry.value.shareWith : undefined;
|
|
38
|
-
if (id === undefined || id === null || id === '' || seen.has(
|
|
43
|
+
if (id === undefined || id === null || id === '' || seen.has(`${kind}:${id}`)) {
|
|
39
44
|
continue
|
|
40
45
|
}
|
|
41
|
-
seen.add(
|
|
42
|
-
out.push({ id: String(id), label: entry.label || String(id) });
|
|
46
|
+
seen.add(`${kind}:${id}`);
|
|
47
|
+
out.push({ id: String(id), kind, label: entry.label || String(id) });
|
|
43
48
|
}
|
|
44
49
|
return out
|
|
45
50
|
} catch {
|
|
@@ -47,5 +52,35 @@ async function searchGroupSharees(query = '', options = {}) {
|
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Search the sharee API for groups. Fails soft: any error answers `[]`, and so
|
|
57
|
+
* does an instance where sharing is off, so the caller can hide the section.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} [query] The search term (empty loads a first page).
|
|
60
|
+
* @param {object} [options] Tuning options.
|
|
61
|
+
* @param {number} [options.limit] Max results (default 25).
|
|
62
|
+
* @return {Promise<Array<{id: string, label: string}>>} Group options (`id` is the group id).
|
|
63
|
+
*/
|
|
64
|
+
async function searchGroupSharees(query = '', options = {}) {
|
|
65
|
+
const { limit = 25 } = options;
|
|
66
|
+
const found = await searchSharees(query, [1], limit);
|
|
67
|
+
return found.filter((s) => s.kind === 'group').map(({ id, label }) => ({ id, label }))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Search the sharee API for users and groups, within the instance's own
|
|
72
|
+
* sharing settings (the source the Files share dialog uses).
|
|
73
|
+
*
|
|
74
|
+
* @param {string} [query] The search term (empty loads a first page).
|
|
75
|
+
* @param {object} [options] Tuning options.
|
|
76
|
+
* @param {number} [options.limit] Max results per type (default 25).
|
|
77
|
+
* @return {Promise<Array<{id: string, kind: 'user'|'group', label: string}>>} The sharees.
|
|
78
|
+
* @spec openspec/changes/saved-views-manage-and-defaults/tasks.md#task-5
|
|
79
|
+
*/
|
|
80
|
+
async function searchUserAndGroupSharees(query = '', options = {}) {
|
|
81
|
+
const { limit = 25 } = options;
|
|
82
|
+
return searchSharees(query, [0, 1], limit)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { searchGroupSharees, searchUserAndGroupSharees };
|
|
51
86
|
//# sourceMappingURL=searchGroupSharees.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"searchGroupSharees.js","sources":["../../../src/utils/searchGroupSharees.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * The groups a user may share with, from Nextcloud's sharee API (the source\n * the Files share dialog uses), so a saved view can be shared with exactly\n * the groups the instance lets this user share with.\n *\n * @spec openspec/changes/saved-views-shared-by-role/tasks.md#task-2\n */\nimport axios from '@nextcloud/axios'\nimport { generateOcsUrl } from '@nextcloud/router'\n\n/**\n * Search the sharee API for
|
|
1
|
+
{"version":3,"file":"searchGroupSharees.js","sources":["../../../src/utils/searchGroupSharees.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n *\n * The groups a user may share with, from Nextcloud's sharee API (the source\n * the Files share dialog uses), so a saved view can be shared with exactly\n * the groups the instance lets this user share with.\n *\n * @spec openspec/changes/saved-views-shared-by-role/tasks.md#task-2\n */\nimport axios from '@nextcloud/axios'\nimport { generateOcsUrl } from '@nextcloud/router'\n\n/**\n * Search the sharee API for the given share types. Fails soft: any error\n * answers `[]`, and so does an instance where sharing is off.\n *\n * @param {string} query The search term.\n * @param {Array<number>} shareTypes Nextcloud share types (0 user, 1 group).\n * @param {number} limit Max results per type.\n * @return {Promise<Array<{id: string, kind: 'user'|'group', label: string}>>} The sharees, exact matches first.\n */\nasync function searchSharees(query, shareTypes, limit) {\n\ttry {\n\t\tconst response = await axios.get(generateOcsUrl('apps/files_sharing/api/v1/sharees'), {\n\t\t\theaders: { 'OCS-APIRequest': 'true', Accept: 'application/json' },\n\t\t\tparams: { search: query || '', itemType: 'file', shareType: shareTypes, perPage: limit, lookup: false, format: 'json' },\n\t\t})\n\t\tconst body = response && response.data\n\t\tconst data = body && body.ocs ? body.ocs.data : body\n\t\tconst exact = (data && data.exact) || {}\n\t\tconst lists = [\n\t\t\t...(exact.users || []).map((e) => ['user', e]),\n\t\t\t...(exact.groups || []).map((e) => ['group', e]),\n\t\t\t...((data && data.users) || []).map((e) => ['user', e]),\n\t\t\t...((data && data.groups) || []).map((e) => ['group', e]),\n\t\t]\n\t\tconst seen = new Set()\n\t\tconst out = []\n\t\tfor (const [kind, entry] of lists) {\n\t\t\tconst id = entry && entry.value ? entry.value.shareWith : undefined\n\t\t\tif (id === undefined || id === null || id === '' || seen.has(`${kind}:${id}`)) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tseen.add(`${kind}:${id}`)\n\t\t\tout.push({ id: String(id), kind, label: entry.label || String(id) })\n\t\t}\n\t\treturn out\n\t} catch {\n\t\treturn []\n\t}\n}\n\n/**\n * Search the sharee API for groups. Fails soft: any error answers `[]`, and so\n * does an instance where sharing is off, so the caller can hide the section.\n *\n * @param {string} [query] The search term (empty loads a first page).\n * @param {object} [options] Tuning options.\n * @param {number} [options.limit] Max results (default 25).\n * @return {Promise<Array<{id: string, label: string}>>} Group options (`id` is the group id).\n */\nexport async function searchGroupSharees(query = '', options = {}) {\n\tconst { limit = 25 } = options\n\tconst found = await searchSharees(query, [1], limit)\n\treturn found.filter((s) => s.kind === 'group').map(({ id, label }) => ({ id, label }))\n}\n\n/**\n * Search the sharee API for users and groups, within the instance's own\n * sharing settings (the source the Files share dialog uses).\n *\n * @param {string} [query] The search term (empty loads a first page).\n * @param {object} [options] Tuning options.\n * @param {number} [options.limit] Max results per type (default 25).\n * @return {Promise<Array<{id: string, kind: 'user'|'group', label: string}>>} The sharees.\n * @spec openspec/changes/saved-views-manage-and-defaults/tasks.md#task-5\n */\nexport async function searchUserAndGroupSharees(query = '', options = {}) {\n\tconst { limit = 25 } = options\n\treturn searchSharees(query, [0, 1], limit)\n}\n"],"names":["axios"],"mappings":"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"}
|