@conduction/nextcloud-vue 2.83.0 → 2.85.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/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +131 -3
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +54 -18
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +232 -4
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +95 -51
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue.js +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js +22 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue2.js.map +1 -1
- package/dist/esm/components/CnBodySections/CnBodySections.vue3.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue.js +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js +47 -4
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnCountdownWidget/CnCountdownWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +239 -3
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +5 -5
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +50 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -12
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnDeltaWidget/CnDeltaWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +226 -8
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +3 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +18 -5
- 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.vue.js +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +38 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +45 -5
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnGaugeWidget/CnGaugeWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +548 -77
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +191 -82
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +11 -2
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js +29 -6
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnInteractionFormWidget/CnInteractionFormWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +126 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +106 -3
- 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/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +67 -5
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +181 -124
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +135 -2
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +95 -43
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +5 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue.js +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +72 -5
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +33 -8
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +135 -11
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +17 -2
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +39 -6
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +140 -7
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +37 -9
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +6 -5
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue.js +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +16 -0
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js +53 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js +3 -2
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +135 -0
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +19 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +37 -19
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/composables/useListView.js +21 -0
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +68 -6
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +133 -105
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +38 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +78 -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 +1030 -533
- package/dist/esm/store/useObjectStore.js +16 -2
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/boardColumns.js +8 -1
- package/dist/esm/utils/boardColumns.js.map +1 -1
- package/dist/esm/utils/boardReferenceColumns.js +278 -0
- package/dist/esm/utils/boardReferenceColumns.js.map +1 -0
- package/dist/esm/utils/buildManifest.js +41 -4
- package/dist/esm/utils/buildManifest.js.map +1 -1
- package/dist/esm/utils/detailWidgetVisibility.js +149 -0
- package/dist/esm/utils/detailWidgetVisibility.js.map +1 -0
- package/dist/esm/utils/formatMetric.js +39 -3
- package/dist/esm/utils/formatMetric.js.map +1 -1
- package/dist/esm/utils/gridStack.js +40 -1
- package/dist/esm/utils/gridStack.js.map +1 -1
- package/dist/esm/utils/humaniseKey.js +41 -0
- package/dist/esm/utils/humaniseKey.js.map +1 -0
- package/dist/esm/utils/notificationRuleLabel.js +6 -23
- package/dist/esm/utils/notificationRuleLabel.js.map +1 -1
- package/dist/esm/utils/resolveManifestTokens.js +1 -0
- package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +9 -0
- package/dist/esm/utils/sentinelTokens.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/esm/utils/widgetDispatch.js +8 -2
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +17159 -13422
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1030 -533
- package/l10n/en.json +38 -1
- package/l10n/nl.json +78 -1
- package/package.json +1 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +160 -8
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +1 -1
- package/src/components/CnBoardView/CnBoardView.vue +262 -6
- package/src/components/CnBodySections/CnBodySections.vue +22 -1
- package/src/components/CnCountdownWidget/CnCountdownWidget.vue +47 -4
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +244 -8
- package/src/components/CnDashboardPage/CnDashboardPage.vue +62 -2
- package/src/components/CnDeltaWidget/CnDeltaWidget.vue +1 -1
- package/src/components/CnDetailPage/CnDetailPage.md +3 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +226 -8
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +3 -1
- package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -5
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +63 -1
- package/src/components/CnGaugeWidget/CnGaugeWidget.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +586 -25
- package/src/components/CnIndexPage/useSelfFetchList.js +10 -1
- package/src/components/CnInteractionFormWidget/CnInteractionFormWidget.vue +29 -6
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +154 -5
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +106 -3
- package/src/components/CnRegisterMapping/CnRegisterMapping.md +5 -0
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +231 -96
- package/src/components/CnReportsPage/CnReportsPage.vue +179 -1
- package/src/components/CnSettingsPage/CnSettingsPage.vue +5 -1
- package/src/components/CnStagesWidget/CnStagesWidget.vue +103 -6
- package/src/components/CnStatWidget/CnStatWidget.vue +137 -12
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +34 -3
- package/src/components/CnTabsWidget/CnTabsWidget.vue +39 -6
- package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +140 -7
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.md +2 -1
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +42 -13
- package/src/components/CnWalkthrough/CnWalkthrough.vue +16 -0
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
- package/src/components/CnWidgetCardGrid/CnWidgetCardGrid.vue +55 -2
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +135 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +10 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +44 -1
- package/src/composables/useListView.js +21 -0
- package/src/css/board.css +65 -4
- package/src/css/dialog.css +4 -2
- package/src/css/form-field.css +14 -0
- package/src/css/form-widget.css +11 -0
- package/src/css/index-page.css +49 -0
- package/src/css/kpi-card.css +13 -0
- package/src/css/look-board-reports.css +58 -0
- package/src/css/look-board.css +108 -7
- package/src/dialogs/CnTransitionInputDialog.vue +87 -7
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +137 -6
- package/src/store/useObjectStore.js +16 -2
- package/src/utils/boardColumns.js +8 -1
- package/src/utils/boardReferenceColumns.js +274 -0
- package/src/utils/buildManifest.js +40 -3
- package/src/utils/detailWidgetVisibility.js +145 -0
- package/src/utils/formatMetric.js +38 -2
- package/src/utils/gridStack.js +39 -0
- package/src/utils/humaniseKey.js +38 -0
- package/src/utils/notificationRuleLabel.js +5 -23
- package/src/utils/resolveManifestTokens.js +1 -0
- package/src/utils/sentinelTokens.js +9 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/widgetDispatch.js +8 -2
|
@@ -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.99.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -150,6 +150,7 @@
|
|
|
150
150
|
"title": { "type": "string", "description": "i18n key / text for the tour name (restart menu)." },
|
|
151
151
|
"trigger": { "type": "string", "enum": ["first-visit", "version-bump", "empty-index", "manual"], "default": "manual", "description": "When the tour auto-starts." },
|
|
152
152
|
"minAppVersion": { "type": "string", "description": "Semver; the tour is inert below this app version." },
|
|
153
|
+
"startOn": { "type": "array", "items": { "type": "string", "minLength": 1 }, "minItems": 1, "uniqueItems": true, "description": "Route names the tour may auto-start on. A user who first lands anywhere else, such as on a case opened from a link, does not get the tour there: it waits until they reach one of these routes. Without it a tour whose first step is centered starts wherever the user lands. Has no effect on a tour started by hand." },
|
|
153
154
|
"steps": {
|
|
154
155
|
"type": "array",
|
|
155
156
|
"description": "Ordered steps.",
|
|
@@ -808,6 +809,11 @@
|
|
|
808
809
|
"pattern": "^@row\\.[A-Za-z][A-Za-z0-9_.@]*$",
|
|
809
810
|
"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
|
},
|
|
812
|
+
"sentinelContextToken": {
|
|
813
|
+
"type": "string",
|
|
814
|
+
"pattern": "^@context\\.[A-Za-z][A-Za-z0-9_.]*$",
|
|
815
|
+
"description": "Detail context: a record a detail page reads beside the loaded one (`config.visibilityContext`), read by a grid widget's or a tab's `visibleWhen` field as @context.<name>.<field> (dot path allowed). Mirrors SENTINEL_TOKEN_PATTERNS.context."
|
|
816
|
+
},
|
|
811
817
|
"sentinelSubjectToken": {
|
|
812
818
|
"type": "string",
|
|
813
819
|
"pattern": "^@subject\\.[A-Za-z][A-Za-z0-9_.]*$",
|
|
@@ -826,6 +832,7 @@
|
|
|
826
832
|
{ "$ref": "#/$defs/sentinelAnswerToken" },
|
|
827
833
|
{ "$ref": "#/$defs/sentinelRowToken" },
|
|
828
834
|
{ "$ref": "#/$defs/sentinelSubjectToken" },
|
|
835
|
+
{ "$ref": "#/$defs/sentinelContextToken" },
|
|
829
836
|
{ "$ref": "#/$defs/sentinelDeprecatedToken" }
|
|
830
837
|
]
|
|
831
838
|
},
|
|
@@ -1109,7 +1116,12 @@
|
|
|
1109
1116
|
"additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] }
|
|
1110
1117
|
},
|
|
1111
1118
|
"due": { "type": "string", "description": "The date field of the footer's due text, marked by `dueRule`." },
|
|
1112
|
-
"owner": { "type": "string", "description": "The field of the owner shown as an avatar (initials) at the footer's end." }
|
|
1119
|
+
"owner": { "type": "string", "description": "The field of the owner shown as an avatar (initials) at the footer's end." },
|
|
1120
|
+
"formatters": {
|
|
1121
|
+
"type": "object",
|
|
1122
|
+
"description": "screens-board-reference-columns (2.97.0): a card field (one of the role fields above) to the id of a formatter in the app's `cnFormatters` registry, the one table columns name in `formatter`. The card shows the formatted text, so a reference reads as its name (`{ \"caseType\": \"caseTypeName\" }`).",
|
|
1123
|
+
"additionalProperties": { "type": "string", "minLength": 1 }
|
|
1124
|
+
}
|
|
1113
1125
|
}
|
|
1114
1126
|
},
|
|
1115
1127
|
"colorField": {
|
|
@@ -1138,6 +1150,95 @@
|
|
|
1138
1150
|
"dueRule": {
|
|
1139
1151
|
"$ref": "#/$defs/dueRule",
|
|
1140
1152
|
"description": "Marks late cards. A card whose date is before today gets an error edge and the label 'Overdue'; one due within `soonDays` gets 'Due soon'. Omitted, no card is marked."
|
|
1153
|
+
},
|
|
1154
|
+
"columnFields": {
|
|
1155
|
+
"type": "array",
|
|
1156
|
+
"minItems": 1,
|
|
1157
|
+
"description": "screens-board-reference-columns (2.97.0): the fields the columns can come from, offered in a \"Columns:\" switch after the search when there are two or more (\"status | type | assignee\"). The entry on `statusField` is the default. An entry with a `source` takes its columns from the rows the field REFERENCES (a case's status pointing at statusType rows), fetched, not from the values on the cards; `kind: \"values\"` (a user id, which references no schema) takes the values present plus a column for no value. Without a `source` or `kind` the field's enum or lifecycle states are read as before. A drop writes the reference: through `moveHandler` when named, else a PATCH of the field on the row.",
|
|
1158
|
+
"items": {
|
|
1159
|
+
"type": "object",
|
|
1160
|
+
"additionalProperties": false,
|
|
1161
|
+
"required": ["field"],
|
|
1162
|
+
"properties": {
|
|
1163
|
+
"field": { "type": "string", "minLength": 1, "description": "The row field the columns come from." },
|
|
1164
|
+
"label": { "type": "string", "description": "Its name in the column switch (an i18n key). Defaults to the field." },
|
|
1165
|
+
"kind": { "type": "string", "enum": ["values"], "description": "`values`: the field references no schema (a Nextcloud user id); the columns are the values present on the rows, by `valueLabels` or the value itself, then one column for no value (`emptyLabel`)." },
|
|
1166
|
+
"valueLabels": { "type": "object", "additionalProperties": { "type": "string" }, "description": "`kind: values`: raw value to the column label." },
|
|
1167
|
+
"emptyLabel": { "type": "string", "description": "`kind: values`: the label of the column for no value (an i18n key). Defaults to \"Nobody\"." },
|
|
1168
|
+
"source": {
|
|
1169
|
+
"type": "object",
|
|
1170
|
+
"additionalProperties": false,
|
|
1171
|
+
"description": "The referenced rows that are the columns: one list request to `register`/`schema` (defaults: the page's register and the property's `$ref`).",
|
|
1172
|
+
"properties": {
|
|
1173
|
+
"register": { "type": "string", "minLength": 1 },
|
|
1174
|
+
"schema": { "type": "string", "minLength": 1 },
|
|
1175
|
+
"labelField": { "type": "string", "description": "The referenced row's field that names a column. Defaults to `name`, then `title`." },
|
|
1176
|
+
"orderField": { "type": "string", "description": "The referenced row's numeric field the columns are ordered by." },
|
|
1177
|
+
"colorField": { "type": "string", "description": "The referenced row's field that holds the column's dot colour (a badge variant or a CSS colour)." },
|
|
1178
|
+
"filter": { "type": "object", "description": "Equality filters on the referenced rows, e.g. `{ \"isFinal\": false }` to leave out end states.", "additionalProperties": { "type": ["string", "number", "boolean"] } },
|
|
1179
|
+
"mergeBy": { "type": "string", "description": "Merge referenced rows with the same value of this field into one column (statuses of every case type by `name`). A drop then writes the row of the card's own parent (`narrowBy`), and is refused when that parent has none." },
|
|
1180
|
+
"narrowBy": {
|
|
1181
|
+
"type": "object",
|
|
1182
|
+
"additionalProperties": false,
|
|
1183
|
+
"required": ["field", "rowField"],
|
|
1184
|
+
"description": "The parent link: the referenced row's `field` equals the card's `rowField` (statusType.caseType = case.caseType). While the page filters on `rowField` with one value, the columns are that parent's rows only.",
|
|
1185
|
+
"properties": {
|
|
1186
|
+
"field": { "type": "string", "minLength": 1 },
|
|
1187
|
+
"rowField": { "type": "string", "minLength": 1 }
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
},
|
|
1192
|
+
"readOnly": { "type": "boolean", "default": false, "description": "No drag and no Move to while the columns come from this field." },
|
|
1193
|
+
"moveHandler": { "type": "string", "minLength": 1, "description": "A handler in the app's registry that makes the move: it gets `{ card, toKey, field, value, target }` (value: the referenced id to write; target: its row) and returns a promise; a rejection shows its message as the refusal. Absent, the field is written with a PATCH." },
|
|
1194
|
+
"moveTargetsHandler": { "type": "string", "minLength": 1, "description": "A handler in the app's registry that answers where one card may go: it gets `{ card, field }` and returns the values (referenced ids) it can reach. Columns out of reach fade during a drag, read \"not allowed\" in the card menu, and refuse a drop." }
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
},
|
|
1198
|
+
"moveHandler": {
|
|
1199
|
+
"type": "string",
|
|
1200
|
+
"minLength": 1,
|
|
1201
|
+
"description": "screens-board-reference-columns (2.97.0): a handler in the app's registry that moves a card on `statusField` (the app's transition), with the `columnFields[].moveHandler` call shape. Lets a manifest-only page run the host's transition, which the `runTransition` prop could not."
|
|
1202
|
+
},
|
|
1203
|
+
"moveTargetsHandler": {
|
|
1204
|
+
"type": "string",
|
|
1205
|
+
"minLength": 1,
|
|
1206
|
+
"description": "screens-board-reference-columns (2.97.0): where a card on `statusField` may go, with the `columnFields[].moveTargetsHandler` call shape."
|
|
1207
|
+
},
|
|
1208
|
+
"narrow": {
|
|
1209
|
+
"type": "object",
|
|
1210
|
+
"additionalProperties": false,
|
|
1211
|
+
"required": ["field"],
|
|
1212
|
+
"description": "screens-board-reference-columns (2.97.0): a select after the search (\"Case type: All case types | Woo-verzoek | ...\") that sets the page filter on `field`. With a reference column's `narrowBy.rowField` on the same field, the columns narrow to that parent's rows.",
|
|
1213
|
+
"properties": {
|
|
1214
|
+
"field": { "type": "string", "minLength": 1, "description": "The row field the select filters on." },
|
|
1215
|
+
"label": { "type": "string", "description": "The select's label (an i18n key), shown with a colon. Defaults to the property's title." },
|
|
1216
|
+
"allLabel": { "type": "string", "description": "The option for no narrowing (an i18n key). Defaults to \"All\"." },
|
|
1217
|
+
"source": {
|
|
1218
|
+
"type": "object",
|
|
1219
|
+
"additionalProperties": false,
|
|
1220
|
+
"description": "Where the options come from (defaults: the page's register and the property's `$ref`).",
|
|
1221
|
+
"properties": {
|
|
1222
|
+
"register": { "type": "string", "minLength": 1 },
|
|
1223
|
+
"schema": { "type": "string", "minLength": 1 },
|
|
1224
|
+
"labelField": { "type": "string", "description": "The option row's label field. Defaults to `title`." },
|
|
1225
|
+
"filter": { "type": "object", "additionalProperties": { "type": ["string", "number", "boolean"] } }
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
},
|
|
1230
|
+
"columnSwitchLabel": {
|
|
1231
|
+
"type": "string",
|
|
1232
|
+
"description": "screens-board-reference-columns (2.97.0): the column switch's label (an i18n key), shown with a colon. Defaults to \"Columns\"."
|
|
1233
|
+
},
|
|
1234
|
+
"hint": {
|
|
1235
|
+
"type": "string",
|
|
1236
|
+
"description": "screens-board-reference-columns (2.97.0): one sentence after the board's controls, in the secondary colour (an i18n key), e.g. how a move works."
|
|
1237
|
+
},
|
|
1238
|
+
"wrap": {
|
|
1239
|
+
"type": "boolean",
|
|
1240
|
+
"default": false,
|
|
1241
|
+
"description": "screens-board-reference-columns (2.97.0): wrap the columns onto more rows, as many to a row as fit at 240px, instead of one row that scrolls sideways. For a board with more stages than fit."
|
|
1141
1242
|
}
|
|
1142
1243
|
}
|
|
1143
1244
|
},
|
|
@@ -3526,7 +3627,7 @@
|
|
|
3526
3627
|
},
|
|
3527
3628
|
"titleField": {
|
|
3528
3629
|
"type": "string",
|
|
3529
|
-
"description": "type='wiki': property on the article object holding the page title. Defaults to 'title'. Maps to CnWikiPage `titleField` prop."
|
|
3630
|
+
"description": "type='wiki': property on the article object holding the page title. Defaults to 'title'. Maps to CnWikiPage `titleField` prop. type='detail' (detail-title-field): the record field (a dotted path, for example `number`) the header names the record by, as an invoice page names the invoice number instead of the schema title; an empty value falls back to the display name, and the breadcrumb's current crumb and the type eyebrow follow. Maps to CnDetailPage `titleField`."
|
|
3530
3631
|
},
|
|
3531
3632
|
"idParam": {
|
|
3532
3633
|
"type": "string",
|
|
@@ -4170,8 +4271,19 @@
|
|
|
4170
4271
|
"description": "type='index': show the page header (icon, title, description) inline. Default false: the title renders visually hidden and the sidebar names the page. Maps to CnIndexPage `showTitle`."
|
|
4171
4272
|
},
|
|
4172
4273
|
"countSubtitle": {
|
|
4173
|
-
"
|
|
4174
|
-
|
|
4274
|
+
"anyOf": [
|
|
4275
|
+
{ "type": "string" },
|
|
4276
|
+
{
|
|
4277
|
+
"type": "object",
|
|
4278
|
+
"additionalProperties": false,
|
|
4279
|
+
"required": ["singular", "plural"],
|
|
4280
|
+
"properties": {
|
|
4281
|
+
"singular": { "type": "string", "minLength": 1, "description": "i18n key of the line for a total of one, e.g. \"{total} invoice\"." },
|
|
4282
|
+
"plural": { "type": "string", "minLength": 1, "description": "i18n key of the line for any other total, e.g. \"{total} invoices\"." }
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
],
|
|
4286
|
+
"description": "type='index': an i18n key for the description line with `{total}` replaced by the collection's total, e.g. \"{total} open cases\" (zuiddrecht-pixel-gaps). Shown in place of `description` once a total is known. `{ singular, plural }` picks the form by the total through the app's plural translation (`translatePlural`), so \"1 factuur\" and \"12 facturen\" each read right (index-count-plural). Maps to CnIndexPage `countSubtitle`."
|
|
4175
4287
|
},
|
|
4176
4288
|
"showTitleIcon": {
|
|
4177
4289
|
"type": "boolean",
|
|
@@ -4297,7 +4409,7 @@
|
|
|
4297
4409
|
},
|
|
4298
4410
|
"headerButtons": {
|
|
4299
4411
|
"type": "array",
|
|
4300
|
-
"description": "type='index': buttons beside the page title, as the board's \"Export\" and \"New case\" (zuiddrecht-pixel-gaps-3). `action` is `add` (the Add flow; the label defaults to the Add label), `export` (the export leaf in `format`, else the export dialog; under the board look it reads \"Download\" by default), `actions-menu` (the page's header actions as one labelled menu button, absent when the page has none), `import`, `refresh`, or the id of a `headerActions` entry. Under the board look the buttons render in a fixed order whatever the manifest declares: export, actions-menu, other secondary buttons, the buildiq square, then the primary button. Shown only with `showTitle`; when they show, the actions bar drops its Views and Actions menus, and the Add button and Export menu when a button takes their action. Maps to CnIndexPage `headerButtons`.",
|
|
4412
|
+
"description": "type='index': buttons beside the page title, as the board's \"Export\" and \"New case\" (zuiddrecht-pixel-gaps-3). `action` is `add` (the Add flow; the label defaults to the Add label), `export` (the export leaf in `format`, else the export dialog; under the board look it reads \"Download\" by default), `actions-menu` (the page's header actions as one labelled menu button, absent when the page has none), `import`, `refresh`, or the id of a `headerActions` entry. Under the board look the buttons render in a fixed order whatever the manifest declares: export, actions-menu, other secondary buttons, the buildiq square, then the primary button. Shown only with `showTitle`; when they show, the actions bar drops its Views and Actions menus, and the Add button and Export menu when a button takes their action. Maps to CnIndexPage `headerButtons`. type='reports' (screens-reports-header-buttons, 2.96.0): the same buttons beside the reports page title, as board DcRapportages draws \"Downloaden\". There `label` is required and `action` is the id of a `headerActions` entry (same shape as on an index page), which says what the click does; every click also emits `header-action`. Under the board look the buildiq square sits between the secondary buttons and the primary one. Maps to CnReportsPage `headerButtons`.",
|
|
4301
4413
|
"items": {
|
|
4302
4414
|
"type": "object",
|
|
4303
4415
|
"additionalProperties": false,
|
|
@@ -4349,6 +4461,20 @@
|
|
|
4349
4461
|
"minLength": 1,
|
|
4350
4462
|
"description": "type='detail': the id of a widget in `widgets` to render inside the header on its own row under the title, without a card of its own, e.g. the stages bars (zuiddrecht-pixel-gaps-2). The widget leaves the body grid. Maps to CnDetailPage `headerWidget`."
|
|
4351
4463
|
},
|
|
4464
|
+
"visibilityContext": {
|
|
4465
|
+
"type": "object",
|
|
4466
|
+
"description": "type='detail': records the page reads beside the loaded one, so a widget's or a tab's `visibleWhen` can ask about them (detail-widgets-visible-when). `{ <name>: { field, register, schema } }`: `field` is the property of the loaded record holding the other record's id, and a condition reads that record as `@context.<name>.<path>`, e.g. `{ \"field\": \"@context.caseType.category\", \"op\": \"in\", \"value\": [\"Sociaal domein\"] }`. A record that cannot be read is absent, so its condition fails safe. Maps to CnDetailPage `visibilityContext`.",
|
|
4467
|
+
"additionalProperties": {
|
|
4468
|
+
"type": "object",
|
|
4469
|
+
"required": ["field", "register", "schema"],
|
|
4470
|
+
"additionalProperties": false,
|
|
4471
|
+
"properties": {
|
|
4472
|
+
"field": { "type": "string", "minLength": 1, "description": "Dot-path on the loaded record to the id of the record to read." },
|
|
4473
|
+
"register": { "type": "string", "minLength": 1, "description": "Register slug or id of the record to read." },
|
|
4474
|
+
"schema": { "type": "string", "minLength": 1, "description": "Schema slug or id of the record to read." }
|
|
4475
|
+
}
|
|
4476
|
+
}
|
|
4477
|
+
},
|
|
4352
4478
|
"mapConfig": {
|
|
4353
4479
|
"type": "object",
|
|
4354
4480
|
"additionalProperties": true,
|
|
@@ -4510,6 +4636,11 @@
|
|
|
4510
4636
|
}
|
|
4511
4637
|
}
|
|
4512
4638
|
},
|
|
4639
|
+
{
|
|
4640
|
+
"description": "screens-board-reference-columns (2.97.0): config.board on an index page, the block CnPageRenderer hands to CnIndexPage's `board` prop, is typed by boardView like pages[].board.",
|
|
4641
|
+
"if": { "properties": { "type": { "const": "index" } }, "required": ["type"] },
|
|
4642
|
+
"then": { "properties": { "config": { "properties": { "board": { "$ref": "#/$defs/boardView" } } } } }
|
|
4643
|
+
},
|
|
4513
4644
|
{
|
|
4514
4645
|
"description": "index-calendar-view-mode: config.calendar on an index page is typed by calendarView.",
|
|
4515
4646
|
"if": { "properties": { "type": { "const": "index" } }, "required": ["type"] },
|
|
@@ -707,6 +707,8 @@ const baseActions = {
|
|
|
707
707
|
* @param {object} [params] Query parameters (_limit, _page, _search, _order, filters)
|
|
708
708
|
* @param {object} [options] Call options
|
|
709
709
|
* @param {{error?: object|null}} [options.outcome] Opt-in sink for THIS call's own result: `error` is set to the error it records on `errors[type]`, or `null` on success. Unlike `errors[type]`, which every call for the type shares, a later call cannot overwrite it.
|
|
710
|
+
* @param {boolean} [options.append] Add the fetched page after the rows already stored instead of replacing them. Dropped when the stored rows are no longer the page before it, or when `options.isCurrent()` returns false (the query changed meanwhile).
|
|
711
|
+
* @param {() => boolean} [options.isCurrent] With `append`: asked once the page arrives; false drops it.
|
|
710
712
|
* @return {Promise<Array>} The fetched collection (also stored in state)
|
|
711
713
|
*/
|
|
712
714
|
async fetchCollection(type, params = {}, options = {}) {
|
|
@@ -750,14 +752,26 @@ const baseActions = {
|
|
|
750
752
|
|
|
751
753
|
const data = await response.json()
|
|
752
754
|
const results = data.results || data
|
|
755
|
+
const append = options?.append === true
|
|
753
756
|
|
|
754
|
-
|
|
757
|
+
const stale = append && (
|
|
758
|
+
Number(this.pagination[type]?.page || 1) !== Number(params._page || 1) - 1
|
|
759
|
+
|| (typeof options.isCurrent === 'function' && !options.isCurrent())
|
|
760
|
+
)
|
|
761
|
+
if (stale) {
|
|
762
|
+
return []
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
this.collections = {
|
|
766
|
+
...this.collections,
|
|
767
|
+
[type]: append ? [...(this.collections[type] || []), ...results] : results,
|
|
768
|
+
}
|
|
755
769
|
// Why a personal lens came back empty, if it says (openregister#4514).
|
|
756
770
|
this.lenses = { ...this.lenses, [type]: readLensReports(data) }
|
|
757
771
|
this.pagination = {
|
|
758
772
|
...this.pagination,
|
|
759
773
|
[type]: {
|
|
760
|
-
total: data.total || results.length,
|
|
774
|
+
total: data.total || (append ? this.collections[type].length : results.length),
|
|
761
775
|
page: data.page || 1,
|
|
762
776
|
pages: data.pages || 1,
|
|
763
777
|
limit: params._limit || 20,
|
|
@@ -128,6 +128,10 @@ function labelFor(field, value) {
|
|
|
128
128
|
* @param {string} options.statusField - The property name on a row.
|
|
129
129
|
* @param {string} [options.offBoardLabel] - What to call the off-board column.
|
|
130
130
|
* @param {string} [options.colorField] - The key on a state that holds its dot colour.
|
|
131
|
+
* @param {?((row: object) => string)} [options.keyOf] - The column a row
|
|
132
|
+
* belongs in, for a field whose raw value is not the column key (a
|
|
133
|
+
* reference whose rows are merged by name). Absent, the row's
|
|
134
|
+
* `statusField` value is the key.
|
|
131
135
|
*
|
|
132
136
|
* @return {{columns: Array<object>, usable: boolean}} The columns, each
|
|
133
137
|
* `{ key, label, cards, count }`, and whether the field could back a board.
|
|
@@ -138,6 +142,7 @@ export function buildBoardColumns({
|
|
|
138
142
|
statusField = '',
|
|
139
143
|
offBoardLabel = 'Elsewhere',
|
|
140
144
|
colorField = '',
|
|
145
|
+
keyOf = null,
|
|
141
146
|
} = {}) {
|
|
142
147
|
const { stages, usable } = stagesOf(field, colorField)
|
|
143
148
|
if (usable === false) {
|
|
@@ -156,7 +161,9 @@ export function buildBoardColumns({
|
|
|
156
161
|
|
|
157
162
|
const offBoard = { key: OFF_BOARD_KEY, label: offBoardLabel, cards: [], count: 0 }
|
|
158
163
|
for (const row of rows) {
|
|
159
|
-
const value =
|
|
164
|
+
const value = typeof keyOf === 'function'
|
|
165
|
+
? String(keyOf(row) ?? '')
|
|
166
|
+
: String(row?.[statusField] ?? '')
|
|
160
167
|
const column = byKey.get(value) || offBoard
|
|
161
168
|
column.cards.push(row)
|
|
162
169
|
column.count += 1
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* Board columns from a REFERENCED field (screens-board-reference-columns).
|
|
6
|
+
*
|
|
7
|
+
* A status that is an enum or a lifecycle list carries its stages on the
|
|
8
|
+
* field, and `boardColumns.js` reads them there. A status that is a reference
|
|
9
|
+
* to another object (a case's `status` pointing at a statusType row) carries
|
|
10
|
+
* nothing on the field: the stages are the rows of the referenced schema. This
|
|
11
|
+
* module turns those rows into the same `states` list `stagesOf()` reads, plus
|
|
12
|
+
* the map from a row's raw value (the referenced id) to its column.
|
|
13
|
+
*
|
|
14
|
+
* 🔴 THE COLUMNS ARE STILL THE SCHEMA'S, NOT THE DATA'S. They are the
|
|
15
|
+
* referenced rows, fetched, not the values present on the loaded cards, so an
|
|
16
|
+
* empty stage is still a column to drop a card into. The one exception is a
|
|
17
|
+
* field with no referenced schema at all (a Nextcloud user id): there the
|
|
18
|
+
* values present are all there is, and a "nobody" column is always added so
|
|
19
|
+
* work without an owner stays visible and can be dropped back to nobody.
|
|
20
|
+
*
|
|
21
|
+
* 🔴 MERGED COLUMNS RESOLVE PER CARD. Referenced rows are often per parent
|
|
22
|
+
* (every case type has its own "In behandeling"). Merged by name they make one
|
|
23
|
+
* column, and a drop must then write the row of THAT card's parent. A column
|
|
24
|
+
* with no row for the card's parent is not a place the card can go, and the
|
|
25
|
+
* drop is refused rather than written to some other parent's row.
|
|
26
|
+
*
|
|
27
|
+
* Pure apart from `fetchReferenceObjects`: no store, no Vue.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { buildHeaders, buildQueryString, prefixUrl } from './headers.js'
|
|
31
|
+
import { readPath } from './readPath.js'
|
|
32
|
+
import { schemaRefSlug } from './schemaRefSlug.js'
|
|
33
|
+
|
|
34
|
+
/** The column of cards with no value, for a `values` column field. */
|
|
35
|
+
export const NO_VALUE_KEY = '__none__'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The id a reference value carries: a string as is, an object by its `id`,
|
|
39
|
+
* `uuid` or `@self.id`.
|
|
40
|
+
*
|
|
41
|
+
* @param {unknown} value A reference as a row holds it.
|
|
42
|
+
* @return {string} The id, '' when there is none.
|
|
43
|
+
*/
|
|
44
|
+
export function refIdOf(value) {
|
|
45
|
+
if (value === null || value === undefined) {
|
|
46
|
+
return ''
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === 'object') {
|
|
49
|
+
const id = value.id ?? value.uuid ?? value['@self']?.id ?? value.value ?? ''
|
|
50
|
+
return id === null || id === undefined ? '' : String(id)
|
|
51
|
+
}
|
|
52
|
+
return String(value)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A value as text: a string or number as is, an object by its name.
|
|
57
|
+
*
|
|
58
|
+
* @param {unknown} value The raw value.
|
|
59
|
+
* @return {string} The text.
|
|
60
|
+
*/
|
|
61
|
+
function textOf(value) {
|
|
62
|
+
if (value === null || value === undefined) {
|
|
63
|
+
return ''
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === 'object') {
|
|
66
|
+
return String(value.displayName ?? value.name ?? value.title ?? value.label ?? '').trim()
|
|
67
|
+
}
|
|
68
|
+
return String(value).trim()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The column fields a board offers, normalised. Without `columnFields` the
|
|
73
|
+
* board has one, its `statusField`, read from the schema as before.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} board The board configuration (manifest `config.board`).
|
|
76
|
+
* @return {Array<{field: string, label: string, source: (object|null), kind: string, readOnly: boolean, moveHandler: string, moveTargetsHandler: string}>} The fields.
|
|
77
|
+
* @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-a-board-can-switch-the-field-its-columns-come-from
|
|
78
|
+
*/
|
|
79
|
+
export function columnFieldsOf(board) {
|
|
80
|
+
const declared = Array.isArray(board?.columnFields) ? board.columnFields : []
|
|
81
|
+
return declared
|
|
82
|
+
.map((entry) => (typeof entry === 'string' ? { field: entry } : entry))
|
|
83
|
+
.filter((entry) => entry && typeof entry.field === 'string' && entry.field !== '')
|
|
84
|
+
.map((entry) => ({
|
|
85
|
+
field: entry.field,
|
|
86
|
+
label: typeof entry.label === 'string' && entry.label !== '' ? entry.label : entry.field,
|
|
87
|
+
source: entry.source && typeof entry.source === 'object' ? entry.source : null,
|
|
88
|
+
kind: entry.kind === 'values' ? 'values' : (entry.source ? 'reference' : 'schema'),
|
|
89
|
+
readOnly: entry.readOnly === true,
|
|
90
|
+
moveHandler: typeof entry.moveHandler === 'string' ? entry.moveHandler : '',
|
|
91
|
+
moveTargetsHandler: typeof entry.moveTargetsHandler === 'string' ? entry.moveTargetsHandler : '',
|
|
92
|
+
valueLabels: entry.valueLabels && typeof entry.valueLabels === 'object' ? entry.valueLabels : null,
|
|
93
|
+
emptyLabel: typeof entry.emptyLabel === 'string' ? entry.emptyLabel : '',
|
|
94
|
+
}))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The register and schema a reference column reads its rows from: the
|
|
99
|
+
* source's own, else the page's register and the property's `$ref`.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} source The column field's `source`.
|
|
102
|
+
* @param {object} options The context.
|
|
103
|
+
* @param {string} options.register The page's register.
|
|
104
|
+
* @param {object} [options.property] The field's schema property.
|
|
105
|
+
* @return {{register: string, schema: string}} Both, '' when unknown.
|
|
106
|
+
*/
|
|
107
|
+
export function referenceTargetOf(source, { register = '', property = null } = {}) {
|
|
108
|
+
const schema = source?.schema || schemaRefSlug(property?.$ref ?? property?.items?.$ref ?? '')
|
|
109
|
+
return {
|
|
110
|
+
register: String(source?.register || register || ''),
|
|
111
|
+
schema: schema === '' || schema === null || schema === undefined ? '' : String(schema),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The stages of a referenced field: the referenced rows, narrowed to one
|
|
117
|
+
* parent when asked, ordered, and merged by a field when asked.
|
|
118
|
+
*
|
|
119
|
+
* @param {object} options The call.
|
|
120
|
+
* @param {Array<object>} options.refs The referenced rows.
|
|
121
|
+
* @param {string} [options.labelField] The row field that names a column.
|
|
122
|
+
* @param {string} [options.orderField] The numeric row field the columns are ordered by.
|
|
123
|
+
* @param {string} [options.colorField] The row field that holds the dot colour.
|
|
124
|
+
* @param {string} [options.mergeBy] The row field whose equal values make one column.
|
|
125
|
+
* @param {{field: string, rowField: string}|null} [options.narrowBy] The parent link.
|
|
126
|
+
* @param {string} [options.narrowValue] The parent the board is narrowed to, '' for all.
|
|
127
|
+
* @return {{states: Array<{key: string, label: string, color: string}>, keyByValue: Map<string, string>, refsByKey: Map<string, Array<object>>}} The stages.
|
|
128
|
+
* @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-columns-can-be-the-rows-a-field-references
|
|
129
|
+
*/
|
|
130
|
+
export function buildReferenceStages({
|
|
131
|
+
refs = [],
|
|
132
|
+
labelField = '',
|
|
133
|
+
orderField = '',
|
|
134
|
+
colorField = '',
|
|
135
|
+
mergeBy = '',
|
|
136
|
+
narrowBy = null,
|
|
137
|
+
narrowValue = '',
|
|
138
|
+
} = {}) {
|
|
139
|
+
const list = (Array.isArray(refs) ? refs : []).filter((ref) => ref && typeof ref === 'object' && refIdOf(ref) !== '')
|
|
140
|
+
const narrowed = narrowBy && narrowBy.field && narrowValue !== ''
|
|
141
|
+
? list.filter((ref) => refIdOf(readPath(ref, narrowBy.field)) === String(narrowValue))
|
|
142
|
+
: list
|
|
143
|
+
|
|
144
|
+
const orderOf = (ref) => {
|
|
145
|
+
const n = Number(orderField ? readPath(ref, orderField) : NaN)
|
|
146
|
+
return Number.isFinite(n) ? n : Number.MAX_SAFE_INTEGER
|
|
147
|
+
}
|
|
148
|
+
const ordered = narrowed
|
|
149
|
+
.map((ref, index) => ({ ref, index }))
|
|
150
|
+
.sort((a, b) => (orderOf(a.ref) - orderOf(b.ref)) || (a.index - b.index))
|
|
151
|
+
.map((entry) => entry.ref)
|
|
152
|
+
|
|
153
|
+
const states = []
|
|
154
|
+
const keyByValue = new Map()
|
|
155
|
+
const refsByKey = new Map()
|
|
156
|
+
for (const ref of ordered) {
|
|
157
|
+
const id = refIdOf(ref)
|
|
158
|
+
const label = textOf(labelField ? readPath(ref, labelField) : (ref.name ?? ref.title)) || id
|
|
159
|
+
const key = mergeBy ? (textOf(readPath(ref, mergeBy)) || id) : id
|
|
160
|
+
keyByValue.set(id, key)
|
|
161
|
+
if (!refsByKey.has(key)) {
|
|
162
|
+
refsByKey.set(key, [])
|
|
163
|
+
const color = colorField ? readPath(ref, colorField) : ref.color
|
|
164
|
+
states.push({ key, label, color: typeof color === 'string' ? color : '' })
|
|
165
|
+
}
|
|
166
|
+
refsByKey.get(key).push(ref)
|
|
167
|
+
}
|
|
168
|
+
return { states, keyByValue, refsByKey }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The stages of a field with no referenced schema (a user id): the values
|
|
173
|
+
* present on the rows, by label, then one column for no value.
|
|
174
|
+
*
|
|
175
|
+
* @param {object} options The call.
|
|
176
|
+
* @param {Array<object>} options.rows The rows the list holds.
|
|
177
|
+
* @param {string} options.field The row field.
|
|
178
|
+
* @param {object} [options.valueLabels] Raw value to the label to show.
|
|
179
|
+
* @param {string} [options.emptyLabel] The no-value column's label.
|
|
180
|
+
* @return {{states: Array<{key: string, label: string, color: string}>, keyByValue: Map<string, string>, refsByKey: Map<string, Array<object>>}} The stages.
|
|
181
|
+
* @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-a-field-without-a-referenced-schema-takes-its-values
|
|
182
|
+
*/
|
|
183
|
+
export function buildValueStages({ rows = [], field = '', valueLabels = null, emptyLabel = 'Nobody' } = {}) {
|
|
184
|
+
const labels = valueLabels && typeof valueLabels === 'object' ? valueLabels : {}
|
|
185
|
+
const seen = new Map()
|
|
186
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
187
|
+
const raw = readPath(row, field)
|
|
188
|
+
const id = refIdOf(raw)
|
|
189
|
+
if (id !== '' && !seen.has(id)) {
|
|
190
|
+
seen.set(id, typeof labels[id] === 'string' ? labels[id] : (textOf(raw) || id))
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const states = [...seen.entries()]
|
|
194
|
+
.sort((a, b) => a[1].localeCompare(b[1]))
|
|
195
|
+
.map(([key, label]) => ({ key, label, color: '' }))
|
|
196
|
+
states.push({ key: NO_VALUE_KEY, label: emptyLabel, color: '' })
|
|
197
|
+
const keyByValue = new Map(states.map((state) => [state.key, state.key]))
|
|
198
|
+
keyByValue.set('', NO_VALUE_KEY)
|
|
199
|
+
return { states, keyByValue, refsByKey: new Map() }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The column a row sits in, under a value-to-column map.
|
|
204
|
+
*
|
|
205
|
+
* @param {object} row The row.
|
|
206
|
+
* @param {string} field The row field.
|
|
207
|
+
* @param {Map<string, string>} keyByValue The value-to-column map.
|
|
208
|
+
* @return {string} The column key, or the raw value when the map has none
|
|
209
|
+
* (which lands the card in the off-board column).
|
|
210
|
+
*/
|
|
211
|
+
export function columnKeyOfRow(row, field, keyByValue) {
|
|
212
|
+
const id = refIdOf(readPath(row, field))
|
|
213
|
+
return keyByValue.has(id) ? keyByValue.get(id) : id
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* What a drop on a column writes for one card: the referenced row of that
|
|
218
|
+
* column which belongs to the card's parent, or null when the column has none
|
|
219
|
+
* (the card cannot go there).
|
|
220
|
+
*
|
|
221
|
+
* @param {object} options The call.
|
|
222
|
+
* @param {object} options.card The card.
|
|
223
|
+
* @param {string} options.toKey The column it was dropped on.
|
|
224
|
+
* @param {string} options.kind `reference` or `values`.
|
|
225
|
+
* @param {Map<string, Array<object>>} [options.refsByKey] The column's rows.
|
|
226
|
+
* @param {{field: string, rowField: string}|null} [options.narrowBy] The parent link.
|
|
227
|
+
* @return {{value: (string|null), target: (object|null)}|null} The value to write and its row, or null.
|
|
228
|
+
* @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-a-drop-writes-the-reference
|
|
229
|
+
*/
|
|
230
|
+
export function resolveDropValue({ card, toKey, kind, refsByKey = new Map(), narrowBy = null } = {}) {
|
|
231
|
+
if (kind === 'values') {
|
|
232
|
+
return { value: toKey === NO_VALUE_KEY ? null : String(toKey), target: null }
|
|
233
|
+
}
|
|
234
|
+
const candidates = refsByKey.get(String(toKey)) || []
|
|
235
|
+
if (candidates.length === 0) {
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
if (narrowBy && narrowBy.field && narrowBy.rowField) {
|
|
239
|
+
const parent = refIdOf(readPath(card, narrowBy.rowField))
|
|
240
|
+
const match = candidates.find((ref) => refIdOf(readPath(ref, narrowBy.field)) === parent)
|
|
241
|
+
return match ? { value: refIdOf(match), target: match } : null
|
|
242
|
+
}
|
|
243
|
+
return { value: refIdOf(candidates[0]), target: candidates[0] }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Fetch the rows a reference column is built from, one list request.
|
|
248
|
+
*
|
|
249
|
+
* @param {object} options The call.
|
|
250
|
+
* @param {string} options.register The register slug.
|
|
251
|
+
* @param {string} options.schema The schema slug.
|
|
252
|
+
* @param {object} [options.filter] Equality filters, e.g. `{ isFinal: false }`.
|
|
253
|
+
* @param {number} [options.limit] The page size, 500 by default.
|
|
254
|
+
* @return {Promise<Array<object>>} The rows.
|
|
255
|
+
* @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-columns-can-be-the-rows-a-field-references
|
|
256
|
+
*/
|
|
257
|
+
export async function fetchReferenceObjects({ register, schema, filter = {}, limit = 500 } = {}) {
|
|
258
|
+
if (!register || !schema) {
|
|
259
|
+
return []
|
|
260
|
+
}
|
|
261
|
+
const params = { _limit: limit }
|
|
262
|
+
for (const [key, value] of Object.entries(filter || {})) {
|
|
263
|
+
if (value !== undefined && value !== null && typeof value !== 'object') {
|
|
264
|
+
params[key] = typeof value === 'boolean' ? String(value) : value
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const url = prefixUrl(`/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}${buildQueryString(params)}`)
|
|
268
|
+
const response = await fetch(url, { headers: buildHeaders() })
|
|
269
|
+
if (!response.ok) {
|
|
270
|
+
throw new Error(`list returned ${response.status}`)
|
|
271
|
+
}
|
|
272
|
+
const data = await response.json()
|
|
273
|
+
return Array.isArray(data?.results) ? data.results : (Array.isArray(data) ? data : [])
|
|
274
|
+
}
|
|
@@ -137,7 +137,10 @@ export function mergeMenuItems(target, incoming) {
|
|
|
137
137
|
target.push(copy)
|
|
138
138
|
return
|
|
139
139
|
}
|
|
140
|
-
|
|
140
|
+
// `query` and `params` belong to `route`: an entry that takes its route
|
|
141
|
+
// from here without them opens the list unfiltered (a "board" entry to
|
|
142
|
+
// `Cases` with `{ _view: 'board' }` opened the table).
|
|
143
|
+
for (const key of ['label', 'icon', 'route', 'query', 'params', 'order', 'section', 'featureFlag', 'permission', 'visibleIf', 'href', 'action']) {
|
|
141
144
|
if (existing[key] === undefined && item[key] !== undefined) {
|
|
142
145
|
existing[key] = item[key]
|
|
143
146
|
}
|
|
@@ -195,6 +198,7 @@ export function applyMenuRelocations(menu, relocations) {
|
|
|
195
198
|
if (!relocations || typeof relocations !== 'object') {
|
|
196
199
|
return menu
|
|
197
200
|
}
|
|
201
|
+
const heldEntries = captionsHoldingEntries(menu)
|
|
198
202
|
for (let pass = 0; pass < 5; pass++) {
|
|
199
203
|
const moves = []
|
|
200
204
|
for (let i = menu.length - 1; i >= 0; i--) {
|
|
@@ -240,9 +244,42 @@ export function applyMenuRelocations(menu, relocations) {
|
|
|
240
244
|
}
|
|
241
245
|
})
|
|
242
246
|
}
|
|
243
|
-
// Drop empty group shells left behind by relocations.
|
|
244
|
-
|
|
247
|
+
// Drop empty group shells left behind by relocations. A caption (a
|
|
248
|
+
// section heading) has no route and no children and is kept, unless the
|
|
249
|
+
// relocations emptied it: every entry under it moved away.
|
|
250
|
+
const kept = menu.filter((m) => m.type === 'caption' || m.route || m.href || m.action
|
|
245
251
|
|| (Array.isArray(m.children) && m.children.length > 0))
|
|
252
|
+
const holdingNow = captionsHoldingEntries(kept)
|
|
253
|
+
return kept.filter((m) => m.type !== 'caption' || !heldEntries.has(m) || holdingNow.has(m))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The captions (`type: "caption"`) that have at least one entry under them:
|
|
258
|
+
* a non-caption entry of the same section before the next caption.
|
|
259
|
+
*
|
|
260
|
+
* @param {Array<object>} menu The top-level menu.
|
|
261
|
+
* @return {Set<object>} The captions holding an entry.
|
|
262
|
+
*/
|
|
263
|
+
function captionsHoldingEntries(menu) {
|
|
264
|
+
const holding = new Set()
|
|
265
|
+
menu.forEach((entry, index) => {
|
|
266
|
+
if (entry.type !== 'caption') {
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
const section = entry.section ?? 'main'
|
|
270
|
+
for (let i = index + 1; i < menu.length; i++) {
|
|
271
|
+
const next = menu[i]
|
|
272
|
+
if ((next.section ?? 'main') !== section) {
|
|
273
|
+
continue
|
|
274
|
+
}
|
|
275
|
+
if (next.type === 'caption') {
|
|
276
|
+
break
|
|
277
|
+
}
|
|
278
|
+
holding.add(entry)
|
|
279
|
+
break
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
return holding
|
|
246
283
|
}
|
|
247
284
|
|
|
248
285
|
/**
|