@conduction/nextcloud-vue 2.78.0 → 2.80.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/bundled-packages.json +0 -2
- package/dist/esm/bundled-packages.json +0 -2
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +3 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +32 -3
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +115 -104
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +12 -2
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +39 -3
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +158 -0
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +157 -96
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +139 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +140 -92
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +77 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +5 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +62 -4
- 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.vue2.js +85 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +43 -4
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +11 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +736 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +238 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/index.js +30 -0
- package/dist/esm/components/CnFactsWidget/index.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +10 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +551 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +183 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/index.js +29 -0
- package/dist/esm/components/CnFieldListWidget/index.js.map +1 -0
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +173 -5
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +181 -99
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js +29 -11
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/openRegisterSource.js +252 -0
- package/dist/esm/components/CnFilesBrowser/openRegisterSource.js.map +1 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +66 -18
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +4 -4
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +18 -3
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +330 -74
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +34 -15
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +10 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js.map +1 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +99 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +48 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +100 -3
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -2
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +29 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +3 -2
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +10 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +387 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +169 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/index.js +31 -0
- package/dist/esm/components/CnRecordListWidget/index.js.map +1 -0
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +10 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +351 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +71 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/index.js +35 -0
- package/dist/esm/components/CnSectionsWidget/index.js.map +1 -0
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +81 -2
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +1 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +152 -8
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +84 -23
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +106 -2
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +105 -53
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
- package/dist/esm/composables/useAppManifest.js +8 -3
- package/dist/esm/composables/useAppManifest.js.map +1 -1
- package/dist/esm/composables/useFormDraft.js +140 -9
- package/dist/esm/composables/useFormDraft.js.map +1 -1
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +28 -20
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js +9 -0
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/field-inspection.js +21 -0
- package/dist/esm/integrations/builtin/field-inspection.js.map +1 -1
- package/dist/esm/integrations/offline/checklistSubmission.js +141 -0
- package/dist/esm/integrations/offline/checklistSubmission.js.map +1 -0
- package/dist/esm/integrations/offline/fieldCollectionHelpers.js +70 -5
- package/dist/esm/integrations/offline/fieldCollectionHelpers.js.map +1 -1
- package/dist/esm/integrations/offline/index.js +2 -1
- package/dist/esm/integrations/offline/index.js.map +1 -1
- package/dist/esm/integrations/offline/offlineDb.js +6 -1
- package/dist/esm/integrations/offline/offlineDb.js.map +1 -1
- package/dist/esm/integrations/offline/syncReplayService.js +8 -2
- package/dist/esm/integrations/offline/syncReplayService.js.map +1 -1
- package/dist/esm/l10n/en.json.js +72 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +120 -2
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +1314 -74
- package/dist/esm/utils/boardDate.js +193 -0
- package/dist/esm/utils/boardDate.js.map +1 -0
- package/dist/esm/utils/detailActionModel.js +60 -3
- package/dist/esm/utils/detailActionModel.js.map +1 -1
- package/dist/esm/utils/dynamicProperties.js +5 -1
- package/dist/esm/utils/dynamicProperties.js.map +1 -1
- package/dist/esm/utils/fillTemplate.js +114 -0
- package/dist/esm/utils/fillTemplate.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +4 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/queryPresets.js +96 -0
- package/dist/esm/utils/queryPresets.js.map +1 -0
- package/dist/esm/utils/resolveFilterTokens.js +11 -0
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/schema.js +21 -0
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +3 -3
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +57 -6
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/widgetDispatch.js +7 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +14278 -8169
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1314 -74
- package/l10n/en.json +72 -1
- package/l10n/nl.json +120 -2
- package/package.json +1 -1
- package/scripts/build-validators.js +26 -0
- package/src/components/CnActionButtons/CnActionButtons.vue +2 -0
- package/src/components/CnActionsBar/CnActionsBar.md +4 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +83 -42
- package/src/components/CnAppRoot/CnAppRoot.vue +12 -2
- package/src/components/CnBannerWidget/CnBannerWidget.vue +39 -3
- package/src/components/CnCellRenderer/CnCellRenderer.vue +306 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +219 -4
- package/src/components/CnDataTable/CnDataTable.md +2 -0
- package/src/components/CnDataTable/CnDataTable.vue +82 -2
- package/src/components/CnDetailPage/CnDetailPage.md +1 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +63 -3
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +127 -1
- package/src/components/CnFactsWidget/CnFactsWidget.vue +1050 -0
- package/src/components/CnFactsWidget/index.js +29 -0
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +746 -0
- package/src/components/CnFieldListWidget/index.js +28 -0
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +232 -11
- package/src/components/CnFilesBrowser/filesBrowser.js +43 -2
- package/src/components/CnFilesBrowser/index.js +1 -0
- package/src/components/CnFilesBrowser/openRegisterSource.js +248 -0
- package/src/components/CnFormDialog/CnFormDialog.md +9 -3
- package/src/components/CnFormDialog/CnFormDialog.vue +70 -22
- package/src/components/CnFormPage/CnFormPage.vue +18 -3
- package/src/components/CnIndexPage/CnIndexPage.vue +281 -9
- package/src/components/CnIndexPage/CnIndexPageWidgets.vue +169 -0
- package/src/components/CnObjectSidebar/CnFilesTab.vue +103 -4
- package/src/components/CnPageRenderer/CnPageRenderer.vue +29 -1
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +3 -2
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +603 -0
- package/src/components/CnRecordListWidget/index.js +30 -0
- package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +1 -1
- package/src/components/CnSectionsWidget/CnSectionsWidget.vue +489 -0
- package/src/components/CnSectionsWidget/index.js +34 -0
- package/src/components/CnStagesWidget/CnStagesWidget.vue +81 -2
- package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
- package/src/components/CnTabsWidget/CnTabsWidget.vue +214 -9
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
- package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +188 -4
- package/src/components/index.js +5 -1
- package/src/composables/__tests__/useFormDraft.spec.js +69 -0
- package/src/composables/useAppManifest.js +8 -3
- package/src/composables/useFormDraft.js +139 -8
- package/src/css/form-field.css +14 -0
- package/src/css/index-page.css +71 -0
- package/src/css/kpi-card.css +32 -0
- package/src/css/look-board-detail.css +87 -0
- package/src/css/look-board-index.css +143 -14
- package/src/css/look-board.css +114 -2
- package/src/css/table.css +56 -0
- package/src/index.js +8 -0
- package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +35 -20
- package/src/integrations/builtin/field-inspection.js +21 -0
- package/src/integrations/offline/checklistSubmission.js +137 -0
- package/src/integrations/offline/fieldCollectionHelpers.js +69 -4
- package/src/integrations/offline/index.js +3 -0
- package/src/integrations/offline/offlineDb.js +6 -1
- package/src/integrations/offline/syncReplayService.js +8 -2
- package/src/schemas/app-manifest-v2.schema.json +149 -5
- package/src/utils/boardDate.js +189 -0
- package/src/utils/detailActionModel.js +59 -2
- package/src/utils/dynamicProperties.js +5 -1
- package/src/utils/fillTemplate.js +110 -0
- package/src/utils/libraryWidgetKeys.js +4 -0
- package/src/utils/queryPresets.js +93 -0
- package/src/utils/resolveFilterTokens.js +11 -0
- package/src/utils/schema.js +21 -0
- package/src/utils/sentinelTokens.js +3 -3
- package/src/utils/validateManifestV2.compiled.js +57 -2
- package/src/utils/widgetDispatch.js +7 -1
- package/dist/esm/_virtual/equal.js +0 -4
- package/dist/esm/_virtual/equal.js.map +0 -1
- package/dist/esm/_virtual/ucs2length.js +0 -4
- package/dist/esm/_virtual/ucs2length.js.map +0 -1
- package/dist/esm/node_modules/ajv/dist/runtime/equal.js +0 -9
- package/dist/esm/node_modules/ajv/dist/runtime/equal.js.map +0 -1
- package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js +0 -25
- package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js.map +0 -1
- package/dist/esm/node_modules/fast-deep-equal/index.js +0 -47
- package/dist/esm/node_modules/fast-deep-equal/index.js.map +0 -1
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<div
|
|
8
|
+
class="cn-sections-widget"
|
|
9
|
+
:class="{
|
|
10
|
+
'cn-sections-widget--cards': cards,
|
|
11
|
+
'cn-sections-widget--two-column': twoColumn,
|
|
12
|
+
}"
|
|
13
|
+
data-testid="cn-sections-widget">
|
|
14
|
+
<div
|
|
15
|
+
v-for="column in columns"
|
|
16
|
+
:key="column.id"
|
|
17
|
+
class="cn-sections-widget__column"
|
|
18
|
+
:class="`cn-sections-widget__column--${column.id}`">
|
|
19
|
+
<section
|
|
20
|
+
v-for="section in column.sections"
|
|
21
|
+
:key="section.key"
|
|
22
|
+
class="cn-sections-widget__section"
|
|
23
|
+
:class="{ 'cn-sections-widget__section--empty': !isShown(section.key) }"
|
|
24
|
+
:aria-labelledby="section.title && isShown(section.key) ? headingId(section.key) : undefined"
|
|
25
|
+
:data-testid="`cn-section-${section.key}`">
|
|
26
|
+
<!-- The heading waits for content: a widget type that does not
|
|
27
|
+
resolve renders nothing, and a heading over nothing is a
|
|
28
|
+
label on a void. -->
|
|
29
|
+
<h3
|
|
30
|
+
v-if="section.title && isShown(section.key)"
|
|
31
|
+
:id="headingId(section.key)"
|
|
32
|
+
class="cn-sections-widget__heading">
|
|
33
|
+
{{ section.title }}
|
|
34
|
+
</h3>
|
|
35
|
+
<p
|
|
36
|
+
v-if="section.description && isShown(section.key)"
|
|
37
|
+
class="cn-sections-widget__description">
|
|
38
|
+
{{ section.description }}
|
|
39
|
+
</p>
|
|
40
|
+
<!-- The host always mounts, also while the section reads as
|
|
41
|
+
empty: a widget that has not mounted cannot fetch, so it
|
|
42
|
+
could never gain the content that would show it. -->
|
|
43
|
+
<div :ref="(el) => setHostRef(section.key, el)" class="cn-sections-widget__body">
|
|
44
|
+
<CnDetailWidgetHost
|
|
45
|
+
:widget="section.widget"
|
|
46
|
+
chrome="bare"
|
|
47
|
+
:objectId="objectId"
|
|
48
|
+
:object="objectData"
|
|
49
|
+
:objectType="objectType"
|
|
50
|
+
:schemaObject="schemaObject"
|
|
51
|
+
:register="register"
|
|
52
|
+
:schema="schema"
|
|
53
|
+
:store="store"
|
|
54
|
+
:surface="surface"
|
|
55
|
+
:integrationContext="integrationContext"
|
|
56
|
+
:cnRegistry="cnRegistry"
|
|
57
|
+
:availableWidgets="availableWidgets" />
|
|
58
|
+
</div>
|
|
59
|
+
</section>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
|
|
64
|
+
<script>
|
|
65
|
+
import CnDetailWidgetHost from '../CnDetailWidgetHost/CnDetailWidgetHost.vue'
|
|
66
|
+
|
|
67
|
+
/** Counter for unique heading ids across instances. */
|
|
68
|
+
let instanceCounter = 0
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* CnSectionsWidget — the `sections` widget: several widgets in one place,
|
|
72
|
+
* each under its own heading.
|
|
73
|
+
*
|
|
74
|
+
* A tab strip binds one widget per tab, and a tab often holds more than one
|
|
75
|
+
* thing: the documents list and the case folder (DqZaak), a portal's details
|
|
76
|
+
* form, house style and sign-in (PtPortalInstellingen). This widget stacks
|
|
77
|
+
* them as sections under headings with a rule between them (the default), or
|
|
78
|
+
* as one card per section (`cards: true`), optionally in a main and a side
|
|
79
|
+
* column (`column: "side"` on a section). A single column of separate titled
|
|
80
|
+
* cards in a tab is the tabs widget's own `widgetIds` stack; this widget is
|
|
81
|
+
* for headed sections, and for the two-column card grid a settings tab draws.
|
|
82
|
+
*
|
|
83
|
+
* ```json
|
|
84
|
+
* {
|
|
85
|
+
* "type": "sections",
|
|
86
|
+
* "content": {
|
|
87
|
+
* "sections": [
|
|
88
|
+
* { "title": "Documenten", "widgetId": "case-documents" },
|
|
89
|
+
* { "title": "Zaakmap", "widget": { "type": "files" } }
|
|
90
|
+
* ]
|
|
91
|
+
* }
|
|
92
|
+
* }
|
|
93
|
+
* ```
|
|
94
|
+
*
|
|
95
|
+
* A section names its widget inline (`widget`) or by id (`widgetId`, a
|
|
96
|
+
* sibling definition on the same surface). Children render through
|
|
97
|
+
* `CnDetailWidgetHost` in bare mode, the dispatch a tab panel uses, so a child
|
|
98
|
+
* behaves here as it does in a tab. A section whose widget draws nothing
|
|
99
|
+
* shows no heading and takes no space (`hideEmpty`, default true). Stacked
|
|
100
|
+
* children share one cell, so an object list in a section renders every
|
|
101
|
+
* fetched row (`content.fit: false`) unless its definition says otherwise.
|
|
102
|
+
*
|
|
103
|
+
* @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md
|
|
104
|
+
*/
|
|
105
|
+
export default {
|
|
106
|
+
name: 'CnSectionsWidget',
|
|
107
|
+
|
|
108
|
+
components: { CnDetailWidgetHost },
|
|
109
|
+
|
|
110
|
+
inject: {
|
|
111
|
+
cnTranslate: { default: () => (key) => key },
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
props: {
|
|
115
|
+
/**
|
|
116
|
+
* The widget configuration.
|
|
117
|
+
*
|
|
118
|
+
* - `sections` — `[{ key?, title?, description?, column?, widget? | widgetId? }]`, in order.
|
|
119
|
+
* `label` is accepted as an alias of `title`; `column` is `main` (default) or `side`.
|
|
120
|
+
* - `cards` — draw each section as its own card (default false: headings and a rule between sections).
|
|
121
|
+
* - `hideEmpty` — hide the heading and the space of a section whose widget drew nothing (default true).
|
|
122
|
+
*
|
|
123
|
+
* @type {{sections?: Array<object>, cards?: boolean, hideEmpty?: boolean}}
|
|
124
|
+
*/
|
|
125
|
+
content: {
|
|
126
|
+
type: Object,
|
|
127
|
+
default: () => ({}),
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/** Sibling widget definitions on the surface, for `widgetId` sections. */
|
|
131
|
+
availableWidgets: {
|
|
132
|
+
type: Array,
|
|
133
|
+
default: () => [],
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
/** The bound record's id. */
|
|
137
|
+
objectId: {
|
|
138
|
+
type: [String, Number],
|
|
139
|
+
default: '',
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
/** The loaded record, or null while it is still being fetched. */
|
|
143
|
+
objectData: {
|
|
144
|
+
type: Object,
|
|
145
|
+
default: null,
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/** The resolved object-type slug. */
|
|
149
|
+
objectType: {
|
|
150
|
+
type: String,
|
|
151
|
+
default: '',
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/** The resolved JSON Schema object, needed by a `data` child. */
|
|
155
|
+
schemaObject: {
|
|
156
|
+
type: Object,
|
|
157
|
+
default: null,
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
/** OpenRegister register slug of the surface. */
|
|
161
|
+
register: {
|
|
162
|
+
type: [String, Object],
|
|
163
|
+
default: '',
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/** OpenRegister schema slug of the surface. */
|
|
167
|
+
schema: {
|
|
168
|
+
type: [String, Object],
|
|
169
|
+
default: '',
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/** The effective object store. */
|
|
173
|
+
store: {
|
|
174
|
+
type: Object,
|
|
175
|
+
default: null,
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
/** Rendering surface forwarded to the children. */
|
|
179
|
+
surface: {
|
|
180
|
+
type: String,
|
|
181
|
+
default: 'detail-page',
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
/** Object context forwarded to integration children. */
|
|
185
|
+
integrationContext: {
|
|
186
|
+
type: Object,
|
|
187
|
+
default: null,
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/** The consumer's component registry, for custom child widget types. */
|
|
191
|
+
cnRegistry: {
|
|
192
|
+
type: Object,
|
|
193
|
+
default: () => ({}),
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
data() {
|
|
198
|
+
instanceCounter += 1
|
|
199
|
+
return {
|
|
200
|
+
uid: instanceCounter,
|
|
201
|
+
/**
|
|
202
|
+
* Section key to "has this section rendered anything".
|
|
203
|
+
*
|
|
204
|
+
* @type {Record<string, boolean>}
|
|
205
|
+
*/
|
|
206
|
+
filled: {},
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
computed: {
|
|
211
|
+
/**
|
|
212
|
+
* Whether each section is its own card.
|
|
213
|
+
*
|
|
214
|
+
* @return {boolean}
|
|
215
|
+
*/
|
|
216
|
+
cards() {
|
|
217
|
+
return this.content?.cards === true
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Whether an empty section hides.
|
|
222
|
+
*
|
|
223
|
+
* @return {boolean}
|
|
224
|
+
*/
|
|
225
|
+
hideEmpty() {
|
|
226
|
+
return this.content?.hideEmpty !== false
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* The sections to render: a stable key, a translated title, the column
|
|
231
|
+
* and the widget definition. A section whose widget cannot be found is
|
|
232
|
+
* dropped (with a development warning for an unknown id).
|
|
233
|
+
*
|
|
234
|
+
* @return {Array<{key: string, title: string, description: string, column: string, widget: object}>}
|
|
235
|
+
* @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md#requirement-a-sections-widget-stacks-configured-widgets-under-headings
|
|
236
|
+
*/
|
|
237
|
+
sections() {
|
|
238
|
+
const raw = Array.isArray(this.content?.sections) ? this.content.sections : []
|
|
239
|
+
const out = []
|
|
240
|
+
raw.forEach((entry, index) => {
|
|
241
|
+
if (!entry || typeof entry !== 'object') {
|
|
242
|
+
return
|
|
243
|
+
}
|
|
244
|
+
let widget = entry.widget && typeof entry.widget === 'object' ? entry.widget : null
|
|
245
|
+
if (!widget && typeof entry.widgetId === 'string' && entry.widgetId !== '') {
|
|
246
|
+
widget = this.availableWidgets.find((w) => w && w.id === entry.widgetId) || null
|
|
247
|
+
if (!widget) {
|
|
248
|
+
// eslint-disable-next-line no-console
|
|
249
|
+
console.warn(`[CnSectionsWidget] section widgetId "${entry.widgetId}" names no widget on this surface; section skipped.`)
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
if (!widget || !widget.type) {
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
out.push({
|
|
256
|
+
key: String(entry.key || widget.id || entry.widgetId || `${widget.type}-${index}`),
|
|
257
|
+
title: this.tr(entry.title || entry.label || ''),
|
|
258
|
+
description: this.tr(entry.description || ''),
|
|
259
|
+
column: entry.column === 'side' ? 'side' : 'main',
|
|
260
|
+
widget: this.shared(widget),
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
return out
|
|
264
|
+
},
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Whether any section sits in the side column.
|
|
268
|
+
*
|
|
269
|
+
* @return {boolean}
|
|
270
|
+
*/
|
|
271
|
+
twoColumn() {
|
|
272
|
+
return this.sections.some((section) => section.column === 'side')
|
|
273
|
+
},
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The columns to draw, each with its sections in order.
|
|
277
|
+
*
|
|
278
|
+
* @return {Array<{id: string, sections: Array<object>}>}
|
|
279
|
+
* @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md#requirement-a-sections-widget-can-draw-cards-in-a-main-and-a-side-column
|
|
280
|
+
*/
|
|
281
|
+
columns() {
|
|
282
|
+
if (!this.twoColumn) {
|
|
283
|
+
return [{ id: 'main', sections: this.sections }]
|
|
284
|
+
}
|
|
285
|
+
return [
|
|
286
|
+
{ id: 'main', sections: this.sections.filter((section) => section.column === 'main') },
|
|
287
|
+
{ id: 'side', sections: this.sections.filter((section) => section.column === 'side') },
|
|
288
|
+
]
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
beforeUnmount() {
|
|
293
|
+
this.stopObserving()
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
methods: {
|
|
297
|
+
/**
|
|
298
|
+
* Translate through the host app.
|
|
299
|
+
*
|
|
300
|
+
* @param {string} text The text.
|
|
301
|
+
* @return {string}
|
|
302
|
+
*/
|
|
303
|
+
tr(text) {
|
|
304
|
+
if (typeof text !== 'string' || text === '') {
|
|
305
|
+
return ''
|
|
306
|
+
}
|
|
307
|
+
return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
|
|
308
|
+
},
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The heading id of a section.
|
|
312
|
+
*
|
|
313
|
+
* @param {string} key The section key.
|
|
314
|
+
* @return {string}
|
|
315
|
+
*/
|
|
316
|
+
headingId(key) {
|
|
317
|
+
return `cn-sections-${this.uid}-${key}`.replace(/[^\w-]/g, '-')
|
|
318
|
+
},
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* A child definition with the cell budget off: stacked children share
|
|
322
|
+
* one cell, so an object list must not size itself to that cell.
|
|
323
|
+
*
|
|
324
|
+
* @param {object} widget The definition.
|
|
325
|
+
* @return {object}
|
|
326
|
+
*/
|
|
327
|
+
shared(widget) {
|
|
328
|
+
const content = widget.content || {}
|
|
329
|
+
if (Object.hasOwn(content, 'fit')) {
|
|
330
|
+
return widget
|
|
331
|
+
}
|
|
332
|
+
return { ...widget, content: { ...content, fit: false } }
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Whether a section shows its heading and takes space.
|
|
337
|
+
*
|
|
338
|
+
* @param {string} key The section key.
|
|
339
|
+
* @return {boolean}
|
|
340
|
+
* @spec openspec/changes/abstract-sections-widget/specs/sections-widget/spec.md#requirement-a-section-whose-widget-draws-nothing-stays-silent
|
|
341
|
+
*/
|
|
342
|
+
isShown(key) {
|
|
343
|
+
return !this.hideEmpty || this.filled[key] === true
|
|
344
|
+
},
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Watch one section's host for content, so its heading appears when
|
|
348
|
+
* the widget renders and stays away when it never does. A
|
|
349
|
+
* MutationObserver, because most widgets mount empty and fill after a
|
|
350
|
+
* fetch.
|
|
351
|
+
*
|
|
352
|
+
* @param {string} key The section key.
|
|
353
|
+
* @param {HTMLElement|null} el The host wrapper, or null on teardown.
|
|
354
|
+
* @return {void}
|
|
355
|
+
*/
|
|
356
|
+
setHostRef(key, el) {
|
|
357
|
+
this.observers = this.observers || {}
|
|
358
|
+
if (this.observers[key]) {
|
|
359
|
+
this.observers[key].disconnect()
|
|
360
|
+
delete this.observers[key]
|
|
361
|
+
}
|
|
362
|
+
if (!el) {
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
// Assign only on a change: `filled` drives the template, so an
|
|
366
|
+
// unconditional write re-renders, mutates, and fires again.
|
|
367
|
+
const measure = () => {
|
|
368
|
+
const next = this.hasContent(el)
|
|
369
|
+
if (this.filled[key] === next) {
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
this.filled = { ...this.filled, [key]: next }
|
|
373
|
+
}
|
|
374
|
+
if (typeof MutationObserver === 'function') {
|
|
375
|
+
const observer = new MutationObserver(measure)
|
|
376
|
+
observer.observe(el, { childList: true, subtree: true, characterData: true })
|
|
377
|
+
this.observers[key] = observer
|
|
378
|
+
}
|
|
379
|
+
// Not synchronously: a ref callback runs during the patch.
|
|
380
|
+
this.$nextTick(measure)
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Whether a host rendered anything a reader would see: text (an empty
|
|
385
|
+
* state counts), or a graphical or interactive element.
|
|
386
|
+
*
|
|
387
|
+
* @param {HTMLElement} el The host wrapper.
|
|
388
|
+
* @return {boolean}
|
|
389
|
+
*/
|
|
390
|
+
hasContent(el) {
|
|
391
|
+
if (!el) {
|
|
392
|
+
return false
|
|
393
|
+
}
|
|
394
|
+
if (el.textContent && el.textContent.trim().length > 0) {
|
|
395
|
+
return true
|
|
396
|
+
}
|
|
397
|
+
return el.querySelector('img, svg, canvas, table, input, button, select, textarea') !== null
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Drop every observer.
|
|
402
|
+
*
|
|
403
|
+
* @return {void}
|
|
404
|
+
*/
|
|
405
|
+
stopObserving() {
|
|
406
|
+
for (const observer of Object.values(this.observers || {})) {
|
|
407
|
+
observer.disconnect()
|
|
408
|
+
}
|
|
409
|
+
this.observers = {}
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
}
|
|
413
|
+
</script>
|
|
414
|
+
|
|
415
|
+
<style scoped>
|
|
416
|
+
.cn-sections-widget {
|
|
417
|
+
min-width: 0;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.cn-sections-widget__column {
|
|
421
|
+
display: flex;
|
|
422
|
+
flex-direction: column;
|
|
423
|
+
gap: calc(var(--default-grid-baseline, 4px) * 4);
|
|
424
|
+
min-width: 0;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/* A main and a side column (the settings boards: 1fr and 418px, 20px apart). */
|
|
428
|
+
.cn-sections-widget--two-column {
|
|
429
|
+
display: grid;
|
|
430
|
+
grid-template-columns: minmax(0, 1fr) minmax(280px, 418px);
|
|
431
|
+
gap: 20px;
|
|
432
|
+
align-items: start;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
@media (max-width: 1024px) {
|
|
436
|
+
.cn-sections-widget--two-column {
|
|
437
|
+
grid-template-columns: minmax(0, 1fr);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* A host page may space every <section>; these sections are spaced by the column gap only. */
|
|
442
|
+
.cn-sections-widget__section {
|
|
443
|
+
margin: 0;
|
|
444
|
+
min-width: 0;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/* A rule above a section that follows a shown one. */
|
|
448
|
+
.cn-sections-widget:not(.cn-sections-widget--cards) .cn-sections-widget__section:not(.cn-sections-widget__section--empty) ~ .cn-sections-widget__section {
|
|
449
|
+
padding-top: calc(var(--default-grid-baseline, 4px) * 4);
|
|
450
|
+
border-top: 1px solid var(--color-border);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/*
|
|
454
|
+
* An empty section keeps its host mounted (display: none still mounts it, so
|
|
455
|
+
* the widget fetches and can fill later) but takes no space, gap or rule.
|
|
456
|
+
*/
|
|
457
|
+
.cn-sections-widget__section--empty {
|
|
458
|
+
display: none;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
.cn-sections-widget__heading {
|
|
462
|
+
margin: 0 0 calc(var(--default-grid-baseline, 4px) * 2);
|
|
463
|
+
font-size: 16px;
|
|
464
|
+
font-weight: 600;
|
|
465
|
+
color: var(--color-main-text);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
.cn-sections-widget__description {
|
|
469
|
+
margin: calc(var(--default-grid-baseline, 4px) * -1) 0 calc(var(--default-grid-baseline, 4px) * 3);
|
|
470
|
+
color: var(--color-text-maxcontrast);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/* One card per section. */
|
|
474
|
+
.cn-sections-widget--cards .cn-sections-widget__column {
|
|
475
|
+
gap: 20px;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.cn-sections-widget--cards .cn-sections-widget__section {
|
|
479
|
+
background: var(--color-main-background);
|
|
480
|
+
border: 1px solid var(--color-border);
|
|
481
|
+
border-radius: var(--cn-board-card-radius, var(--border-radius-large, 12px));
|
|
482
|
+
padding: var(--cn-board-card-padding, 16px 20px);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.cn-sections-widget--cards .cn-sections-widget__heading {
|
|
486
|
+
font-size: 18px;
|
|
487
|
+
margin-bottom: 12px;
|
|
488
|
+
}
|
|
489
|
+
</style>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* CnSectionsWidget — self-registers the `sections` widget type (several
|
|
6
|
+
* widgets in one place, each under its own heading, optionally as cards in a
|
|
7
|
+
* main and a side column) into the shared dashboardWidgetRegistry at module
|
|
8
|
+
* load. Renderer only: the sections are declared in the manifest.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import CnSectionsWidget from './CnSectionsWidget.vue'
|
|
12
|
+
import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
|
|
13
|
+
|
|
14
|
+
registerDashboardWidget('sections', {
|
|
15
|
+
renderer: CnSectionsWidget,
|
|
16
|
+
form: null,
|
|
17
|
+
defaultContent: {
|
|
18
|
+
sections: [],
|
|
19
|
+
cards: false,
|
|
20
|
+
hideEmpty: true,
|
|
21
|
+
},
|
|
22
|
+
displayName: 'Sections',
|
|
23
|
+
icon: 'ViewSequential',
|
|
24
|
+
// Renders OTHER widgets, so CnDetailWidgetHost hands it the surface's
|
|
25
|
+
// sibling definitions and the context its children need.
|
|
26
|
+
container: true,
|
|
27
|
+
// The section headings are the titles; a card header above them would
|
|
28
|
+
// name the group twice.
|
|
29
|
+
ownsTitle: true,
|
|
30
|
+
surfaces: ['detail-page'],
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export { CnSectionsWidget }
|
|
34
|
+
export default CnSectionsWidget
|
|
@@ -178,8 +178,10 @@ import {
|
|
|
178
178
|
readAvailableActions,
|
|
179
179
|
transitionError,
|
|
180
180
|
} from '../../composables/useLifecycleTransitions.js'
|
|
181
|
+
import { normalizeLook } from '../../composables/useLook.js'
|
|
181
182
|
import { useObjectStore } from '../../store/useObjectStore.js'
|
|
182
183
|
import { resolveObjectOpType } from '../../utils/actionsDispatcher.js'
|
|
184
|
+
import { formatBoardDate } from '../../utils/boardDate.js'
|
|
183
185
|
import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
|
|
184
186
|
import {
|
|
185
187
|
dropOptionalUnresolved,
|
|
@@ -264,6 +266,14 @@ function userError(message) {
|
|
|
264
266
|
* labelField?, descriptionField?, finalField?, limit? }`, an OpenRegister
|
|
265
267
|
* query whose filter takes the same tokens.
|
|
266
268
|
*
|
|
269
|
+
* `datesSource`: `{ url, method?, params?, path?, idField?, dateField? }`, an
|
|
270
|
+
* endpoint (same token grammar) answering the date the record reached each
|
|
271
|
+
* stage, when the stage list does not carry it: the case type's statuses list
|
|
272
|
+
* the steps, `/blueprint?case=@objectId` says when this case reached them.
|
|
273
|
+
* Read once, merged into the steps by `idField` (default `id`) with the value
|
|
274
|
+
* at `dateField` (default `reachedAt`); a date it does not give falls back to
|
|
275
|
+
* the stage list's own `dateField`. The bars variant draws it.
|
|
276
|
+
*
|
|
267
277
|
* `finalField` marks the stages that close the record, which the timeline
|
|
268
278
|
* draws differently. It grants nothing either.
|
|
269
279
|
*
|
|
@@ -356,7 +366,7 @@ export default {
|
|
|
356
366
|
* `label`, the placement's own title, so a titled card does not have to
|
|
357
367
|
* repeat itself, and then to "Stages".
|
|
358
368
|
*
|
|
359
|
-
* @type {{currentField?: string, stagesEndpoint?: object, stagesSource?: object, transition?: object, unreachableReason?: string, orientation?: ('horizontal'|'vertical'), size?: ('medium'|'small'), ariaLabel?: string, label?: string, variant?: ('dots'|'bars')}}
|
|
369
|
+
* @type {{currentField?: string, stagesEndpoint?: object, stagesSource?: object, transition?: object, unreachableReason?: string, orientation?: ('horizontal'|'vertical'), size?: ('medium'|'small'), ariaLabel?: string, label?: string, variant?: ('dots'|'bars'), datesSource?: {url: string, method?: string, params?: object, path?: string, idField?: string, dateField?: string}}}
|
|
360
370
|
*/
|
|
361
371
|
content: {
|
|
362
372
|
type: Object,
|
|
@@ -437,6 +447,8 @@ export default {
|
|
|
437
447
|
const workspaceRaw = inject('cnWorkspaceContext', ref({}))
|
|
438
448
|
const appConfigRaw = inject('cnAppConfig', ref({}))
|
|
439
449
|
const cnTranslate = inject('cnTranslate', (key) => key)
|
|
450
|
+
// The app's look (`nextcloud` or `board`), provided by CnAppRoot or CnPageRenderer.
|
|
451
|
+
const cnLook = inject('cnLook', 'nextcloud')
|
|
440
452
|
|
|
441
453
|
/**
|
|
442
454
|
* The token context: the bound record, its id, and the page context.
|
|
@@ -462,15 +474,24 @@ export default {
|
|
|
462
474
|
return (cfg && cfg.url) ? { url: cfg.url, method: cfg.method, params: cfg.params } : null
|
|
463
475
|
}, { ctx: tokenCtx })
|
|
464
476
|
|
|
477
|
+
// The dates the record reached each stage, when they live apart from
|
|
478
|
+
// the stage list (`datesSource`): one read, merged into the steps by id.
|
|
479
|
+
const datesRead = useEndpointSource(() => {
|
|
480
|
+
const cfg = props.content?.datesSource
|
|
481
|
+
return (cfg && cfg.url) ? { url: cfg.url, method: cfg.method, params: cfg.params } : null
|
|
482
|
+
}, { ctx: tokenCtx })
|
|
483
|
+
|
|
465
484
|
return {
|
|
466
485
|
detailCtxRaw,
|
|
467
486
|
objectCtxRaw,
|
|
468
487
|
cnTranslate,
|
|
488
|
+
cnLook,
|
|
469
489
|
tokenCtx,
|
|
470
490
|
stagesBody: stagesRead.data,
|
|
471
491
|
stagesBodyLoading: stagesRead.loading,
|
|
472
492
|
stagesBodyError: stagesRead.error,
|
|
473
493
|
stagesBodyBlocked: stagesRead.blocked,
|
|
494
|
+
datesBody: datesRead.data,
|
|
474
495
|
}
|
|
475
496
|
},
|
|
476
497
|
|
|
@@ -614,6 +635,42 @@ export default {
|
|
|
614
635
|
return normalizeStages(rows, { ...cfg, orderField })
|
|
615
636
|
},
|
|
616
637
|
|
|
638
|
+
/**
|
|
639
|
+
* The reached dates by stage id, from `datesSource`: the rows at its
|
|
640
|
+
* `path` (or the response itself when it is a list), keyed by
|
|
641
|
+
* `idField` (default `id`), the value at `dateField` (default
|
|
642
|
+
* `reachedAt`). Empty without the option, while it loads, or when it
|
|
643
|
+
* failed: the steps then show the stage list's own dates.
|
|
644
|
+
*
|
|
645
|
+
* @return {{[id: string]: string}} Date by stage id.
|
|
646
|
+
* @spec openspec/changes/screens-stage-dates-source/specs/stage-dates-source/spec.md#requirement-the-reached-dates-can-come-from-their-own-endpoint
|
|
647
|
+
*/
|
|
648
|
+
stageDates() {
|
|
649
|
+
const cfg = this.content.datesSource
|
|
650
|
+
const body = this.datesBody
|
|
651
|
+
if (!cfg || !cfg.url || body === null || body === undefined) {
|
|
652
|
+
return {}
|
|
653
|
+
}
|
|
654
|
+
const rows = Array.isArray(body) ? body : getByPath(body, cfg.path || '')
|
|
655
|
+
if (!Array.isArray(rows)) {
|
|
656
|
+
return {}
|
|
657
|
+
}
|
|
658
|
+
const idField = cfg.idField || 'id'
|
|
659
|
+
const dateField = cfg.dateField || 'reachedAt'
|
|
660
|
+
const out = {}
|
|
661
|
+
for (const row of rows) {
|
|
662
|
+
if (!row || typeof row !== 'object') {
|
|
663
|
+
continue
|
|
664
|
+
}
|
|
665
|
+
const id = getByPath(row, idField)
|
|
666
|
+
const date = getByPath(row, dateField)
|
|
667
|
+
if (id !== null && id !== undefined && id !== '' && typeof date === 'string' && date !== '') {
|
|
668
|
+
out[String(id)] = date
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return out
|
|
672
|
+
},
|
|
673
|
+
|
|
617
674
|
/**
|
|
618
675
|
* Whether the stage list is still on its way.
|
|
619
676
|
*
|
|
@@ -795,7 +852,7 @@ export default {
|
|
|
795
852
|
}
|
|
796
853
|
return {
|
|
797
854
|
...stage,
|
|
798
|
-
date: this.stages[index]?.date || '',
|
|
855
|
+
date: this.barDate(this.stageDates[stage.id] || this.stages[index]?.date || '', state),
|
|
799
856
|
state,
|
|
800
857
|
clickable: this.interactive && state !== 'current',
|
|
801
858
|
}
|
|
@@ -891,6 +948,28 @@ export default {
|
|
|
891
948
|
},
|
|
892
949
|
|
|
893
950
|
methods: {
|
|
951
|
+
/**
|
|
952
|
+
* A bar's date line. Under the board look a date value reads in the
|
|
953
|
+
* board's short form ("3 okt"), and the current step's reads "sinds
|
|
954
|
+
* 4 okt", as DqZaak draws it. Any other value, and every value outside
|
|
955
|
+
* the board look, shows as written.
|
|
956
|
+
*
|
|
957
|
+
* @param {string} value The `dateField` value of the stage row.
|
|
958
|
+
* @param {string} state The bar state (`done`, `current`, `todo`).
|
|
959
|
+
* @return {string} The date line, or ''.
|
|
960
|
+
* @spec openspec/changes/screens-stage-bar-dates-parity/specs/stage-bar-dates/spec.md#requirement-a-bar-date-reads-as-the-board-draws-it
|
|
961
|
+
*/
|
|
962
|
+
barDate(value, state) {
|
|
963
|
+
if (!value || normalizeLook(this.cnLook) !== 'board') {
|
|
964
|
+
return value
|
|
965
|
+
}
|
|
966
|
+
const short = formatBoardDate(value)
|
|
967
|
+
if (short === '') {
|
|
968
|
+
return value
|
|
969
|
+
}
|
|
970
|
+
return state === 'current' ? t('nextcloud-vue', 'since {date}', { date: short }) : short
|
|
971
|
+
},
|
|
972
|
+
|
|
894
973
|
/**
|
|
895
974
|
* Translate a library string.
|
|
896
975
|
*
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
aria-hidden="true">
|
|
47
47
|
<CnWidgetIcon :name="resolvedIcon" :size="18" />
|
|
48
48
|
</span>
|
|
49
|
-
<span v-if="content.label" :title="resolvedLabel">{{ resolvedLabel }}</span>
|
|
49
|
+
<span v-if="content.label" class="cn-kpi-card__title-text" :title="resolvedLabel">{{ resolvedLabel }}</span>
|
|
50
50
|
|
|
51
51
|
<!-- Per-tile range override. Rendered ONLY when the tile declares its
|
|
52
52
|
own `content.dateRange.presets`; a tile that merely follows the
|