@conduction/nextcloud-vue 2.56.0-beta.30 → 2.56.0-beta.31
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/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +3 -3
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +80 -4
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +95 -91
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +85 -6
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +4 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +2 -2
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +40 -3
- 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 +19 -11
- 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 +43 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js +7 -7
- package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +125 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +3 -2
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +302 -4
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +417 -244
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js +101 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +141 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +60 -0
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +74 -49
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +29 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +48 -4
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +7 -7
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +1 -1
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +63 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +8 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +14 -0
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js +5 -1
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +13 -6
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +84 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +85 -41
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +7 -2
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
- package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +197 -6
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +94 -50
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
- package/dist/esm/composables/useListView.js +4 -4
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/index.js +10 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +8 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +54 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +198 -8
- package/dist/esm/utils/facets.js +75 -3
- package/dist/esm/utils/facets.js.map +1 -1
- package/dist/esm/utils/fillTemplate.js +7 -1
- package/dist/esm/utils/fillTemplate.js.map +1 -1
- package/dist/esm/utils/pageControls.js +81 -0
- package/dist/esm/utils/pageControls.js.map +1 -0
- package/dist/esm/utils/personalLenses.js +40 -3
- package/dist/esm/utils/personalLenses.js.map +1 -1
- package/dist/esm/utils/resolveFilterTokens.js +1 -1
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/resolveManifestTokens.js +2 -0
- package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +19 -1
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/userGroupsPeek.js +91 -0
- package/dist/esm/utils/userGroupsPeek.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/visibleWhen.js +57 -5
- package/dist/esm/utils/visibleWhen.js.map +1 -1
- package/dist/esm/utils/widgetVisibility.js +4 -57
- package/dist/esm/utils/widgetVisibility.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +4309 -2233
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +198 -8
- package/l10n/en.json +8 -1
- package/l10n/nl.json +54 -1
- package/package.json +2 -1
- package/scripts/check-docs-mdx.mjs +108 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
- package/src/components/CnAppRoot/CnAppRoot.md +4 -2
- package/src/components/CnAppRoot/CnAppRoot.vue +151 -59
- package/src/components/CnBannerWidget/CnBannerWidget.vue +112 -7
- package/src/components/CnCellRenderer/CnCellRenderer.vue +4 -1
- package/src/components/CnConversationThread/CnConversationWidget.vue +1 -1
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +2 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +40 -3
- package/src/components/CnDataTable/CnDataTable.vue +19 -11
- package/src/components/CnDetailPage/CnDetailPage.vue +44 -1
- package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +1 -1
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +7 -7
- package/src/components/CnFactsWidget/CnFactsWidget.vue +125 -0
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +3 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +588 -117
- package/src/components/CnFilesBrowser/filesBrowser.js +100 -0
- package/src/components/CnFilesBrowser/filesBrowserColumns.js +140 -0
- package/src/components/CnFormPage/CnFormPage.vue +81 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +56 -3
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +7 -7
- package/src/components/CnNextStepCard/CnNextStepWidget.vue +1 -1
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
- package/src/components/CnObjectSidebar/CnFilesTab.vue +70 -1
- package/src/components/CnObjectSidebar/CnNotesTab.vue +16 -0
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +13 -6
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +180 -3
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +7 -2
- package/src/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
- package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +281 -10
- package/src/components/index.js +8 -0
- package/src/composables/useListView.js +4 -4
- package/src/css/look-board-index.css +16 -8
- package/src/index.js +8 -0
- package/src/schemas/app-manifest-v2.schema.json +93 -9
- package/src/utils/facets.js +74 -2
- package/src/utils/fillTemplate.js +7 -1
- package/src/utils/pageControls.js +78 -0
- package/src/utils/personalLenses.js +39 -2
- package/src/utils/resolveFilterTokens.js +3 -1
- package/src/utils/resolveManifestTokens.js +2 -0
- package/src/utils/sentinelTokens.js +19 -1
- package/src/utils/userGroupsPeek.js +87 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/visibleWhen.js +56 -4
- package/src/utils/widgetVisibility.js +7 -56
|
@@ -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.89.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -803,6 +803,16 @@
|
|
|
803
803
|
"pattern": "^@answer\\.[A-Za-z][A-Za-z0-9_]*$",
|
|
804
804
|
"description": "form-live-values: another answer in the same form (@answer.<field>), read by a form field's assign rule and resolved client-side by CnFormPage / CnFormDialog. Mirrors SENTINEL_TOKEN_PATTERNS.answer."
|
|
805
805
|
},
|
|
806
|
+
"sentinelRowToken": {
|
|
807
|
+
"type": "string",
|
|
808
|
+
"pattern": "^@row\\.[A-Za-z][A-Za-z0-9_.@]*$",
|
|
809
|
+
"description": "Row context: a field of the row a row action runs on (@row.<field>, dot path allowed), read by the field-list widget's rowActions request url and payload (payload values resolve at any depth). Mirrors SENTINEL_TOKEN_PATTERNS.row."
|
|
810
|
+
},
|
|
811
|
+
"sentinelSubjectToken": {
|
|
812
|
+
"type": "string",
|
|
813
|
+
"pattern": "^@subject\\.[A-Za-z][A-Za-z0-9_.]*$",
|
|
814
|
+
"description": "Subject context: a field of the record an attention card names (the first row of its ordered `visibleWhen` source query), read by the banner widget's action route params and query (@subject.<field>, dot path allowed). Mirrors SENTINEL_TOKEN_PATTERNS.subject."
|
|
815
|
+
},
|
|
806
816
|
"sentinelTokenAny": {
|
|
807
817
|
"$comment": "Union of every canonical context plus the deprecated-during-window overlay. Single source the shared resolver + gate import.",
|
|
808
818
|
"anyOf": [
|
|
@@ -814,6 +824,8 @@
|
|
|
814
824
|
{ "$ref": "#/$defs/sentinelDeclarativeToken" },
|
|
815
825
|
{ "$ref": "#/$defs/sentinelVisibleWhenToken" },
|
|
816
826
|
{ "$ref": "#/$defs/sentinelAnswerToken" },
|
|
827
|
+
{ "$ref": "#/$defs/sentinelRowToken" },
|
|
828
|
+
{ "$ref": "#/$defs/sentinelSubjectToken" },
|
|
817
829
|
{ "$ref": "#/$defs/sentinelDeprecatedToken" }
|
|
818
830
|
]
|
|
819
831
|
},
|
|
@@ -1691,7 +1703,7 @@
|
|
|
1691
1703
|
"sizeToContent": {
|
|
1692
1704
|
"type": ["boolean", "integer"],
|
|
1693
1705
|
"minimum": 1,
|
|
1694
|
-
"description": "Opt-in: hand this placement's height to whatever it renders, so a widget that decides AT RUNTIME it has nothing to show costs no row. `true` sizes purely to content; an integer
|
|
1706
|
+
"description": "Opt-in: hand this placement's height to whatever it renders, so a widget that decides AT RUNTIME it has nothing to show costs no row. `true` sizes purely to content; an integer sizes to content too but caps the height at that many rows (GridStack's maximum, NOT a minimum: content shorter than N rows is as short as it is, content taller is clipped at N). It makes gridHeight advisory for this widget — right for anything whose height is whatever it turned out to be (a banner whose condition is false, a list that came back empty), wrong for anything with a deliberate size. Maps to CnDashboardGrid's per-item `sizeToContent`."
|
|
1695
1707
|
},
|
|
1696
1708
|
"props": {
|
|
1697
1709
|
"type": "object",
|
|
@@ -1898,11 +1910,16 @@
|
|
|
1898
1910
|
"properties": {
|
|
1899
1911
|
"register": { "type": "string" },
|
|
1900
1912
|
"schema": { "type": "string" },
|
|
1901
|
-
"filter": { "type": "object", "additionalProperties": true }
|
|
1913
|
+
"filter": { "type": "object", "additionalProperties": true },
|
|
1914
|
+
"order": {
|
|
1915
|
+
"type": "object",
|
|
1916
|
+
"additionalProperties": { "type": "string", "enum": ["asc", "desc"] },
|
|
1917
|
+
"description": "Order of the query (`{ field: \"asc\" | \"desc\" }`), so the first result is the first of an ordered list. With it an attention card names that row: `{subject.<field>}` in its title, reason and kicker and `@subject.<field>` in an action route read it (screens-start-here-banner, 2.89.0)."
|
|
1918
|
+
}
|
|
1902
1919
|
}
|
|
1903
1920
|
},
|
|
1904
1921
|
"field": { "type": "string", "description": "Dot-path into the response ('@total' or omitted = collection total for a source)." },
|
|
1905
|
-
"op": { "type": "string", "enum": ["eq", "neq", "gt", "gte", "lt", "lte"], "default": "eq", "description": "Comparison operator." },
|
|
1922
|
+
"op": { "type": "string", "enum": ["eq", "neq", "gt", "gte", "lt", "lte", "empty", "notEmpty"], "default": "eq", "description": "Comparison operator. `notEmpty` on `id` is the \"there is a first row\" test of a card that names one." },
|
|
1906
1923
|
"value": { "description": "Literal right-hand side of the comparison." }
|
|
1907
1924
|
}
|
|
1908
1925
|
},
|
|
@@ -1919,6 +1936,11 @@
|
|
|
1919
1936
|
"default": "banner",
|
|
1920
1937
|
"description": "How the banner renders. `banner` (default) is the note card. `attention` is a card with a severity edge, a kicker, a title, a reason line and up to two actions."
|
|
1921
1938
|
},
|
|
1939
|
+
"stacked": {
|
|
1940
|
+
"type": "boolean",
|
|
1941
|
+
"default": false,
|
|
1942
|
+
"description": "Attention card: put the actions under the text instead of beside it, with a 16px title (PqMijnWerk \"Begin hier\"; screens-start-here-banner, 2.89.0)."
|
|
1943
|
+
},
|
|
1922
1944
|
"kicker": {
|
|
1923
1945
|
"type": "string",
|
|
1924
1946
|
"description": "Attention card: the small label above the title."
|
|
@@ -2184,9 +2206,27 @@
|
|
|
2184
2206
|
},
|
|
2185
2207
|
"style": {
|
|
2186
2208
|
"type": "string",
|
|
2187
|
-
"enum": ["radio", "select"],
|
|
2209
|
+
"enum": ["radio", "select", "chips", "toggle"],
|
|
2188
2210
|
"default": "radio",
|
|
2189
|
-
"description": "Choice style: 'radio' (default) list
|
|
2211
|
+
"description": "Choice style: 'radio' (default) list, 'select' dropdown, 'chips' (one compact segmented group with a count after each label; screens-page-lenses, 2.87.0) or 'toggle' (a switch writing `onValue` / `offValue`; screens-page-controls, 2.88.0)."
|
|
2212
|
+
},
|
|
2213
|
+
"layout": {
|
|
2214
|
+
"type": "string",
|
|
2215
|
+
"enum": ["start", "bar"],
|
|
2216
|
+
"description": "'start' draws the start card; 'bar' draws `controls` side by side in one row (screens-page-controls, 2.88.0)."
|
|
2217
|
+
},
|
|
2218
|
+
"controls": {
|
|
2219
|
+
"type": "array",
|
|
2220
|
+
"items": { "type": "object", "additionalProperties": true },
|
|
2221
|
+
"description": "With `layout: \"bar\"`: the bar's controls, each a workspace-filter content (chips, a toggle, a select) with its own `writes`. `align: \"end\"` pushes a control to the right edge (2.88.0)."
|
|
2222
|
+
},
|
|
2223
|
+
"onValue": { "description": "style 'toggle': the value written while the switch is on. Default true (2.88.0)." },
|
|
2224
|
+
"offValue": { "description": "style 'toggle': the value written while the switch is off. Default false (2.88.0)." },
|
|
2225
|
+
"align": { "type": "string", "enum": ["start", "end"], "description": "In a bar: 'end' pushes this control to the right edge (2.88.0)." },
|
|
2226
|
+
"urlKey": {
|
|
2227
|
+
"type": "string",
|
|
2228
|
+
"minLength": 1,
|
|
2229
|
+
"description": "Keep the choice in the address as `?<urlKey>=<value>` (replacing the history entry, keeping other query keys). The address wins over `default` on load, so a chip is linkable (screens-page-lenses, 2.87.0)."
|
|
2190
2230
|
},
|
|
2191
2231
|
"allLabel": {
|
|
2192
2232
|
"type": "string",
|
|
@@ -2199,7 +2239,7 @@
|
|
|
2199
2239
|
},
|
|
2200
2240
|
"options": {
|
|
2201
2241
|
"type": "array",
|
|
2202
|
-
"description": "Static options — each { value, label, count? } (or { id, name } / bare strings, normalised).",
|
|
2242
|
+
"description": "Static options — each { value, label, count?, countFrom? } (or { id, name } / bare strings, normalised). `countFrom` is `{ register, schema, filter? }`, or a list of those that are added up; the option shows that list's total (one list request each; none when it fails). screens-page-lenses, 2.87.0.",
|
|
2203
2243
|
"items": { "type": ["object", "string", "number"] }
|
|
2204
2244
|
},
|
|
2205
2245
|
"source": {
|
|
@@ -2925,15 +2965,20 @@
|
|
|
2925
2965
|
"filter": { "type": "object", "additionalProperties": true }
|
|
2926
2966
|
}
|
|
2927
2967
|
},
|
|
2968
|
+
"workspace": {
|
|
2969
|
+
"type": "string",
|
|
2970
|
+
"minLength": 1,
|
|
2971
|
+
"description": "WORKSPACE mode (screens-page-lenses, 2.87.0): the key of the page workspace context to compare, read at once with no request. A page control (a `workspace-filter` with `writes`) sets the key, and a dashboard widget carrying this condition appears and disappears as it changes. Honoured on a dashboard widget or KPI-row tile only, and as a top-level condition (not inside `all` / `any`)."
|
|
2972
|
+
},
|
|
2928
2973
|
"field": {
|
|
2929
2974
|
"type": "string",
|
|
2930
2975
|
"description": "Dot-path to the compared value — into the endpoint body, the source's first result, or (LOCAL mode) the caller's object context."
|
|
2931
2976
|
},
|
|
2932
2977
|
"op": {
|
|
2933
2978
|
"type": "string",
|
|
2934
|
-
"enum": ["eq", "neq", "gt", "gte", "lt", "lte", "empty", "notEmpty"],
|
|
2979
|
+
"enum": ["eq", "neq", "gt", "gte", "lt", "lte", "empty", "notEmpty", "in"],
|
|
2935
2980
|
"default": "eq",
|
|
2936
|
-
"description": "Comparison operator (default eq). eq/neq compare loosely-normalised primitives; ordering operators coerce both sides to Number. `empty` / `notEmpty` take no `value` and treat undefined, null, '' and an empty array alike — the one question eq cannot ask, because an unset field arrives in a shape that depends on the store and String(null) is the word 'null' rather than blank."
|
|
2981
|
+
"description": "Comparison operator (default eq). eq/neq compare loosely-normalised primitives; ordering operators coerce both sides to Number. `in` holds when the value is one of the listed `value` array (2.87.0). `empty` / `notEmpty` take no `value` and treat undefined, null, '' and an empty array alike — the one question eq cannot ask, because an unset field arrives in a shape that depends on the store and String(null) is the word 'null' rather than blank."
|
|
2937
2982
|
},
|
|
2938
2983
|
"value": {
|
|
2939
2984
|
"description": "The right-hand side of the comparison, resolved through the shared @-token grammar so `@me` names the signed-in reader rather than a literal only the server knows. Omitted for `empty` / `notEmpty`."
|
|
@@ -2985,6 +3030,45 @@
|
|
|
2985
3030
|
"type": "array",
|
|
2986
3031
|
"items": { "type": "string" },
|
|
2987
3032
|
"description": "OpenRegister `_extend[]` values forwarded on the fetch (e.g. [\"calculations\"]) so read-time virtual/derived fields are hydrated and available as columns. Each entry is passed through verbatim."
|
|
3033
|
+
},
|
|
3034
|
+
"pageControls": { "$ref": "#/$defs/pageControls" }
|
|
3035
|
+
}
|
|
3036
|
+
},
|
|
3037
|
+
"pageControls": {
|
|
3038
|
+
"type": "object",
|
|
3039
|
+
"additionalProperties": false,
|
|
3040
|
+
"description": "screens-page-controls (2.88.0): which page controls a list widget (`record-list`, `object-table`) listens to, and what they mean for it. A page control is a `workspace-filter` select or switch that writes a workspace key. `sort` picks the list's sort from the key's value; `toggle` drops filter keys while the key is on. Without it the list ignores the page's controls.",
|
|
3041
|
+
"properties": {
|
|
3042
|
+
"sort": {
|
|
3043
|
+
"type": "object",
|
|
3044
|
+
"required": ["key", "map"],
|
|
3045
|
+
"additionalProperties": false,
|
|
3046
|
+
"description": "The workspace key a sort select writes, and what each of its values sorts by. A value with no entry leaves the list's own sort.",
|
|
3047
|
+
"properties": {
|
|
3048
|
+
"key": { "type": "string", "minLength": 1 },
|
|
3049
|
+
"map": {
|
|
3050
|
+
"type": "object",
|
|
3051
|
+
"additionalProperties": {
|
|
3052
|
+
"type": "object",
|
|
3053
|
+
"required": ["field"],
|
|
3054
|
+
"additionalProperties": false,
|
|
3055
|
+
"properties": {
|
|
3056
|
+
"field": { "type": "string", "minLength": 1 },
|
|
3057
|
+
"dir": { "type": "string", "enum": ["asc", "desc"], "default": "asc" }
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
3062
|
+
},
|
|
3063
|
+
"toggle": {
|
|
3064
|
+
"type": "object",
|
|
3065
|
+
"required": ["key", "removes"],
|
|
3066
|
+
"additionalProperties": false,
|
|
3067
|
+
"description": "The workspace key a switch writes. While it is on (true, 1, \"1\", \"true\") the filter keys in `removes` are dropped, so a list that hides finished work shows it.",
|
|
3068
|
+
"properties": {
|
|
3069
|
+
"key": { "type": "string", "minLength": 1 },
|
|
3070
|
+
"removes": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } }
|
|
3071
|
+
}
|
|
2988
3072
|
}
|
|
2989
3073
|
}
|
|
2990
3074
|
},
|
package/src/utils/facets.js
CHANGED
|
@@ -37,14 +37,29 @@ export function normalizeFacetBucket(bucket) {
|
|
|
37
37
|
return option
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The option id that stands for "this property holds no value".
|
|
42
|
+
*
|
|
43
|
+
* Chosen so no real bucket key can collide with it. Selecting it is sent to
|
|
44
|
+
* OpenRegister as `<property>_isnull=true` by {@link facetFilterParams}.
|
|
45
|
+
*
|
|
46
|
+
* @type {string}
|
|
47
|
+
*/
|
|
48
|
+
export const MISSING_VALUE = '__cn_missing__'
|
|
49
|
+
|
|
40
50
|
/**
|
|
41
51
|
* Normalise the `facets` block of a collection response into sidebar format.
|
|
42
52
|
*
|
|
43
53
|
* Fields whose facet carries no bucket list are skipped entirely, so a caller
|
|
44
54
|
* can tell an unfaceted field from one that faceted to nothing.
|
|
45
55
|
*
|
|
56
|
+
* What OpenRegister puts beside the buckets is carried through rather than
|
|
57
|
+
* dropped: `missing` (how many objects in scope hold no value, sent as
|
|
58
|
+
* `{ results }` or `{ count }`), `type` and `title`. A missing count is only
|
|
59
|
+
* set when the backend sent one, for the same reason a bucket count is.
|
|
60
|
+
*
|
|
46
61
|
* @param {object} facets The `facets` block of an API response
|
|
47
|
-
* @return {object} `{ fieldName: { values: [{ value, count?, label? }] } }`
|
|
62
|
+
* @return {object} `{ fieldName: { values: [{ value, count?, label? }], missing?, type?, title? } }`
|
|
48
63
|
*/
|
|
49
64
|
export function normalizeFacets(facets) {
|
|
50
65
|
const transformed = {}
|
|
@@ -54,12 +69,69 @@ export function normalizeFacets(facets) {
|
|
|
54
69
|
if (!buckets) {
|
|
55
70
|
continue
|
|
56
71
|
}
|
|
57
|
-
|
|
72
|
+
const normalized = { values: buckets.map(normalizeFacetBucket) }
|
|
73
|
+
|
|
74
|
+
const missing = facet.missing?.results ?? facet.missing?.count
|
|
75
|
+
if (typeof missing === 'number') {
|
|
76
|
+
normalized.missing = missing
|
|
77
|
+
}
|
|
78
|
+
for (const name of ['type', 'title']) {
|
|
79
|
+
if (facet[name] !== undefined && facet[name] !== null) {
|
|
80
|
+
normalized[name] = facet[name]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
transformed[key] = normalized
|
|
58
85
|
}
|
|
59
86
|
|
|
60
87
|
return transformed
|
|
61
88
|
}
|
|
62
89
|
|
|
90
|
+
/**
|
|
91
|
+
* The sidebar options for one normalised facet, with the missing-value option
|
|
92
|
+
* appended when some objects hold no value.
|
|
93
|
+
*
|
|
94
|
+
* @param {object|undefined} facet A normalised facet from {@link normalizeFacets}
|
|
95
|
+
* @param {function(number): string} missingLabel Renders the missing option's label from its count
|
|
96
|
+
* @return {Array<{id: unknown, label: string}>|null} The options, or null when the
|
|
97
|
+
* facet offers nothing, so the caller can fall back to its static options
|
|
98
|
+
*/
|
|
99
|
+
export function facetOptions(facet, missingLabel) {
|
|
100
|
+
const options = (facet?.values || []).map((v) => ({
|
|
101
|
+
id: v.value,
|
|
102
|
+
label: facetOptionLabel(v),
|
|
103
|
+
}))
|
|
104
|
+
|
|
105
|
+
if (typeof facet?.missing === 'number' && facet.missing > 0) {
|
|
106
|
+
options.push({ id: MISSING_VALUE, label: missingLabel(facet.missing) })
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return options.length > 0 ? options : null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The query parameters one facet selection sends.
|
|
114
|
+
*
|
|
115
|
+
* An ordinary selection is the property itself, one value unwrapped and
|
|
116
|
+
* several as a list. The missing-value option is exclusive: no object both
|
|
117
|
+
* holds one of the chosen values and holds none, so combining them would
|
|
118
|
+
* always answer an empty list. It is sent as `<property>_isnull=true`.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} key The filtered property
|
|
121
|
+
* @param {Array} values The selected option ids
|
|
122
|
+
* @return {object} The parameters to merge into the request
|
|
123
|
+
*/
|
|
124
|
+
export function facetFilterParams(key, values) {
|
|
125
|
+
if (!Array.isArray(values) || values.length === 0) {
|
|
126
|
+
return {}
|
|
127
|
+
}
|
|
128
|
+
if (values.includes(MISSING_VALUE)) {
|
|
129
|
+
return { [`${key}_isnull`]: 'true' }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { [key]: values.length === 1 ? values[0] : values }
|
|
133
|
+
}
|
|
134
|
+
|
|
63
135
|
/**
|
|
64
136
|
* Build the visible label for one faceted filter option.
|
|
65
137
|
*
|
|
@@ -49,6 +49,11 @@ function formatValue(value, format, now) {
|
|
|
49
49
|
if (format === 'datetime') {
|
|
50
50
|
return formatBoardDateTime(value, now)
|
|
51
51
|
}
|
|
52
|
+
if (format === 'days') {
|
|
53
|
+
// Whole days from the date to now (0 for today; negative for a future date).
|
|
54
|
+
const date = parseDateValue(value)
|
|
55
|
+
return date === null ? '' : String(Math.floor((now.getTime() - date.getTime()) / 86400000))
|
|
56
|
+
}
|
|
52
57
|
return String(value)
|
|
53
58
|
}
|
|
54
59
|
|
|
@@ -57,7 +62,8 @@ function formatValue(value, format, now) {
|
|
|
57
62
|
* `"Termijn {deadline|date}"` from an object.
|
|
58
63
|
*
|
|
59
64
|
* Every `{path}` token is a dot path into the object; `{path|date}` and
|
|
60
|
-
* `{path|datetime}` format it as the boards write dates
|
|
65
|
+
* `{path|datetime}` format it as the boards write dates, and `{path|days}` is the
|
|
66
|
+
* whole days since that date ("has waited {createdAt|days} days"). A token without a
|
|
61
67
|
* value empties the WHOLE template, because "Termijn " is worse than nothing.
|
|
62
68
|
* A template without tokens is returned as written (a constant label).
|
|
63
69
|
*
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* pageControls: how a list widget listens to the page's controls.
|
|
6
|
+
*
|
|
7
|
+
* A page control (a `workspace-filter` select or switch) writes a value to the
|
|
8
|
+
* page workspace. A list widget declares which control it listens to and what
|
|
9
|
+
* the value means for it:
|
|
10
|
+
*
|
|
11
|
+
* pageControls: {
|
|
12
|
+
* sort: { key: "sort", map: { deadline: { field: "slaDeadline", dir: "asc" } } },
|
|
13
|
+
* toggle: { key: "showDone", removes: ["status"] }
|
|
14
|
+
* }
|
|
15
|
+
*
|
|
16
|
+
* `sort`: the workspace value at `key` picks an entry of `map` (the sort the
|
|
17
|
+
* list uses); a value with no entry leaves the list's own sort.
|
|
18
|
+
* `toggle`: while the workspace value at `key` is on (true, "1", "true"), the
|
|
19
|
+
* filter keys in `removes` are dropped, so a list that hides finished work
|
|
20
|
+
* shows it.
|
|
21
|
+
*
|
|
22
|
+
* @module utils/pageControls
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Whether a workspace value reads as on.
|
|
27
|
+
*
|
|
28
|
+
* @param {unknown} value The value.
|
|
29
|
+
* @return {boolean} True for true, 1, "1" and "true".
|
|
30
|
+
*/
|
|
31
|
+
export function isOn(value) {
|
|
32
|
+
return value === true || value === 1 || value === '1' || value === 'true'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The sort a list uses once the page's sort control is read.
|
|
37
|
+
*
|
|
38
|
+
* @param {object|null} pageControls The widget's `pageControls`.
|
|
39
|
+
* @param {object} workspace The plain workspace bag.
|
|
40
|
+
* @param {{field: string, dir?: string}|null} own The list's own sort.
|
|
41
|
+
* @return {{field: string, dir?: string}|null} The sort to use.
|
|
42
|
+
* @spec openspec/changes/screens-page-controls/specs/page-controls/spec.md#requirement-a-list-listens-to-the-pages-controls
|
|
43
|
+
*/
|
|
44
|
+
export function pageSort(pageControls, workspace, own) {
|
|
45
|
+
const listen = pageControls && typeof pageControls === 'object' ? pageControls.sort : null
|
|
46
|
+
if (!listen || typeof listen.key !== 'string' || !listen.map || typeof listen.map !== 'object') {
|
|
47
|
+
return own
|
|
48
|
+
}
|
|
49
|
+
const chosen = workspace ? workspace[listen.key] : undefined
|
|
50
|
+
const entry = chosen !== undefined && chosen !== null ? listen.map[String(chosen)] : null
|
|
51
|
+
return entry && typeof entry === 'object' && typeof entry.field === 'string' && entry.field !== '' ? entry : own
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The filter a list uses once the page's toggle is read: with the toggle on,
|
|
56
|
+
* the keys it `removes` are gone.
|
|
57
|
+
*
|
|
58
|
+
* @param {object|null} pageControls The widget's `pageControls`.
|
|
59
|
+
* @param {object} workspace The plain workspace bag.
|
|
60
|
+
* @param {object} filter The list's own filter (before token resolution).
|
|
61
|
+
* @return {object} The filter to use (the same object when nothing is removed).
|
|
62
|
+
* @spec openspec/changes/screens-page-controls/specs/page-controls/spec.md#requirement-a-list-listens-to-the-pages-controls
|
|
63
|
+
*/
|
|
64
|
+
export function pageFilter(pageControls, workspace, filter) {
|
|
65
|
+
const listen = pageControls && typeof pageControls === 'object' ? pageControls.toggle : null
|
|
66
|
+
const base = filter && typeof filter === 'object' ? filter : {}
|
|
67
|
+
if (!listen || typeof listen.key !== 'string' || !Array.isArray(listen.removes)) {
|
|
68
|
+
return base
|
|
69
|
+
}
|
|
70
|
+
if (!isOn(workspace ? workspace[listen.key] : undefined)) {
|
|
71
|
+
return base
|
|
72
|
+
}
|
|
73
|
+
const out = { ...base }
|
|
74
|
+
for (const key of listen.removes) {
|
|
75
|
+
delete out[key]
|
|
76
|
+
}
|
|
77
|
+
return out
|
|
78
|
+
}
|
|
@@ -37,6 +37,42 @@ export function personalLensTabs(lenses) {
|
|
|
37
37
|
.map((id) => ({ label: labels[id], filter: { [LENS_FILTER_KEYS[id]]: true }, lens: id }))
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Turn the `empty` / `notEmpty` shorthand of quick filters into a filter.
|
|
42
|
+
*
|
|
43
|
+
* `{ label: "Not logged in yet", empty: "lastLoginAt" }` is the rows where the
|
|
44
|
+
* field has no value: the filter `{ lastLoginAt: { isnull: true } }`, which
|
|
45
|
+
* OpenRegister reads as `lastLoginAt[isnull]=true`. `notEmpty` is the rows
|
|
46
|
+
* where it has one. Either takes a field or a list of fields and merges with
|
|
47
|
+
* the entry's own `filter`. Returns the same array when no entry uses it.
|
|
48
|
+
*
|
|
49
|
+
* @param {Array<object>|null} quickFilters The page's quick filters.
|
|
50
|
+
* @return {Array<object>|null} The quick filters with the shorthand expanded.
|
|
51
|
+
*/
|
|
52
|
+
export function expandEmptyFilters(quickFilters) {
|
|
53
|
+
if (!Array.isArray(quickFilters)) {
|
|
54
|
+
return quickFilters
|
|
55
|
+
}
|
|
56
|
+
const uses = (tab) => tab && typeof tab === 'object' && (tab.empty || tab.notEmpty)
|
|
57
|
+
if (!quickFilters.some(uses)) {
|
|
58
|
+
return quickFilters
|
|
59
|
+
}
|
|
60
|
+
const fieldsOf = (value) => (Array.isArray(value) ? value : [value]).filter((f) => typeof f === 'string' && f !== '')
|
|
61
|
+
return quickFilters.map((tab) => {
|
|
62
|
+
if (!uses(tab)) {
|
|
63
|
+
return tab
|
|
64
|
+
}
|
|
65
|
+
const filter = { ...(tab.filter && typeof tab.filter === 'object' ? tab.filter : {}) }
|
|
66
|
+
for (const field of fieldsOf(tab.empty)) {
|
|
67
|
+
filter[field] = { isnull: true }
|
|
68
|
+
}
|
|
69
|
+
for (const field of fieldsOf(tab.notEmpty)) {
|
|
70
|
+
filter[field] = { isnull: false }
|
|
71
|
+
}
|
|
72
|
+
return { ...tab, filter }
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
40
76
|
/**
|
|
41
77
|
* The page's quick filters with its personal lenses appended. A page with no
|
|
42
78
|
* quick filters of its own gets an "All" tab first, so a lens is not active
|
|
@@ -48,10 +84,11 @@ export function personalLensTabs(lenses) {
|
|
|
48
84
|
*/
|
|
49
85
|
export function withPersonalLenses(quickFilters, lenses) {
|
|
50
86
|
const tabs = personalLensTabs(lenses)
|
|
87
|
+
const expanded = expandEmptyFilters(quickFilters)
|
|
51
88
|
if (tabs.length === 0) {
|
|
52
|
-
return
|
|
89
|
+
return expanded
|
|
53
90
|
}
|
|
54
|
-
const own = Array.isArray(
|
|
91
|
+
const own = Array.isArray(expanded) ? expanded : []
|
|
55
92
|
return own.length > 0
|
|
56
93
|
? [...own, ...tabs]
|
|
57
94
|
: [{ label: t('nextcloud-vue', 'All'), filter: {}, default: true }, ...tabs]
|
|
@@ -67,7 +67,9 @@
|
|
|
67
67
|
|
|
68
68
|
import { getCurrentUser } from '@nextcloud/auth'
|
|
69
69
|
import { TODAY_DELTA_RE } from './sentinelTokens.js'
|
|
70
|
-
|
|
70
|
+
// userGroupsPeek, not widgetVisibility: that one imports @nextcloud/axios, and
|
|
71
|
+
// this resolver runs on public pages and in node (see userGroupsPeek.js).
|
|
72
|
+
import { peekCurrentUserGroups } from './userGroupsPeek.js'
|
|
71
73
|
|
|
72
74
|
/** The filter token for the current user's group ids. */
|
|
73
75
|
export const MY_GROUPS_TOKEN = '@myGroups'
|
|
@@ -54,6 +54,8 @@ export const SENTINEL_RESOLVERS = Object.freeze({
|
|
|
54
54
|
declarative: 'fetchAggregate (OpenRegister server-side)',
|
|
55
55
|
visibleWhen: 'evaluateVisibleWhen (utils/visibleWhen.js)',
|
|
56
56
|
answer: 'resolveAssignValue (utils/formAssign.js)',
|
|
57
|
+
row: 'CnFieldListWidget (rowActions request)',
|
|
58
|
+
subject: 'CnBannerWidget (action route)',
|
|
57
59
|
})
|
|
58
60
|
|
|
59
61
|
/**
|
|
@@ -83,6 +83,12 @@ export const SENTINEL_TOKEN_PATTERNS = Object.freeze({
|
|
|
83
83
|
visibleWhen: '^@total$',
|
|
84
84
|
// form-live-values: another answer in the same form, read by a field's `assign` rule.
|
|
85
85
|
answer: '^@answer\\.[A-Za-z][A-Za-z0-9_]*$',
|
|
86
|
+
// A field of the row a row action is run on (`@row.<field>`, dot path), read by the
|
|
87
|
+
// field-list widget's `rowActions` request URL and payload.
|
|
88
|
+
row: '^@row\\.[A-Za-z][A-Za-z0-9_.@]*$',
|
|
89
|
+
// A field of the record a start-here attention card names (`@subject.<field>`, dot path),
|
|
90
|
+
// read by the banner widget's action routes.
|
|
91
|
+
subject: '^@subject\\.[A-Za-z][A-Za-z0-9_.]*$',
|
|
86
92
|
// Deprecated-but-not-yet-removed tokens. Kept in the schema union so
|
|
87
93
|
// deployed manifests still validate during the migration window; the gate
|
|
88
94
|
// downgrades them to a WARN (see SENTINEL_DEPRECATIONS).
|
|
@@ -90,7 +96,7 @@ export const SENTINEL_TOKEN_PATTERNS = Object.freeze({
|
|
|
90
96
|
})
|
|
91
97
|
|
|
92
98
|
/**
|
|
93
|
-
* The
|
|
99
|
+
* The nine canonical context names (excludes `deprecated`, which is a
|
|
94
100
|
* transitional overlay, not a resolution context).
|
|
95
101
|
*
|
|
96
102
|
* @type {string[]}
|
|
@@ -104,6 +110,8 @@ export const SENTINEL_CONTEXTS = Object.freeze([
|
|
|
104
110
|
'declarative',
|
|
105
111
|
'visibleWhen',
|
|
106
112
|
'answer',
|
|
113
|
+
'row',
|
|
114
|
+
'subject',
|
|
107
115
|
])
|
|
108
116
|
|
|
109
117
|
/**
|
|
@@ -154,6 +162,16 @@ export const SENTINEL_VOCABULARY = Object.freeze({
|
|
|
154
162
|
description: 'Another answer in the same form, read by a field\'s `assign` rule.',
|
|
155
163
|
members: ['@answer.<field>'],
|
|
156
164
|
},
|
|
165
|
+
row: {
|
|
166
|
+
resolver: 'CnFieldListWidget (rowActions request url and payload)',
|
|
167
|
+
description: 'A field of the row a row action runs on, read by the field-list widget when it sends the action\'s request. Dot paths are allowed (`@row.owner.id`).',
|
|
168
|
+
members: ['@row.<field>'],
|
|
169
|
+
},
|
|
170
|
+
subject: {
|
|
171
|
+
resolver: 'CnBannerWidget (action route params and query)',
|
|
172
|
+
description: 'A field of the record an attention card names (the first row of its ordered `visibleWhen` source query), read when the card builds an action\'s route. Dot paths are allowed (`@subject.owner.id`).',
|
|
173
|
+
members: ['@subject.<field>'],
|
|
174
|
+
},
|
|
157
175
|
})
|
|
158
176
|
|
|
159
177
|
/**
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* The synchronous, reactive read of the current user's group ids, without
|
|
6
|
+
* the request that loads them.
|
|
7
|
+
*
|
|
8
|
+
* `resolveFilterTokens` (and through it `visibleWhen`) needs the groups for
|
|
9
|
+
* `@myGroups`, and those utilities are used on public, signed-out pages too.
|
|
10
|
+
* Importing `widgetVisibility.js` statically pulled `@nextcloud/axios` and
|
|
11
|
+
* `@nextcloud/router` into every bundle that resolves a filter token: a public
|
|
12
|
+
* site bundle broke its size budget, and node specs crashed because axios
|
|
13
|
+
* reads the request token when it is imported. This module imports no
|
|
14
|
+
* `@nextcloud/*` package. It loads `widgetVisibility.js` lazily, only the
|
|
15
|
+
* first time a signed-in user's groups are actually peeked.
|
|
16
|
+
*
|
|
17
|
+
* @module utils/userGroupsPeek
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { reactive } from 'vue'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Reactive mirror of the group cache, keyed by user id. A computed that reads
|
|
24
|
+
* a missing key tracks it, so it re-runs when the groups arrive.
|
|
25
|
+
*
|
|
26
|
+
* @type {{ byUser: Record<string, string[]> }}
|
|
27
|
+
*/
|
|
28
|
+
const _peek = reactive({ byUser: {} })
|
|
29
|
+
|
|
30
|
+
/** User ids whose groups a peek already asked for (not reactive on purpose). */
|
|
31
|
+
const _peekRequested = new Set()
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get the current Nextcloud user ID from OC.currentUser.
|
|
35
|
+
*
|
|
36
|
+
* @return {string|null} The current user ID, or null if not logged in
|
|
37
|
+
*/
|
|
38
|
+
export function getCurrentUserId() {
|
|
39
|
+
return window.OC?.currentUser?.uid
|
|
40
|
+
|| window.OC?.currentUser
|
|
41
|
+
|| null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Synchronous, reactive read of the current user's group ids, for code that
|
|
46
|
+
* cannot await (the `@myGroups` filter token). Returns the ids once they are
|
|
47
|
+
* loaded, `null` while the first request is in flight (and starts that
|
|
48
|
+
* request on first use), and `[]` when nobody is signed in. Read inside a
|
|
49
|
+
* computed, the computed re-runs when the groups arrive.
|
|
50
|
+
*
|
|
51
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/schema-utilities/spec.md#requirement-mygroups-resolves-to-the-current-users-group-ids
|
|
52
|
+
* @return {string[]|null} The group ids, or null while loading.
|
|
53
|
+
*/
|
|
54
|
+
export function peekCurrentUserGroups() {
|
|
55
|
+
const userId = getCurrentUserId()
|
|
56
|
+
if (!userId) {
|
|
57
|
+
return []
|
|
58
|
+
}
|
|
59
|
+
const known = _peek.byUser[userId]
|
|
60
|
+
if (Array.isArray(known)) {
|
|
61
|
+
return known
|
|
62
|
+
}
|
|
63
|
+
if (!_peekRequested.has(userId)) {
|
|
64
|
+
_peekRequested.add(userId)
|
|
65
|
+
import('./widgetVisibility.js')
|
|
66
|
+
.then((module) => module.getCurrentUserGroups())
|
|
67
|
+
.then((groups) => {
|
|
68
|
+
_peek.byUser[userId] = Array.isArray(groups) ? [...groups] : []
|
|
69
|
+
})
|
|
70
|
+
.catch(() => {
|
|
71
|
+
_peek.byUser[userId] = []
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Forget every peeked group list. `resetVisibilityCache()` calls this.
|
|
79
|
+
*
|
|
80
|
+
* @spec exclude test helper behind resetVisibilityCache, no behaviour of its own
|
|
81
|
+
*/
|
|
82
|
+
export function resetGroupsPeek() {
|
|
83
|
+
_peekRequested.clear()
|
|
84
|
+
for (const key of Object.keys(_peek.byUser)) {
|
|
85
|
+
delete _peek.byUser[key]
|
|
86
|
+
}
|
|
87
|
+
}
|