@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,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* boardDate: the two short time readings the drawn boards put in a table cell.
|
|
6
|
+
*
|
|
7
|
+
* - A date ("5 okt", "30 okt", "14 feb 2024"): day and short month in the
|
|
8
|
+
* user's language, the year only when it is not the current year.
|
|
9
|
+
* - An age ("4 uur", "1 dag", "3 dagen"): how long something has been
|
|
10
|
+
* waiting, in hours under a day and in whole calendar days after that.
|
|
11
|
+
*
|
|
12
|
+
* Both read the Nextcloud user LANGUAGE for the month and unit names. A
|
|
13
|
+
* Nextcloud account keeps its locale (date order) apart from its language,
|
|
14
|
+
* and a Dutch reader whose locale was left at the en_US default saw
|
|
15
|
+
* "Feb 14, 2024": the language decides the words, so it decides here, and the
|
|
16
|
+
* locale is only used when it is a region of that same language.
|
|
17
|
+
*
|
|
18
|
+
* @module utils/boardDate
|
|
19
|
+
* @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { getCanonicalLocale, getLanguage, translatePlural as n } from '@nextcloud/l10n'
|
|
23
|
+
import { parseDateValue } from './dateVariant.js'
|
|
24
|
+
import { compareVisibleWhen } from './visibleWhen.js'
|
|
25
|
+
|
|
26
|
+
const MS_PER_HOUR = 3600000
|
|
27
|
+
const MS_PER_DAY = 86400000
|
|
28
|
+
|
|
29
|
+
/** Variants an age rule may name. `danger` is accepted as an alias of `error`. */
|
|
30
|
+
export const AGE_VARIANTS = Object.freeze(['default', 'success', 'warning', 'error'])
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Read a language or locale tag from an @nextcloud/l10n getter, which throws
|
|
34
|
+
* when Nextcloud has not set one (outside a Nextcloud page, in a test).
|
|
35
|
+
*
|
|
36
|
+
* @param {() => string} getter `getLanguage` or `getCanonicalLocale`.
|
|
37
|
+
* @return {string} The tag with `-` separators, or ''.
|
|
38
|
+
*/
|
|
39
|
+
function readTag(getter) {
|
|
40
|
+
try {
|
|
41
|
+
return String(getter() || '').replace(/_/g, '-')
|
|
42
|
+
} catch {
|
|
43
|
+
return ''
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The BCP 47 tag the board dates are written in: the user's locale when it is
|
|
49
|
+
* a region of the user's language (`nl-BE` for language `nl`), else the
|
|
50
|
+
* language itself.
|
|
51
|
+
*
|
|
52
|
+
* @return {string|undefined} The tag, or undefined to let Intl choose.
|
|
53
|
+
* @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-a-board-date-cell-reads-day-and-short-month-in-the-user-language
|
|
54
|
+
*/
|
|
55
|
+
export function boardDateLocale() {
|
|
56
|
+
const language = readTag(getLanguage)
|
|
57
|
+
const locale = readTag(getCanonicalLocale)
|
|
58
|
+
const base = (tag) => tag.split('-')[0].toLowerCase()
|
|
59
|
+
if (language && locale && base(language) === base(locale)) {
|
|
60
|
+
return locale
|
|
61
|
+
}
|
|
62
|
+
return language || locale || undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A date in the board's short form: "5 okt", or "14 feb 2024" outside the
|
|
67
|
+
* current year.
|
|
68
|
+
*
|
|
69
|
+
* @param {unknown} value A Date, a timestamp or an ISO string.
|
|
70
|
+
* @param {Date} [now] The reference moment (defaults to the current time).
|
|
71
|
+
* @param {string} [locale] BCP 47 tag (defaults to `boardDateLocale()`).
|
|
72
|
+
* @return {string} The short date, or '' when the value is not a date.
|
|
73
|
+
* @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-a-board-date-cell-reads-day-and-short-month-in-the-user-language
|
|
74
|
+
*/
|
|
75
|
+
export function formatBoardDate(value, now = new Date(), locale = boardDateLocale()) {
|
|
76
|
+
const date = parseDateValue(value)
|
|
77
|
+
if (date === null) {
|
|
78
|
+
return ''
|
|
79
|
+
}
|
|
80
|
+
const options = { day: 'numeric', month: 'short' }
|
|
81
|
+
if (date.getFullYear() !== now.getFullYear()) {
|
|
82
|
+
options.year = 'numeric'
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return new Intl.DateTimeFormat(locale, options).format(date)
|
|
86
|
+
} catch {
|
|
87
|
+
return new Intl.DateTimeFormat(undefined, options).format(date)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whole calendar days from the date until `now` (0 today, 1 yesterday), or
|
|
93
|
+
* null when the value is not a date. A future date gives a negative number.
|
|
94
|
+
*
|
|
95
|
+
* @param {unknown} value The date.
|
|
96
|
+
* @param {Date} [now] The reference moment.
|
|
97
|
+
* @return {number|null} The day count.
|
|
98
|
+
* @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-says-how-long-something-has-waited
|
|
99
|
+
*/
|
|
100
|
+
export function ageInDays(value, now = new Date()) {
|
|
101
|
+
const date = parseDateValue(value)
|
|
102
|
+
if (date === null) {
|
|
103
|
+
return null
|
|
104
|
+
}
|
|
105
|
+
// UTC midnights of the two LOCAL days, so a daylight-saving hour never
|
|
106
|
+
// turns one day into zero or two.
|
|
107
|
+
const start = Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())
|
|
108
|
+
const today = Date.UTC(now.getFullYear(), now.getMonth(), now.getDate())
|
|
109
|
+
return Math.round((today - start) / MS_PER_DAY)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* How long something has waited: "4 uur" under 24 hours (at least 1), else
|
|
114
|
+
* "1 dag" / "3 dagen" in whole calendar days. Plural forms come from the
|
|
115
|
+
* library's own catalogue (`nextcloud-vue`). A date in the future reads as
|
|
116
|
+
* waiting no time: "0 hours".
|
|
117
|
+
*
|
|
118
|
+
* @param {unknown} value The moment the wait started.
|
|
119
|
+
* @param {Date} [now] The reference moment.
|
|
120
|
+
* @return {string} The age, or '' when the value is not a date.
|
|
121
|
+
* @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-says-how-long-something-has-waited
|
|
122
|
+
*/
|
|
123
|
+
export function formatAge(value, now = new Date()) {
|
|
124
|
+
const date = parseDateValue(value)
|
|
125
|
+
if (date === null) {
|
|
126
|
+
return ''
|
|
127
|
+
}
|
|
128
|
+
const elapsed = now.getTime() - date.getTime()
|
|
129
|
+
if (elapsed < MS_PER_DAY) {
|
|
130
|
+
const hours = elapsed <= 0 ? 0 : Math.max(1, Math.floor(elapsed / MS_PER_HOUR))
|
|
131
|
+
return n('nextcloud-vue', '%n hour', '%n hours', hours)
|
|
132
|
+
}
|
|
133
|
+
const days = shownAgeDays(date, now)
|
|
134
|
+
return n('nextcloud-vue', '%n day', '%n days', days)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The number of days an age cell shows: 0 under 24 hours (the cell then reads
|
|
139
|
+
* hours), else the whole calendar days, at least 1. The threshold rules
|
|
140
|
+
* compare against this number, so the colour always agrees with the text.
|
|
141
|
+
*
|
|
142
|
+
* @param {unknown} value The moment the wait started.
|
|
143
|
+
* @param {Date} [now] The reference moment.
|
|
144
|
+
* @return {number|null} The day count, or null when the value is not a date.
|
|
145
|
+
* @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-turns-red-past-its-threshold
|
|
146
|
+
*/
|
|
147
|
+
export function shownAgeDays(value, now = new Date()) {
|
|
148
|
+
const date = parseDateValue(value)
|
|
149
|
+
if (date === null) {
|
|
150
|
+
return null
|
|
151
|
+
}
|
|
152
|
+
if (now.getTime() - date.getTime() < MS_PER_DAY) {
|
|
153
|
+
return 0
|
|
154
|
+
}
|
|
155
|
+
return Math.max(1, ageInDays(date, now))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* The variant of the first `variantWhen` rule the age in days matches, or ''
|
|
160
|
+
* when none does. Same rule shape as the `date` widget (`{ op, value,
|
|
161
|
+
* variant }`, first match wins), compared against the number of days the cell
|
|
162
|
+
* shows (`shownAgeDays`): `[{ op: "gte", value: 3, variant: "error" }]` turns a wait of three
|
|
163
|
+
* days or more red.
|
|
164
|
+
*
|
|
165
|
+
* @param {unknown} value The moment the wait started.
|
|
166
|
+
* @param {Array<{op: string, value: number, variant: string}>} rules The rules.
|
|
167
|
+
* @param {Date} [now] The reference moment.
|
|
168
|
+
* @return {string} `success | warning | error | default`, or ''.
|
|
169
|
+
* @spec openspec/changes/screens-cell-date-parity/specs/cell-date-age/spec.md#requirement-an-age-cell-turns-red-past-its-threshold
|
|
170
|
+
*/
|
|
171
|
+
export function resolveAgeVariant(value, rules, now = new Date()) {
|
|
172
|
+
if (!Array.isArray(rules) || rules.length === 0) {
|
|
173
|
+
return ''
|
|
174
|
+
}
|
|
175
|
+
const days = shownAgeDays(value, now)
|
|
176
|
+
if (days === null) {
|
|
177
|
+
return ''
|
|
178
|
+
}
|
|
179
|
+
for (const rule of rules) {
|
|
180
|
+
if (!rule || typeof rule !== 'object') {
|
|
181
|
+
continue
|
|
182
|
+
}
|
|
183
|
+
if (compareVisibleWhen(days, rule.op || 'eq', rule.value) === true) {
|
|
184
|
+
const variant = rule.variant === 'danger' ? 'error' : rule.variant
|
|
185
|
+
return AGE_VARIANTS.includes(variant) ? variant : ''
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return ''
|
|
189
|
+
}
|
|
@@ -201,13 +201,29 @@ export function resolveNextStep(config, object, stageField = 'status') {
|
|
|
201
201
|
/**
|
|
202
202
|
* Resolve a header pill from the record.
|
|
203
203
|
*
|
|
204
|
-
*
|
|
204
|
+
* A pill with `field` reads it off the record; one without `field` and with
|
|
205
|
+
* `text` is a constant. A count pill (`countFrom`) is resolved by
|
|
206
|
+
* {@link resolveCountPill} once its count is known.
|
|
207
|
+
*
|
|
208
|
+
* @param {{field?: string, text?: string, colorMap?: object, labels?: object, variant?: string}|null} config The pill config.
|
|
205
209
|
* @param {object|null} object The record.
|
|
206
210
|
* @return {{label: string, colorKey: string, colorMap: object|null, variant: string}|null} What CnStatusBadge needs, or null when the field is empty.
|
|
207
211
|
* @spec openspec/changes/detail-action-model-and-case-surfaces/specs/detail-action-model/spec.md#requirement-header-pills
|
|
208
212
|
*/
|
|
209
213
|
export function resolvePill(config, object) {
|
|
210
|
-
if (!config || typeof config !== 'object'
|
|
214
|
+
if (!config || typeof config !== 'object') {
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
// A constant pill (`text`): "Inwoner", "Klant" (screens-relation-pages).
|
|
218
|
+
if ((typeof config.field !== 'string' || config.field === '') && typeof config.text === 'string' && config.text !== '') {
|
|
219
|
+
return {
|
|
220
|
+
label: config.text,
|
|
221
|
+
colorKey: config.text,
|
|
222
|
+
colorMap: config.colorMap && typeof config.colorMap === 'object' ? config.colorMap : null,
|
|
223
|
+
variant: typeof config.variant === 'string' && config.variant !== '' ? config.variant : 'default',
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (typeof config.field !== 'string' || config.field === '') {
|
|
211
227
|
return null
|
|
212
228
|
}
|
|
213
229
|
const raw = stageOf(object, config.field)
|
|
@@ -260,3 +276,44 @@ export function resolveTabCount(tab, object) {
|
|
|
260
276
|
const number = Number(value)
|
|
261
277
|
return Number.isFinite(number) ? number : null
|
|
262
278
|
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The list a count pill counts (`countFrom`), or null for any other pill.
|
|
282
|
+
*
|
|
283
|
+
* @param {object|null} config The pill config.
|
|
284
|
+
* @return {{register: string, schema: string, filter: object}|null} The list.
|
|
285
|
+
* @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count
|
|
286
|
+
*/
|
|
287
|
+
export function pillCountSource(config) {
|
|
288
|
+
const from = config && typeof config === 'object' && !config.field ? config.countFrom : null
|
|
289
|
+
if (!from || typeof from !== 'object' || typeof from.register !== 'string' || from.register === '' || typeof from.schema !== 'string' || from.schema === '') {
|
|
290
|
+
return null
|
|
291
|
+
}
|
|
292
|
+
const filter = from.filter && typeof from.filter === 'object' && !Array.isArray(from.filter) ? from.filter : {}
|
|
293
|
+
return { register: from.register, schema: from.schema, filter }
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* A count pill once its count is known: "1 lopende zaak", "2 lopende zaken".
|
|
298
|
+
* Nothing for a count of 0, an unknown count or a failed one.
|
|
299
|
+
*
|
|
300
|
+
* @param {{countLabel?: {one?: string, other?: string}, colorMap?: object, variant?: string}|null} config The pill config.
|
|
301
|
+
* @param {number|null|undefined} count The count.
|
|
302
|
+
* @param {(text: string) => string} [translate] Applied to the label before `{count}` is filled.
|
|
303
|
+
* @return {{label: string, colorKey: string, colorMap: object|null, variant: string}|null} What CnStatusBadge needs, or null.
|
|
304
|
+
* @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count
|
|
305
|
+
*/
|
|
306
|
+
export function resolveCountPill(config, count, translate = (text) => text) {
|
|
307
|
+
if (!pillCountSource(config) || typeof count !== 'number' || !Number.isFinite(count) || count <= 0) {
|
|
308
|
+
return null
|
|
309
|
+
}
|
|
310
|
+
const labels = config.countLabel && typeof config.countLabel === 'object' ? config.countLabel : {}
|
|
311
|
+
const template = (count === 1 ? labels.one : labels.other) || labels.other || labels.one || '{count}'
|
|
312
|
+
const label = translate(template).replace(/\{count\}/g, String(count))
|
|
313
|
+
return {
|
|
314
|
+
label,
|
|
315
|
+
colorKey: '',
|
|
316
|
+
colorMap: config.colorMap && typeof config.colorMap === 'object' ? config.colorMap : null,
|
|
317
|
+
variant: typeof config.variant === 'string' && config.variant !== '' ? config.variant : 'default',
|
|
318
|
+
}
|
|
319
|
+
}
|
|
@@ -101,6 +101,7 @@ const DEFAULT_MAP = {
|
|
|
101
101
|
enum: 'enumValues',
|
|
102
102
|
required: 'isRequired',
|
|
103
103
|
default: 'defaultValue',
|
|
104
|
+
hideOnCreate: 'hideOnCreate',
|
|
104
105
|
}
|
|
105
106
|
|
|
106
107
|
/**
|
|
@@ -312,6 +313,8 @@ export function definitionQueryParams(config, value, formData = {}) {
|
|
|
312
313
|
* @param {object} [config] The `x-openregister-extends-form` block (`map`, `typeMap`).
|
|
313
314
|
* @param {object} [options] Extra options.
|
|
314
315
|
* @param {number} [options.orderFrom] Order assigned to the first field; each subsequent field takes the next integer. Dynamic fields sort after the schema's own, so pass a number above every declared `order`.
|
|
316
|
+
* @param {boolean} [options.create] The form creates a new object: a definition whose `hideOnCreate` (or the field `config.map.hideOnCreate` names) is `true` gets no field, because the app fills that answer itself at creation. Edit forms still show it.
|
|
317
|
+
* @spec openspec/changes/dynamic-question-hide-on-create/specs/dynamic-question-hide-on-create/spec.md#requirement-a-question-can-stay-off-the-create-form
|
|
315
318
|
* @return {{properties: object, required: string[]}} Properties keyed by prefixed id, plus the required key list.
|
|
316
319
|
*/
|
|
317
320
|
export function propertiesFromDefinitions(definitions, config = {}, options = {}) {
|
|
@@ -321,7 +324,8 @@ export function propertiesFromDefinitions(definitions, config = {}, options = {}
|
|
|
321
324
|
|
|
322
325
|
const properties = {}
|
|
323
326
|
const required = []
|
|
324
|
-
const list = Array.isArray(definitions) ? definitions : []
|
|
327
|
+
const list = (Array.isArray(definitions) ? definitions : [])
|
|
328
|
+
.filter((record) => !(options.create === true && record && mapped(record, map, 'hideOnCreate') === true))
|
|
325
329
|
|
|
326
330
|
list.forEach((record, index) => {
|
|
327
331
|
if (!record || !record.id) {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
2
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
3
|
+
|
|
4
|
+
import { boardDateLocale, formatBoardDate } from './boardDate.js'
|
|
5
|
+
import { parseDateValue } from './dateVariant.js'
|
|
6
|
+
import { readPath } from './readPath.js'
|
|
7
|
+
|
|
8
|
+
const TOKEN = /\{([^{}\s|]+)(?:\|([a-z]+))?\}/g
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A moment in the board's short form with the time: "4 okt, 10:52", or
|
|
12
|
+
* "14 feb 2024, 09:00" outside the current year.
|
|
13
|
+
*
|
|
14
|
+
* @param {unknown} value A Date, a timestamp or an ISO string.
|
|
15
|
+
* @param {Date} [now] The reference moment.
|
|
16
|
+
* @return {string} The text, or '' when the value is not a date.
|
|
17
|
+
* @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record
|
|
18
|
+
*/
|
|
19
|
+
export function formatBoardDateTime(value, now = new Date()) {
|
|
20
|
+
const day = formatBoardDate(value, now)
|
|
21
|
+
const date = parseDateValue(value)
|
|
22
|
+
if (day === '' || date === null) {
|
|
23
|
+
return ''
|
|
24
|
+
}
|
|
25
|
+
let time
|
|
26
|
+
try {
|
|
27
|
+
time = new Intl.DateTimeFormat(boardDateLocale(), { hour: '2-digit', minute: '2-digit' }).format(date)
|
|
28
|
+
} catch {
|
|
29
|
+
time = new Intl.DateTimeFormat(undefined, { hour: '2-digit', minute: '2-digit' }).format(date)
|
|
30
|
+
}
|
|
31
|
+
return `${day}, ${time}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Format one token value.
|
|
36
|
+
*
|
|
37
|
+
* @param {unknown} value The raw value.
|
|
38
|
+
* @param {string} format `date`, `datetime` or '' (as is).
|
|
39
|
+
* @param {Date} now The reference moment.
|
|
40
|
+
* @return {string} The text, '' when nothing can be shown.
|
|
41
|
+
*/
|
|
42
|
+
function formatValue(value, format, now) {
|
|
43
|
+
if (value === null || value === undefined || value === '' || typeof value === 'object') {
|
|
44
|
+
return ''
|
|
45
|
+
}
|
|
46
|
+
if (format === 'date') {
|
|
47
|
+
return formatBoardDate(value, now)
|
|
48
|
+
}
|
|
49
|
+
if (format === 'datetime') {
|
|
50
|
+
return formatBoardDateTime(value, now)
|
|
51
|
+
}
|
|
52
|
+
return String(value)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Fill a row template such as `"{identifier} · {caseTypeName}"` or
|
|
57
|
+
* `"Termijn {deadline|date}"` from an object.
|
|
58
|
+
*
|
|
59
|
+
* Every `{path}` token is a dot path into the object; `{path|date}` and
|
|
60
|
+
* `{path|datetime}` format it as the boards write dates. A token without a
|
|
61
|
+
* value empties the WHOLE template, because "Termijn " is worse than nothing.
|
|
62
|
+
* A template without tokens is returned as written (a constant label).
|
|
63
|
+
*
|
|
64
|
+
* @param {string} template The template, already translated.
|
|
65
|
+
* @param {object|null} object The object to read.
|
|
66
|
+
* @param {Date} [now] The reference moment for dates.
|
|
67
|
+
* @return {string} The filled text, or ''.
|
|
68
|
+
* @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record
|
|
69
|
+
*/
|
|
70
|
+
export function fillTemplate(template, object, now = new Date()) {
|
|
71
|
+
if (typeof template !== 'string' || template.trim() === '') {
|
|
72
|
+
return ''
|
|
73
|
+
}
|
|
74
|
+
let missing = false
|
|
75
|
+
const text = template.replace(TOKEN, (_match, path, format) => {
|
|
76
|
+
const value = object && typeof object === 'object' ? readPath(object, path) : undefined
|
|
77
|
+
const shown = formatValue(value, format || '', now)
|
|
78
|
+
if (shown === '') {
|
|
79
|
+
missing = true
|
|
80
|
+
}
|
|
81
|
+
return shown
|
|
82
|
+
})
|
|
83
|
+
return missing ? '' : text.trim()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The first of several templates that fills: `["Klaar {closedAt|date}",
|
|
88
|
+
* "Termijn {deadline|date}"]` reads "Klaar ..." once the case is closed and
|
|
89
|
+
* "Termijn ..." while it is open. A single string is one candidate.
|
|
90
|
+
*
|
|
91
|
+
* @param {string|string[]} templates The candidates, in order.
|
|
92
|
+
* @param {object|null} object The object to read.
|
|
93
|
+
* @param {(key: string) => string} [translate] Applied to each template before filling.
|
|
94
|
+
* @param {Date} [now] The reference moment for dates.
|
|
95
|
+
* @return {string} The first filled text, or ''.
|
|
96
|
+
* @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record
|
|
97
|
+
*/
|
|
98
|
+
export function firstFilled(templates, object, translate = (key) => key, now = new Date()) {
|
|
99
|
+
const list = Array.isArray(templates) ? templates : [templates]
|
|
100
|
+
for (const template of list) {
|
|
101
|
+
if (typeof template !== 'string') {
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
const text = fillTemplate(translate(template), object, now)
|
|
105
|
+
if (text !== '') {
|
|
106
|
+
return text
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return ''
|
|
110
|
+
}
|
|
@@ -100,6 +100,8 @@ export const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
|
|
|
100
100
|
'delta',
|
|
101
101
|
'divider',
|
|
102
102
|
'document-review',
|
|
103
|
+
'facts',
|
|
104
|
+
'field-list',
|
|
103
105
|
'files',
|
|
104
106
|
'flow-runs',
|
|
105
107
|
'gauge',
|
|
@@ -123,8 +125,10 @@ export const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
|
|
|
123
125
|
'object-table',
|
|
124
126
|
'people',
|
|
125
127
|
'quicklinks',
|
|
128
|
+
'record-list',
|
|
126
129
|
'related',
|
|
127
130
|
'saved-view',
|
|
131
|
+
'sections',
|
|
128
132
|
'spend-analytics',
|
|
129
133
|
'stacked-bar',
|
|
130
134
|
'stages',
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* Query presets of an index page (`config.queryPresets`): a menu entry that
|
|
6
|
+
* deep-links to a list with a query (`{ caseType: '<uuid>' }`, the shape hydra
|
|
7
|
+
* gate-68 asks for instead of a second page) can bring its own lenses,
|
|
8
|
+
* columns and copy. The page names, per preset, the query it answers to and
|
|
9
|
+
* the keys it replaces; the first preset whose every `match` pair is in the
|
|
10
|
+
* current route query wins.
|
|
11
|
+
*
|
|
12
|
+
* @module utils/queryPresets
|
|
13
|
+
* @spec openspec/changes/screens-index-query-presets/specs/index-query-presets/spec.md#requirement-a-query-preset-overlays-lenses-columns-and-copy
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The config keys a preset may replace. Anything else in a preset is ignored:
|
|
18
|
+
* a preset changes what the list shows, never which register or schema it
|
|
19
|
+
* reads or what it may write.
|
|
20
|
+
*
|
|
21
|
+
* @type {ReadonlyArray<string>}
|
|
22
|
+
*/
|
|
23
|
+
export const QUERY_PRESET_KEYS = Object.freeze([
|
|
24
|
+
'title',
|
|
25
|
+
'quickFilters',
|
|
26
|
+
'columns',
|
|
27
|
+
'cardFields',
|
|
28
|
+
'countText',
|
|
29
|
+
'searchPlaceholder',
|
|
30
|
+
'footerNote',
|
|
31
|
+
'defaultSort',
|
|
32
|
+
'viewSwitch',
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether one route query value satisfies one `match` value. A repeated query
|
|
37
|
+
* parameter (an array) matches when it holds the value.
|
|
38
|
+
*
|
|
39
|
+
* @param {unknown} actual The route query value.
|
|
40
|
+
* @param {string|number|boolean} expected The preset's value.
|
|
41
|
+
* @return {boolean} True when they match.
|
|
42
|
+
*/
|
|
43
|
+
function queryValueMatches(actual, expected) {
|
|
44
|
+
const want = String(expected)
|
|
45
|
+
if (Array.isArray(actual)) {
|
|
46
|
+
return actual.some((v) => String(v) === want)
|
|
47
|
+
}
|
|
48
|
+
return actual !== undefined && actual !== null && String(actual) === want
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The index of the first preset whose every `match` pair is in the query.
|
|
53
|
+
* A preset without a non-empty `match` never matches.
|
|
54
|
+
*
|
|
55
|
+
* @param {Array<object>} presets The page's `queryPresets`.
|
|
56
|
+
* @param {object} query The current route query.
|
|
57
|
+
* @return {number} The preset index, or -1.
|
|
58
|
+
*/
|
|
59
|
+
export function matchQueryPreset(presets, query) {
|
|
60
|
+
if (!Array.isArray(presets)) {
|
|
61
|
+
return -1
|
|
62
|
+
}
|
|
63
|
+
const q = query && typeof query === 'object' ? query : {}
|
|
64
|
+
return presets.findIndex((preset) => {
|
|
65
|
+
const match = preset && typeof preset.match === 'object' && preset.match !== null ? preset.match : null
|
|
66
|
+
if (!match || Object.keys(match).length === 0) {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
return Object.entries(match).every(([key, value]) => queryValueMatches(q[key], value))
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The page config with the preset's allowed keys laid over it, and without
|
|
75
|
+
* `queryPresets` itself (the page component has no such prop).
|
|
76
|
+
*
|
|
77
|
+
* @param {object} config The page config.
|
|
78
|
+
* @param {object|null} preset The matched preset, or null.
|
|
79
|
+
* @return {object} The effective config.
|
|
80
|
+
*/
|
|
81
|
+
export function applyQueryPreset(config, preset) {
|
|
82
|
+
const { queryPresets, ...rest } = config || {}
|
|
83
|
+
if (!preset || typeof preset !== 'object') {
|
|
84
|
+
return rest
|
|
85
|
+
}
|
|
86
|
+
const out = { ...rest }
|
|
87
|
+
for (const key of QUERY_PRESET_KEYS) {
|
|
88
|
+
if (key !== 'title' && preset[key] !== undefined) {
|
|
89
|
+
out[key] = preset[key]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out
|
|
93
|
+
}
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
* - `@today±Nd` → relative-date arithmetic at day granularity: N days
|
|
23
23
|
* from today (e.g. `@today-30d`, `@today+7d`)
|
|
24
24
|
* - `@monthStart` → first day of the current month, `YYYY-MM-DD`
|
|
25
|
+
* - `@monthEnd` → last day of the current month, `YYYY-MM-DD`; on a
|
|
26
|
+
* date-time field use `lt: '@nextMonthStart'` instead,
|
|
27
|
+
* since `lte` a date stops at that day's midnight
|
|
28
|
+
* - `@nextMonthStart` → first day of the next month, `YYYY-MM-DD`
|
|
25
29
|
* - `@quarterStart` → first day of the current quarter, `YYYY-MM-DD`
|
|
26
30
|
* - `@yearStart` → first day of the current year, `YYYY-MM-DD`
|
|
27
31
|
* - `@currentFiscalYear` → the current calendar year as a number string, e.g. `2026`
|
|
@@ -214,6 +218,13 @@ export function resolveFilterValue(v, ctx) {
|
|
|
214
218
|
if (v === '@monthStart') {
|
|
215
219
|
return ymd(new Date(now.getFullYear(), now.getMonth(), 1))
|
|
216
220
|
}
|
|
221
|
+
if (v === '@monthEnd') {
|
|
222
|
+
// Day 0 of the next month is the last day of this one.
|
|
223
|
+
return ymd(new Date(now.getFullYear(), now.getMonth() + 1, 0))
|
|
224
|
+
}
|
|
225
|
+
if (v === '@nextMonthStart') {
|
|
226
|
+
return ymd(new Date(now.getFullYear(), now.getMonth() + 1, 1))
|
|
227
|
+
}
|
|
217
228
|
if (v === '@quarterStart') {
|
|
218
229
|
return ymd(new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1))
|
|
219
230
|
}
|
package/src/utils/schema.js
CHANGED
|
@@ -1050,12 +1050,33 @@ export function fieldsFromSchema(schema, options = {}) {
|
|
|
1050
1050
|
if (overrides[key]) {
|
|
1051
1051
|
Object.assign(field, overrides[key])
|
|
1052
1052
|
applyPickerOverride(field, overrides[key])
|
|
1053
|
+
translateOverrideText(field, overrides[key], tr)
|
|
1053
1054
|
}
|
|
1054
1055
|
|
|
1055
1056
|
return field
|
|
1056
1057
|
})
|
|
1057
1058
|
}
|
|
1058
1059
|
|
|
1060
|
+
/**
|
|
1061
|
+
* Run an override's display text through the translate function, as the
|
|
1062
|
+
* schema's own title and description are. A manifest writes `label`,
|
|
1063
|
+
* `description` and `placeholder` in its source language; without this a
|
|
1064
|
+
* form showed "Requester" to a Dutch user whose catalogue holds "Aanvrager".
|
|
1065
|
+
*
|
|
1066
|
+
* @param {object} field The field, with the override already merged in.
|
|
1067
|
+
* @param {object} override The per-key override.
|
|
1068
|
+
* @param {(text: string) => string} tr The translate function (identity when none).
|
|
1069
|
+
* @return {void}
|
|
1070
|
+
* @spec openspec/changes/screens-form-override-labels-parity/specs/form-override-labels/spec.md#requirement-override-text-reads-in-the-users-language
|
|
1071
|
+
*/
|
|
1072
|
+
function translateOverrideText(field, override, tr) {
|
|
1073
|
+
for (const name of ['label', 'description', 'placeholder']) {
|
|
1074
|
+
if (typeof override[name] === 'string' && override[name] !== '') {
|
|
1075
|
+
field[name] = tr(override[name])
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1059
1080
|
/**
|
|
1060
1081
|
* Generate faceted filter definitions from a schema's facetable properties.
|
|
1061
1082
|
*
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* A token is valid only in the context whose resolver runs on it:
|
|
27
27
|
*
|
|
28
28
|
* - **filter** — `resolveFilterTokens`, resolved at fetch time:
|
|
29
|
-
* `@me`, `@myGroups`, `@now`, `@today`, `@today±Nd`, `@monthStart`,
|
|
29
|
+
* `@me`, `@myGroups`, `@now`, `@today`, `@today±Nd`, `@monthStart`, `@monthEnd`, `@nextMonthStart`,
|
|
30
30
|
* `@quarterStart`, `@yearStart`.
|
|
31
31
|
* - **config** — `IAppConfig`-sourced: `@resolve:<key>`, `@config.<key>`
|
|
32
32
|
* (trailing `?` marks optional).
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
* @type {Readonly<Record<string, string>>}
|
|
68
68
|
*/
|
|
69
69
|
export const SENTINEL_TOKEN_PATTERNS = Object.freeze({
|
|
70
|
-
filter: '^@(?:me(?:\\.(?:displayName|email))?|myGroups|now|today|monthStart|quarterStart|yearStart)$|^@today[+-][0-9]+d$',
|
|
70
|
+
filter: '^@(?:me(?:\\.(?:displayName|email))?|myGroups|now|today|monthStart|monthEnd|nextMonthStart|quarterStart|yearStart)$|^@today[+-][0-9]+d$',
|
|
71
71
|
config: '^@resolve:[a-z][a-z0-9_-]*$|^@config\\.[A-Za-z][A-Za-z0-9_.]*\\??$',
|
|
72
72
|
object: '^@objectId$|^@object\\.[A-Za-z][A-Za-z0-9_]*$',
|
|
73
73
|
workspace: '^@workspace\\.[A-Za-z][A-Za-z0-9_]*\\??$',
|
|
@@ -117,7 +117,7 @@ export const SENTINEL_VOCABULARY = Object.freeze({
|
|
|
117
117
|
filter: {
|
|
118
118
|
resolver: 'resolveFilterTokens',
|
|
119
119
|
description: 'Relative fetch-time tokens (current user, the current user\'s groups as an IN list, relative dates).',
|
|
120
|
-
members: ['@me', '@me.displayName', '@me.email', '@myGroups', '@now', '@today', '@today±Nd', '@monthStart', '@quarterStart', '@yearStart'],
|
|
120
|
+
members: ['@me', '@me.displayName', '@me.email', '@myGroups', '@now', '@today', '@today±Nd', '@monthStart', '@monthEnd', '@nextMonthStart', '@quarterStart', '@yearStart'],
|
|
121
121
|
},
|
|
122
122
|
config: {
|
|
123
123
|
resolver: 'resolveManifestSentinels / resolveFilterTokens',
|