@conduction/nextcloud-vue 2.96.0 → 2.98.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/CnAppRoot/CnAppRoot.vue2.js +17 -0
- 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/CnCellRenderer/CnCellRenderer.vue.js +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +33 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +383 -359
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +106 -2
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +9 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js +964 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js +282 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChecklistRunWidget/index.js +28 -0
- package/dist/esm/components/CnChecklistRunWidget/index.js.map +1 -0
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +17 -3
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +61 -43
- package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +159 -18
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +9 -7
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +1 -0
- 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 +26 -8
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +188 -5
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +48 -3
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +46 -12
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +3 -3
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +27 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +16 -2
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +144 -0
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +488 -399
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +8 -2
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +344 -21
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +29 -7
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +2 -2
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +2 -2
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js +1 -1
- package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +13 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +5 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +9 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js.map +1 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +220 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +121 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +83 -9
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +80 -11
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/composables/indexSources.js +420 -5
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/composables/useOrganisationStore.js +115 -0
- package/dist/esm/composables/useOrganisationStore.js.map +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +21 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +36 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +725 -383
- package/dist/esm/utils/builtInFormatters.js +34 -1
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/libraryWidgetKeys.js +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/matrixCells.js +158 -0
- package/dist/esm/utils/matrixCells.js.map +1 -0
- package/dist/esm/utils/nextcloudDirectory.js +121 -0
- package/dist/esm/utils/nextcloudDirectory.js.map +1 -0
- package/dist/esm/utils/queryPresets.js +13 -2
- package/dist/esm/utils/queryPresets.js.map +1 -1
- package/dist/esm/utils/scopeListLayout.js +37 -1
- package/dist/esm/utils/scopeListLayout.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 +14443 -10519
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +725 -383
- package/l10n/en.json +21 -1
- package/l10n/nl.json +36 -1
- package/package.json +2 -2
- package/scripts/merge-l10n.js +196 -0
- package/src/components/CnAppRoot/CnAppRoot.md +1 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +17 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +57 -1
- package/src/components/CnChartWidget/CnChartWidget.md +2 -1
- package/src/components/CnChartWidget/CnChartWidget.vue +106 -2
- package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +1269 -0
- package/src/components/CnChecklistRunWidget/index.js +27 -0
- package/src/components/CnConversationThread/CnConversationThread.md +12 -0
- package/src/components/CnConversationThread/CnConversationThread.vue +30 -4
- package/src/components/CnConversationThread/CnConversationWidget.vue +167 -24
- package/src/components/CnDashboardPage/CnDashboardPage.vue +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +26 -8
- package/src/components/CnDetailPage/CnDetailPage.md +48 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +188 -5
- package/src/components/CnFactsWidget/CnFactsWidget.vue +114 -3
- package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +3 -3
- package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +38 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +508 -236
- package/src/components/CnFilesBrowser/filesBrowserColumns.js +8 -2
- package/src/components/CnIndexPage/CnIndexPage.md +28 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +367 -27
- package/src/components/CnIndexPage/CnIndexPageWidgets.vue +13 -4
- package/src/components/CnIndexPage/useNamedSource.js +1 -1
- package/src/components/CnObjectSidebar/CnFilesTab.vue +17 -0
- package/src/components/CnOrderedListField/CnOrderedListField.vue +386 -0
- package/src/components/CnOrderedListField/index.js +7 -0
- package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +139 -10
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/index.js +2 -0
- package/src/composables/indexSources.js +419 -4
- package/src/composables/useOrganisationStore.js +111 -0
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +42 -5
- package/src/utils/builtInFormatters.js +33 -0
- package/src/utils/libraryWidgetKeys.js +1 -0
- package/src/utils/matrixCells.js +154 -0
- package/src/utils/nextcloudDirectory.js +117 -0
- package/src/utils/queryPresets.js +13 -2
- package/src/utils/scopeListLayout.js +37 -1
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
|
|
4
4
|
"title": "Conduction App Manifest v2",
|
|
5
5
|
"description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.122.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -597,7 +597,7 @@
|
|
|
597
597
|
"folderSidebarScope": {
|
|
598
598
|
"type": "object",
|
|
599
599
|
"additionalProperties": true,
|
|
600
|
-
"description": "One folder in `config.folderSidebar.folders[]`, which doubles as a SCOPE: the three optional list-layout keys below let one index page show each folder the way that folder needs. Every other key (id, name, icon, ...) stays free-form, so an existing folder list validates unchanged. The page's `config.columns` still decides which columns this page HAS; a scope only decides which of them are shown, in what order, sorted by what and searched over what. A scope naming a column the page does not declare is refused by validateManifestV2, not silently rendered.",
|
|
600
|
+
"description": "One folder in `config.folderSidebar.folders[]`, which doubles as a SCOPE: the three optional list-layout keys below let one index page show each folder the way that folder needs. Every other key (id, name, icon, ...) stays free-form, so an existing folder list validates unchanged. A scope (or the `x-index` block of the row a register-derived folder comes from) may also carry the presentation keys a query preset sets, under the same names: title, quickFilters, cardFields, countText, searchPlaceholder, footerNote, viewSwitch, addLabel, headerButtons, breadcrumb, headerMeta and afterList (index-scope-presentation); a scope's value wins over a preset's. The page's `config.columns` still decides which columns this page HAS; a scope only decides which of them are shown, in what order, sorted by what and searched over what. A scope naming a column the page does not declare is refused by validateManifestV2, not silently rendered.",
|
|
601
601
|
"properties": {
|
|
602
602
|
"columns": {
|
|
603
603
|
"type": "array",
|
|
@@ -1296,6 +1296,7 @@
|
|
|
1296
1296
|
"filter": { "type": "object", "additionalProperties": true }
|
|
1297
1297
|
}
|
|
1298
1298
|
},
|
|
1299
|
+
"resolve": { "type": "object", "additionalProperties": false, "required": ["register", "schema"], "description": "detail-pill-resolve: the field holds the id of a record of this register and schema; the pill shows that record's `labelField` (else its title or name) and keys `colorMap` by it. The raw value shows while the lookup runs and when it finds nothing.", "properties": { "register": { "type": "string", "minLength": 1 }, "schema": { "type": "string", "minLength": 1 }, "labelField": { "type": "string", "minLength": 1 } } },
|
|
1299
1300
|
"countLabel": {
|
|
1300
1301
|
"type": "object",
|
|
1301
1302
|
"additionalProperties": false,
|
|
@@ -2229,6 +2230,35 @@
|
|
|
2229
2230
|
"columnKeys": { "type": "array", "items": { "type": ["string", "number"] } }
|
|
2230
2231
|
}
|
|
2231
2232
|
},
|
|
2233
|
+
"cellSource": {
|
|
2234
|
+
"type": "object",
|
|
2235
|
+
"additionalProperties": false,
|
|
2236
|
+
"required": ["register", "schema", "rowField", "columnField", "valueField"],
|
|
2237
|
+
"description": "chartKind 'matrix': read the cells from records that each hold one cell. The records are listed from register/schema with `filter` (tokens such as '@object.matrixVersion' resolved; the list waits while one is unresolved), then laid out by rowField / columnField with valueField as the cell. Labels come from rowLabels / columnLabels maps or rowLabelField / columnLabelField; order from rowOrder / columnOrder lists, else rowOrderField / columnOrderField, else natural order. valueLabels maps a stored value to the text shown; colorMap may key on either. With cellsPath the matrix sits inside ONE record: the first record filter finds (else fallbackFilter), its array at cellsPath narrowed by cellFilter (tokens allowed), and the axis order from rowOrderPath / columnOrderPath on that record. Wins over series / categories.",
|
|
2238
|
+
"properties": {
|
|
2239
|
+
"register": { "type": "string" },
|
|
2240
|
+
"schema": { "type": "string" },
|
|
2241
|
+
"filter": { "type": "object", "additionalProperties": true },
|
|
2242
|
+
"rowField": { "type": "string" },
|
|
2243
|
+
"columnField": { "type": "string" },
|
|
2244
|
+
"valueField": { "type": "string" },
|
|
2245
|
+
"rowLabelField": { "type": "string" },
|
|
2246
|
+
"columnLabelField": { "type": "string" },
|
|
2247
|
+
"rowLabels": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
2248
|
+
"columnLabels": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
2249
|
+
"valueLabels": { "type": "object", "additionalProperties": { "type": "string" } },
|
|
2250
|
+
"rowOrder": { "type": "array", "items": { "type": ["string", "number"] } },
|
|
2251
|
+
"columnOrder": { "type": "array", "items": { "type": ["string", "number"] } },
|
|
2252
|
+
"rowOrderField": { "type": "string" },
|
|
2253
|
+
"columnOrderField": { "type": "string" },
|
|
2254
|
+
"limit": { "type": "integer", "minimum": 1 },
|
|
2255
|
+
"cellsPath": { "type": "string", "description": "Path of the cells array inside the one record the source finds." },
|
|
2256
|
+
"cellFilter": { "type": "object", "additionalProperties": true, "description": "Equality filter on the cells of that record, e.g. { actorType: '@object.actorType' }." },
|
|
2257
|
+
"fallbackFilter": { "type": "object", "additionalProperties": true, "description": "Filter used when `filter` finds no record, e.g. { active: true }." },
|
|
2258
|
+
"rowOrderPath": { "type": "string", "description": "Path of an array on that record giving the row order." },
|
|
2259
|
+
"columnOrderPath": { "type": "string", "description": "Path of an array on that record giving the column order." }
|
|
2260
|
+
}
|
|
2261
|
+
},
|
|
2232
2262
|
"endpointSource": {
|
|
2233
2263
|
"$ref": "#/$defs/chartEndpointSource",
|
|
2234
2264
|
"description": "Wave 2 (#91): endpoint-bound series/labels — the shared endpoint transport plus the labelsPath / series[] {name, path} response mapping. Exactly one of dataSource | endpointSource (post-schema check)."
|
|
@@ -3702,9 +3732,10 @@
|
|
|
3702
3732
|
},
|
|
3703
3733
|
"entitySource": {
|
|
3704
3734
|
"type": "string",
|
|
3705
|
-
"enum": ["flows", "tasks"],
|
|
3706
|
-
"description": "Name a registered NON-OBJECT entity collection for a type='index' page, instead of register+schema. Deliberately NOT called `source`: that key is already taken on page config and is polymorphic (a URL string in some manifests, an object with `params` in others), so reusing it would give one key two meanings and break six deployed shillinq pages. A flow definition is deliberately not an OpenRegister object, so an index had nothing to bind to and such lists became bespoke type='custom' pages. With a source the index loads the list itself. Takes precedence over register/schema; non-empty `objects` still wins over both. Registered sources live in src/composables/indexSources.js. `tasks` lists the viewer's task inbox from OpenRegister's flow-tasks read (cn-tasks-entity-source)."
|
|
3735
|
+
"enum": ["flows", "flowRuns", "tasks", "endpoint", "search", "users", "organisations"],
|
|
3736
|
+
"description": "Name a registered NON-OBJECT entity collection for a type='index' page, instead of register+schema. Deliberately NOT called `source`: that key is already taken on page config and is polymorphic (a URL string in some manifests, an object with `params` in others), so reusing it would give one key two meanings and break six deployed shillinq pages. A flow definition is deliberately not an OpenRegister object, so an index had nothing to bind to and such lists became bespoke type='custom' pages. With a source the index loads the list itself. Takes precedence over register/schema; non-empty `objects` still wins over both. Registered sources live in src/composables/indexSources.js. `tasks` lists the viewer's task inbox from OpenRegister's flow-tasks read (cn-tasks-entity-source). `organisations` lists the OpenRegister organisations the viewer belongs to. On a type='detail' page a source that reads one record (today `organisations`) loads the record whose id is the route's objectId, instead of register+schema, and feeds the header, pills, widgets and @objectId / @object.* tokens like a fetched object (organisations-entity-source)."
|
|
3707
3737
|
},
|
|
3738
|
+
"sourceConfig": { "type": "object", "additionalProperties": true, "description": "type='index' with `entitySource`: what the named source loads with. `endpoint`: `url` (an app path, required) and `rowsPath` (default `results`); its other keys go along as query parameters. `search`: `kinds` (`{ id, label, register, schema, titleField?, subtitleField?, statusField?, href?, limit? }` or `{ id, label, files: true }`), `term` and `termQuery` (default `q`). `users`: `defaultGroup`, `functionField`, `rolesField`. `flows` / `tasks`: `app`, `scope`. Maps to CnIndexPage `sourceConfig`.", "properties": { "url": { "type": "string", "minLength": 1 }, "rowsPath": { "type": "string", "minLength": 1 }, "kinds": { "type": "array", "items": { "type": "object", "required": ["id"], "additionalProperties": true, "properties": { "id": { "type": "string", "minLength": 1 }, "label": { "type": "string" }, "register": { "type": "string" }, "schema": { "type": "string" }, "files": { "type": "boolean" } } } }, "term": { "type": "string" }, "termQuery": { "type": "string", "minLength": 1 }, "defaultGroup": { "type": "string", "minLength": 1 } } },
|
|
3708
3739
|
"app": {
|
|
3709
3740
|
"type": "string",
|
|
3710
3741
|
"description": "Scopes a named `entitySource` to one app (e.g. entitySource='flows' with app='dossiq' lists that app's flows)."
|
|
@@ -4427,7 +4458,12 @@
|
|
|
4427
4458
|
"searchPlaceholder": { "type": "string", "minLength": 1, "description": "The search placeholder while the preset applies." },
|
|
4428
4459
|
"footerNote": { "type": "string", "minLength": 1, "description": "The footer note while the preset applies." },
|
|
4429
4460
|
"defaultSort": { "type": "array", "description": "The default sort while the preset applies; same shape as config.defaultSort." },
|
|
4430
|
-
"viewSwitch": { "type": "array", "items": { "type": "string", "enum": ["table", "cards", "board", "map"] }, "description": "The drawn view segments while the preset applies." }
|
|
4461
|
+
"viewSwitch": { "type": "array", "items": { "type": "string", "enum": ["table", "cards", "board", "map"] }, "description": "The drawn view segments while the preset applies." },
|
|
4462
|
+
"addLabel": { "type": "string", "minLength": 1, "description": "The add button label while the preset applies (index-scope-presentation)." },
|
|
4463
|
+
"headerButtons": { "type": "array", "description": "The header buttons while the preset applies; same shape as config.headerButtons (index-scope-presentation)." },
|
|
4464
|
+
"breadcrumb": { "description": "The breadcrumb while the preset applies; same shape as config.breadcrumb (index-scope-presentation).", "anyOf": [{ "$ref": "#/$defs/breadcrumbCrumb" }, { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/breadcrumbCrumb" } }] },
|
|
4465
|
+
"headerMeta": { "type": "string", "minLength": 1, "description": "The note after the breadcrumb while the preset applies (index-scope-presentation)." },
|
|
4466
|
+
"afterList": { "type": "array", "description": "The cards after the list while the preset applies; same shape as config.afterList (index-scope-presentation).", "items": { "type": "object", "required": ["id", "type"], "properties": { "id": { "type": "string", "minLength": 1 }, "type": { "type": "string", "minLength": 1 }, "title": { "type": "string" }, "subtitle": { "type": "string" }, "content": { "type": "object" }, "headerLink": { "type": "object" } } } }
|
|
4431
4467
|
}
|
|
4432
4468
|
}
|
|
4433
4469
|
},
|
|
@@ -4478,6 +4514,7 @@
|
|
|
4478
4514
|
"description": "type='index': the property keys the cards view shows as the facts list of each card, in order; defaults to the first four list columns under the board look (screens-card-parity). Maps to CnIndexPage `cardFields`.",
|
|
4479
4515
|
"items": { "type": "string", "minLength": 1 }
|
|
4480
4516
|
},
|
|
4517
|
+
"afterList": { "type": "array", "description": "type='index': cards after the list, stacked full width in the listed order, the counterpart of statRow above it (index-scope-presentation). Each entry is a widget definition resolved like statRow's. A query preset or a folder scope can set its own. Maps to CnIndexPage `afterList`.", "items": { "type": "object", "required": ["id", "type"], "properties": { "id": { "type": "string", "minLength": 1 }, "type": { "type": "string", "minLength": 1 }, "title": { "type": "string" }, "subtitle": { "type": "string" }, "content": { "type": "object" }, "headerLink": { "type": "object" } } } },
|
|
4481
4518
|
"statRow": {
|
|
4482
4519
|
"type": "array",
|
|
4483
4520
|
"default": [],
|
|
@@ -417,6 +417,38 @@ export function formatConnectionSettingsLabel(value) {
|
|
|
417
417
|
: ''
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Show a value with all but its last characters replaced by `*`, as the facts
|
|
422
|
+
* widget's `masked` format does: an IBAN or a BSN reads `*****4567`.
|
|
423
|
+
*
|
|
424
|
+
* DISPLAY ONLY, NOT ACCESS CONTROL. The full value is still in the row the
|
|
425
|
+
* page received; a reader with the browser's developer tools can read it.
|
|
426
|
+
* A value the reader must not have belongs hidden in OpenRegister (property
|
|
427
|
+
* authorization), not masked here.
|
|
428
|
+
*
|
|
429
|
+
* The column's `formatterOptions.visible` sets how many characters stay
|
|
430
|
+
* (default 4); at most five `*` are drawn, so the length does not leak.
|
|
431
|
+
* Null-safe: `''` for null or empty.
|
|
432
|
+
*
|
|
433
|
+
* @param {unknown} value The value.
|
|
434
|
+
* @param {object} [_row] The full row (unused).
|
|
435
|
+
* @param {object} [_property] The schema property (unused).
|
|
436
|
+
* @param {{visible?: number}} [options] The column's `formatterOptions`.
|
|
437
|
+
* @return {string} The masked value.
|
|
438
|
+
* @spec openspec/changes/masked-formatter-max-aggregate/specs/index-page/spec.md#requirement-a-column-can-mask-its-value-for-display
|
|
439
|
+
*/
|
|
440
|
+
export function formatMasked(value, _row, _property, options) {
|
|
441
|
+
if (value === null || value === undefined || value === '') {
|
|
442
|
+
return ''
|
|
443
|
+
}
|
|
444
|
+
const text = String(value)
|
|
445
|
+
const visible = Number.isInteger(options && options.visible) && options.visible >= 0 ? options.visible : 4
|
|
446
|
+
if (text.length <= visible) {
|
|
447
|
+
return text
|
|
448
|
+
}
|
|
449
|
+
return '*'.repeat(Math.min(text.length - visible, 5)) + (visible > 0 ? text.slice(-visible) : '')
|
|
450
|
+
}
|
|
451
|
+
|
|
420
452
|
/**
|
|
421
453
|
* Built-in formatter registry merged under any consumer-registered
|
|
422
454
|
* `formatters` in `CnAppRoot`'s `cnFormatters` provide.
|
|
@@ -433,4 +465,5 @@ export const BUILT_IN_FORMATTERS = {
|
|
|
433
465
|
count: formatCount,
|
|
434
466
|
connectionStatus: formatConnectionStatus,
|
|
435
467
|
connectionSettingsLabel: formatConnectionSettingsLabel,
|
|
468
|
+
masked: formatMasked,
|
|
436
469
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* matrixCells: lay out records that each hold one matrix cell as the rows and
|
|
6
|
+
* columns a `matrix` chart draws.
|
|
7
|
+
*
|
|
8
|
+
* A matrix kept as data is often stored one record per cell: the row it sits
|
|
9
|
+
* in, the column, and the value. This turns such a list into one series per
|
|
10
|
+
* row with a value per column, in a stable order, so the chart reads the same
|
|
11
|
+
* matrix the app computes with instead of a copy written into a manifest.
|
|
12
|
+
*
|
|
13
|
+
* @module utils/matrixCells
|
|
14
|
+
* @spec openspec/changes/matrix-cell-records/specs/matrix-cell-records/spec.md#requirement-a-matrix-can-read-its-cells-from-records
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readPath } from './readPath.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A stored key as text, or null when there is none.
|
|
21
|
+
*
|
|
22
|
+
* @param {unknown} value The stored value.
|
|
23
|
+
* @return {string|null} The key.
|
|
24
|
+
*/
|
|
25
|
+
function keyOf(value) {
|
|
26
|
+
if (value === null || value === undefined || value === '' || typeof value === 'object') {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
return String(value)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The keys in the order they are drawn: the listed order first, then keys
|
|
34
|
+
* with an order value (lowest first), then the rest in natural order.
|
|
35
|
+
*
|
|
36
|
+
* @param {Map<string, {order: number}>} found The keys seen, with their lowest order value.
|
|
37
|
+
* @param {Array<string|number>|undefined} listed An explicit order.
|
|
38
|
+
* @return {string[]} The ordered keys.
|
|
39
|
+
*/
|
|
40
|
+
function orderKeys(found, listed) {
|
|
41
|
+
const explicit = Array.isArray(listed) ? listed.map((k) => String(k)).filter((k) => found.has(k)) : []
|
|
42
|
+
const rest = [...found.keys()].filter((k) => !explicit.includes(k))
|
|
43
|
+
rest.sort((a, b) => {
|
|
44
|
+
const oa = found.get(a).order
|
|
45
|
+
const ob = found.get(b).order
|
|
46
|
+
if (oa !== ob) {
|
|
47
|
+
return oa - ob
|
|
48
|
+
}
|
|
49
|
+
return a.localeCompare(b, undefined, { numeric: true })
|
|
50
|
+
})
|
|
51
|
+
return [...explicit, ...rest]
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Lay out cell records as matrix rows and columns.
|
|
56
|
+
*
|
|
57
|
+
* Config: `rowField`, `columnField`, `valueField` (paths on a record);
|
|
58
|
+
* optional `rowLabelField` / `columnLabelField` (a path holding the label),
|
|
59
|
+
* `rowLabels` / `columnLabels` (key to label maps, these win), `rowOrder` /
|
|
60
|
+
* `columnOrder` (keys in order) and `rowOrderField` / `columnOrderField` (a
|
|
61
|
+
* numeric path to sort by). A record without a row or column is skipped; when
|
|
62
|
+
* two records name the same cell, the first wins.
|
|
63
|
+
*
|
|
64
|
+
* @param {Array<object>} records The cell records.
|
|
65
|
+
* @param {object} config The `cellSource` config.
|
|
66
|
+
* @return {{series: Array<{key: string, name: string, data: Array<unknown>}>, categories: string[], columnKeys: string[]}} The matrix.
|
|
67
|
+
* @spec openspec/changes/matrix-cell-records/specs/matrix-cell-records/spec.md#requirement-a-matrix-can-read-its-cells-from-records
|
|
68
|
+
*/
|
|
69
|
+
export function pivotCells(records, config) {
|
|
70
|
+
const cfg = config && typeof config === 'object' ? config : {}
|
|
71
|
+
const rows = new Map()
|
|
72
|
+
const columns = new Map()
|
|
73
|
+
const cells = new Map()
|
|
74
|
+
const note = (map, key, label, order) => {
|
|
75
|
+
const entry = map.get(key)
|
|
76
|
+
const value = Number(order)
|
|
77
|
+
const rank = Number.isFinite(value) ? value : Infinity
|
|
78
|
+
if (!entry) {
|
|
79
|
+
map.set(key, { label, order: rank })
|
|
80
|
+
} else {
|
|
81
|
+
entry.order = Math.min(entry.order, rank)
|
|
82
|
+
entry.label = entry.label || label
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const record of Array.isArray(records) ? records : []) {
|
|
86
|
+
if (!record || typeof record !== 'object') {
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
const row = keyOf(readPath(record, cfg.rowField))
|
|
90
|
+
const column = keyOf(readPath(record, cfg.columnField))
|
|
91
|
+
if (row === null || column === null) {
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
note(rows, row, cfg.rowLabelField ? keyOf(readPath(record, cfg.rowLabelField)) : null, cfg.rowOrderField ? readPath(record, cfg.rowOrderField) : undefined)
|
|
95
|
+
note(columns, column, cfg.columnLabelField ? keyOf(readPath(record, cfg.columnLabelField)) : null, cfg.columnOrderField ? readPath(record, cfg.columnOrderField) : undefined)
|
|
96
|
+
const cell = `${row}\u0000${column}`
|
|
97
|
+
if (!cells.has(cell)) {
|
|
98
|
+
const value = readPath(record, cfg.valueField)
|
|
99
|
+
cells.set(cell, value === undefined ? null : value)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const rowKeys = orderKeys(rows, cfg.rowOrder)
|
|
103
|
+
const columnKeys = orderKeys(columns, cfg.columnOrder)
|
|
104
|
+
const labelOf = (key, map, labels) => {
|
|
105
|
+
if (labels && typeof labels === 'object' && typeof labels[key] === 'string') {
|
|
106
|
+
return labels[key]
|
|
107
|
+
}
|
|
108
|
+
return map.get(key).label || key
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
series: rowKeys.map((row) => ({
|
|
112
|
+
key: row,
|
|
113
|
+
name: labelOf(row, rows, cfg.rowLabels),
|
|
114
|
+
data: columnKeys.map((column) => {
|
|
115
|
+
const value = cells.get(`${row}\u0000${column}`)
|
|
116
|
+
return value === undefined ? null : value
|
|
117
|
+
}),
|
|
118
|
+
})),
|
|
119
|
+
categories: columnKeys.map((column) => labelOf(column, columns, cfg.columnLabels)),
|
|
120
|
+
columnKeys,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The cells of a matrix kept inside ONE record: the array at `cellsPath`,
|
|
126
|
+
* narrowed by `cellFilter` (field to value, compared as text), with the axis
|
|
127
|
+
* order read from `rowOrderPath` / `columnOrderPath` on the record when the
|
|
128
|
+
* config gives no `rowOrder` / `columnOrder` list.
|
|
129
|
+
*
|
|
130
|
+
* @param {object|null} record The record that holds the matrix.
|
|
131
|
+
* @param {object} config The `cellSource` config.
|
|
132
|
+
* @param {object} [cellFilter] The `cellFilter`, tokens already resolved.
|
|
133
|
+
* @return {{cells: Array<object>, config: object}} The cells, and the config to lay them out with.
|
|
134
|
+
* @spec openspec/changes/matrix-cell-records/specs/matrix-cell-records/spec.md#requirement-a-matrix-can-read-its-cells-from-one-record
|
|
135
|
+
*/
|
|
136
|
+
export function cellsFromRecord(record, config, cellFilter = {}) {
|
|
137
|
+
const cfg = config && typeof config === 'object' ? config : {}
|
|
138
|
+
const list = record ? readPath(record, cfg.cellsPath) : null
|
|
139
|
+
const wanted = Object.entries(cellFilter && typeof cellFilter === 'object' ? cellFilter : {})
|
|
140
|
+
const cells = (Array.isArray(list) ? list : []).filter((cell) => cell && typeof cell === 'object'
|
|
141
|
+
&& wanted.every(([field, value]) => keyOf(readPath(cell, field)) === keyOf(value)))
|
|
142
|
+
const axis = (path) => {
|
|
143
|
+
const value = record && path ? readPath(record, path) : null
|
|
144
|
+
return Array.isArray(value) ? value : undefined
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
cells,
|
|
148
|
+
config: {
|
|
149
|
+
...cfg,
|
|
150
|
+
rowOrder: Array.isArray(cfg.rowOrder) ? cfg.rowOrder : axis(cfg.rowOrderPath),
|
|
151
|
+
columnOrder: Array.isArray(cfg.columnOrder) ? cfg.columnOrder : axis(cfg.columnOrderPath),
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { generateOcsUrl } from '@nextcloud/router'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Nextcloud's own directory: groups, the users in a group, and whether a
|
|
10
|
+
* user is in today. Read through the OCS API with the reader's session, so
|
|
11
|
+
* Nextcloud decides what the reader may see (listing a group's members needs
|
|
12
|
+
* an admin or the group's sub-admin).
|
|
13
|
+
*
|
|
14
|
+
* @module utils/nextcloudDirectory
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const OCS_HEADERS = { 'OCS-APIRequest': 'true' }
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The `ocs.data` of an OCS response, or null.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} res An axios response.
|
|
23
|
+
* @return {object|null}
|
|
24
|
+
*/
|
|
25
|
+
function ocsData(res) {
|
|
26
|
+
return (res && res.data && res.data.ocs && res.data.ocs.data) || null
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The groups, with their member counts.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} axios The axios instance.
|
|
33
|
+
* @param {{search?: string, limit?: number}} [options] Narrowing.
|
|
34
|
+
* @return {Promise<Array<{id: string, name: string, count: number}>>}
|
|
35
|
+
* @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members
|
|
36
|
+
*/
|
|
37
|
+
export async function fetchGroups(axios, options = {}) {
|
|
38
|
+
const res = await axios.get(generateOcsUrl('cloud/groups/details'), {
|
|
39
|
+
params: { search: options.search || '', limit: options.limit || 200, format: 'json' },
|
|
40
|
+
headers: OCS_HEADERS,
|
|
41
|
+
})
|
|
42
|
+
const groups = (ocsData(res) && ocsData(res).groups) || []
|
|
43
|
+
return groups.map((group) => ({
|
|
44
|
+
id: String(group.id),
|
|
45
|
+
name: group.displayname || String(group.id),
|
|
46
|
+
count: Number(group.usercount) || 0,
|
|
47
|
+
}))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The members of one group, with their details.
|
|
52
|
+
*
|
|
53
|
+
* @param {object} axios The axios instance.
|
|
54
|
+
* @param {string} groupId The group id.
|
|
55
|
+
* @return {Promise<Array<object>>} Nextcloud's user details, one per member.
|
|
56
|
+
* @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members
|
|
57
|
+
*/
|
|
58
|
+
export async function fetchGroupMembers(axios, groupId) {
|
|
59
|
+
const res = await axios.get(generateOcsUrl('cloud/groups/{groupId}/users/details', { groupId }), {
|
|
60
|
+
params: { format: 'json' },
|
|
61
|
+
headers: OCS_HEADERS,
|
|
62
|
+
})
|
|
63
|
+
const users = (ocsData(res) && ocsData(res).users) || {}
|
|
64
|
+
return Array.isArray(users) ? users : Object.values(users)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether a user is in today: an out-of-office period that runs now (the
|
|
69
|
+
* dav app's absence, Nextcloud 28 and later), else their user status.
|
|
70
|
+
*
|
|
71
|
+
* @param {object} axios The axios instance.
|
|
72
|
+
* @param {string} userId The user id.
|
|
73
|
+
* @param {object|null} status The user's entry from the user status list, or null.
|
|
74
|
+
* @return {Promise<{state: string, until: (string|null), message: string}>}
|
|
75
|
+
* `state` is `absent` (with `until`, an ISO date), `away`, `present` or `unknown`.
|
|
76
|
+
* @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members
|
|
77
|
+
*/
|
|
78
|
+
export async function fetchPresence(axios, userId, status) {
|
|
79
|
+
try {
|
|
80
|
+
const res = await axios.get(generateOcsUrl('apps/dav/api/v1/outOfOffice/{userId}/now', { userId }), { params: { format: 'json' }, headers: OCS_HEADERS })
|
|
81
|
+
const ooo = ocsData(res)
|
|
82
|
+
if (ooo && ooo.endDate) {
|
|
83
|
+
const end = typeof ooo.endDate === 'number' ? new Date(ooo.endDate * 1000) : new Date(ooo.endDate)
|
|
84
|
+
return { state: 'absent', until: Number.isNaN(end.getTime()) ? null : end.toISOString().slice(0, 10), message: ooo.shortMessage || '' }
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// 404: no absence now, or an instance without the absence API.
|
|
88
|
+
}
|
|
89
|
+
if (status && typeof status === 'object') {
|
|
90
|
+
const message = typeof status.message === 'string' ? status.message : ''
|
|
91
|
+
if (status.status === 'online') {
|
|
92
|
+
return { state: 'present', until: null, message }
|
|
93
|
+
}
|
|
94
|
+
if (status.status === 'away' || status.status === 'dnd') {
|
|
95
|
+
return { state: 'away', until: null, message }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return { state: 'unknown', until: null, message: '' }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The user statuses Nextcloud shares with the reader, by user id. Empty when
|
|
103
|
+
* the user status app is off.
|
|
104
|
+
*
|
|
105
|
+
* @param {object} axios The axios instance.
|
|
106
|
+
* @return {Promise<Record<string, object>>}
|
|
107
|
+
* @spec openspec/changes/index-groups-users-source/specs/index-page/spec.md#requirement-an-index-page-can-list-nextcloud-groups-and-their-members
|
|
108
|
+
*/
|
|
109
|
+
export async function fetchStatuses(axios) {
|
|
110
|
+
try {
|
|
111
|
+
const res = await axios.get(generateOcsUrl('apps/user_status/api/v1/statuses'), { params: { limit: 500, format: 'json' }, headers: OCS_HEADERS })
|
|
112
|
+
const list = ocsData(res) || []
|
|
113
|
+
return Object.fromEntries((Array.isArray(list) ? list : []).map((entry) => [entry.userId, entry]))
|
|
114
|
+
} catch {
|
|
115
|
+
return {}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -30,6 +30,11 @@ export const QUERY_PRESET_KEYS = Object.freeze([
|
|
|
30
30
|
'footerNote',
|
|
31
31
|
'defaultSort',
|
|
32
32
|
'viewSwitch',
|
|
33
|
+
'addLabel',
|
|
34
|
+
'headerButtons',
|
|
35
|
+
'breadcrumb',
|
|
36
|
+
'headerMeta',
|
|
37
|
+
'afterList',
|
|
33
38
|
])
|
|
34
39
|
|
|
35
40
|
/**
|
|
@@ -71,8 +76,9 @@ export function matchQueryPreset(presets, query) {
|
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
/**
|
|
74
|
-
* The page config with the preset's allowed keys laid over it,
|
|
75
|
-
* `queryPresets` itself (the page component
|
|
79
|
+
* The page config with the preset's allowed keys laid over it, its `match`
|
|
80
|
+
* as `fixedFilters`, and without `queryPresets` itself (the page component
|
|
81
|
+
* has no such prop).
|
|
76
82
|
*
|
|
77
83
|
* @param {object} config The page config.
|
|
78
84
|
* @param {object|null} preset The matched preset, or null.
|
|
@@ -89,5 +95,10 @@ export function applyQueryPreset(config, preset) {
|
|
|
89
95
|
out[key] = preset[key]
|
|
90
96
|
}
|
|
91
97
|
}
|
|
98
|
+
// The query pairs that chose the preset scope the list: the page shows
|
|
99
|
+
// them as no removable filter chip.
|
|
100
|
+
if (preset.match && typeof preset.match === 'object') {
|
|
101
|
+
out.fixedFilters = { ...preset.match }
|
|
102
|
+
}
|
|
92
103
|
return out
|
|
93
104
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { QUERY_PRESET_KEYS } from './queryPresets.js'
|
|
7
|
+
|
|
6
8
|
/**
|
|
7
9
|
* The three list-layout keys a `folderSidebar` scope may carry.
|
|
8
10
|
*
|
|
@@ -10,6 +12,15 @@
|
|
|
10
12
|
*/
|
|
11
13
|
const SCOPE_LAYOUT_KEYS = ['columns', 'defaultSort', 'searchFields']
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The presentation keys a scope may carry beside its list layout: the keys a
|
|
17
|
+
* query preset may set (`QUERY_PRESET_KEYS`), less the two the list layout
|
|
18
|
+
* resolves itself (`columns`, `defaultSort`).
|
|
19
|
+
*
|
|
20
|
+
* @type {ReadonlyArray<string>}
|
|
21
|
+
*/
|
|
22
|
+
const SCOPE_PRESENTATION_KEYS = Object.freeze(QUERY_PRESET_KEYS.filter((key) => key !== 'columns' && key !== 'defaultSort'))
|
|
23
|
+
|
|
13
24
|
/**
|
|
14
25
|
* The key of a column entry, in either of the two shapes the manifest accepts:
|
|
15
26
|
* a bare string (legacy shorthand) or a `{ key, label, ... }` object.
|
|
@@ -149,4 +160,29 @@ export function resolveScopeLayout({ scope = null, row = null, pageColumns = []
|
|
|
149
160
|
return { columns, sortKeys, searchFields }
|
|
150
161
|
}
|
|
151
162
|
|
|
152
|
-
|
|
163
|
+
/**
|
|
164
|
+
* The presentation a scope asks for: each of `SCOPE_PRESENTATION_KEYS` the
|
|
165
|
+
* folder entry declares, else the one its schema row's `x-index` declares.
|
|
166
|
+
* Only declared keys are in the result, so the page's own value stands for
|
|
167
|
+
* every other key. These are the keys a query preset sets, under the same
|
|
168
|
+
* names, and a scope's value wins over a preset's because it is applied on
|
|
169
|
+
* top of the page config the preset produced.
|
|
170
|
+
*
|
|
171
|
+
* @param {object} options Resolution inputs.
|
|
172
|
+
* @param {(object|null)} [options.scope] The folder entry for the active scope.
|
|
173
|
+
* @param {(object|null)} [options.row] The schema row the folder derives from.
|
|
174
|
+
* @return {object} The declared presentation keys.
|
|
175
|
+
* @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-a-preset-or-a-scope-sets-the-list-s-presentation
|
|
176
|
+
*/
|
|
177
|
+
export function resolveScopePresentation({ scope = null, row = null } = {}) {
|
|
178
|
+
const out = {}
|
|
179
|
+
for (const key of SCOPE_PRESENTATION_KEYS) {
|
|
180
|
+
const value = declaredOrRow(scope, row, key)
|
|
181
|
+
if (value !== undefined) {
|
|
182
|
+
out[key] = value
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export { SCOPE_LAYOUT_KEYS, SCOPE_PRESENTATION_KEYS }
|