@conduction/nextcloud-vue 2.91.0 → 2.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +57 -4
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +44 -17
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +44 -2
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +140 -59
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +20 -6
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +3 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +10 -39
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +5 -3
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +6 -4
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +46 -0
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js +6 -2
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +82 -9
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js +104 -90
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js +9 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue.js.map +1 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +355 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +462 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +39 -10
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +418 -399
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
- package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue.js +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js +156 -3
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js +168 -52
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js +9 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue.js.map +1 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js +83 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue2.js.map +1 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js +119 -0
- package/dist/esm/components/CnModuleCard/CnModuleCard.vue3.js.map +1 -0
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +22 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +20 -14
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue.js +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js +2 -0
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue2.js.map +1 -1
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js +15 -5
- package/dist/esm/components/CnNotificationPreferences/CnNotificationPreferences.vue3.js.map +1 -1
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue.js +1 -1
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js +8 -30
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue2.js.map +1 -1
- package/dist/esm/components/CnRoadmapTab/CnRoadmapTab.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +33 -2
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue.js +1 -1
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js +63 -4
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js +19 -2
- package/dist/esm/components/CnTextWidget/CnTextWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +9 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js.map +1 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +164 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +93 -0
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +12 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +45 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +44 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +1182 -328
- package/dist/esm/utils/boardDate.js +35 -1
- package/dist/esm/utils/boardDate.js.map +1 -1
- package/dist/esm/utils/featureCatalog.js +135 -0
- package/dist/esm/utils/featureCatalog.js.map +1 -0
- package/dist/esm/utils/fillTemplate.js +84 -4
- package/dist/esm/utils/fillTemplate.js.map +1 -1
- package/dist/esm/utils/headerMeta.js +62 -12
- package/dist/esm/utils/headerMeta.js.map +1 -1
- package/dist/esm/utils/roadmapIssues.js +86 -0
- package/dist/esm/utils/roadmapIssues.js.map +1 -0
- package/dist/esm/utils/userDisplayName.js +47 -0
- package/dist/esm/utils/userDisplayName.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +15618 -13097
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1182 -328
- package/l10n/en.json +45 -1
- package/l10n/nl.json +44 -1
- package/package.json +1 -1
- package/src/components/CnAppRoot/CnAppRoot.md +2 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +66 -5
- package/src/components/CnChoiceCards/CnChoiceCards.md +27 -0
- package/src/components/CnChoiceCards/CnChoiceCards.vue +168 -3
- package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -6
- package/src/components/CnDataTable/CnDataTable.vue +10 -39
- package/src/components/CnDetailPage/CnDetailPage.vue +5 -3
- package/src/components/CnFactsWidget/CnFactsWidget.vue +6 -4
- package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +51 -1
- package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +98 -13
- package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +1053 -0
- package/src/components/CnFormDialog/CnFormDialog.md +1 -0
- package/src/components/CnFormDialog/CnFormDialog.vue +619 -562
- package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +338 -5
- package/src/components/CnModuleCard/CnModuleCard.vue +258 -0
- package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +23 -2
- package/src/components/CnNotificationPreferences/CnNotificationPreferences.vue +5 -0
- package/src/components/CnRoadmapTab/CnRoadmapTab.vue +6 -28
- package/src/components/CnTabsWidget/CnTabsWidget.vue +33 -2
- package/src/components/CnTextWidget/CnTextWidget.vue +89 -4
- package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +305 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +12 -0
- package/src/css/dialog.css +47 -0
- package/src/css/look-board-detail.css +8 -0
- package/src/css/look-board.css +33 -0
- package/src/schemas/app-manifest-v2.schema.json +67 -4
- package/src/utils/boardDate.js +34 -0
- package/src/utils/featureCatalog.js +132 -0
- package/src/utils/fillTemplate.js +83 -3
- package/src/utils/headerMeta.js +62 -12
- package/src/utils/roadmapIssues.js +82 -0
- package/src/utils/userDisplayName.js +54 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
package/src/css/dialog.css
CHANGED
|
@@ -203,3 +203,50 @@
|
|
|
203
203
|
.cn-dialog__confirm-pending {
|
|
204
204
|
opacity: 0.45;
|
|
205
205
|
}
|
|
206
|
+
|
|
207
|
+
/* On a phone the board dialog is a bottom sheet (board PqMobiel): full width,
|
|
208
|
+
anchored to the bottom, as tall as its content up to the screen less the
|
|
209
|
+
header, a 16px top radius and a grab bar. The footer buttons share the row. */
|
|
210
|
+
@media (max-width: 600px) {
|
|
211
|
+
.modal-mask.cn-dialog-board .modal-wrapper .modal-container,
|
|
212
|
+
.modal-mask.cn-dialog-board .modal-wrapper--normal > .modal-container {
|
|
213
|
+
position: absolute;
|
|
214
|
+
inset: auto 0 0;
|
|
215
|
+
width: 100%;
|
|
216
|
+
max-width: 100%;
|
|
217
|
+
height: auto;
|
|
218
|
+
max-height: calc(100% - var(--header-height, 50px));
|
|
219
|
+
border-radius: 16px 16px 0 0;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.modal-mask.cn-dialog-board .modal-container::before {
|
|
223
|
+
content: '';
|
|
224
|
+
display: block;
|
|
225
|
+
flex: none;
|
|
226
|
+
width: 40px;
|
|
227
|
+
height: 4px;
|
|
228
|
+
margin: 8px auto 0;
|
|
229
|
+
border-radius: 2px;
|
|
230
|
+
background: var(--color-border-dark);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.modal-mask.cn-dialog-board .modal-container {
|
|
234
|
+
flex-direction: column;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.cn-dialog-board .dialog {
|
|
238
|
+
height: auto;
|
|
239
|
+
min-height: 0;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.cn-dialog-board .dialog__content {
|
|
243
|
+
flex: 0 1 auto;
|
|
244
|
+
overflow-y: auto;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.cn-dialog-board .dialog__actions > .button-vue,
|
|
248
|
+
.cn-dialog-board .dialog__actions > * {
|
|
249
|
+
flex: 1 1 0;
|
|
250
|
+
min-height: 44px;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -660,3 +660,11 @@
|
|
|
660
660
|
.cn-look-board:has(.cn-detail-page) > .app-sidebar__toggle {
|
|
661
661
|
display: none;
|
|
662
662
|
}
|
|
663
|
+
|
|
664
|
+
/* Below 768px the header is a column (detail-page.css), where the 320px basis
|
|
665
|
+
above would be a 320px HEIGHT: the title sat in a band of white on a phone. */
|
|
666
|
+
@media (max-width: 768px) {
|
|
667
|
+
.cn-look-board .cn-detail-page__header--board > :first-child {
|
|
668
|
+
flex: none;
|
|
669
|
+
}
|
|
670
|
+
}
|
package/src/css/look-board.css
CHANGED
|
@@ -591,6 +591,39 @@
|
|
|
591
591
|
max-width: none;
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
+
/* A Nextcloud text field or text area in a board settings card has its label
|
|
595
|
+
above the control (14px/600), as the boards draw it (PqBeheer Leadbeheer,
|
|
596
|
+
PqBeheerKassa Btw), instead of floating inside the border. The label stays
|
|
597
|
+
the field's own `<label for>`; it moves out of the control and the field
|
|
598
|
+
makes room for it. With the label outside, the placeholder shows at rest.
|
|
599
|
+
@spec openspec/changes/settings-section-board-fields/specs/settings-board-look/spec.md#requirement-a-settings-card-labels-its-text-fields-above-the-control */
|
|
600
|
+
.cn-look-board .settings-section:not(.cn-look-nextcloud *) .input-field:has(.input-field__label),
|
|
601
|
+
.cn-look-board .settings-section:not(.cn-look-nextcloud *) .textarea:has(.textarea__label) {
|
|
602
|
+
margin-block-start: 26px;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.cn-look-board .settings-section:not(.cn-look-nextcloud *) .input-field__input + .input-field__label,
|
|
606
|
+
.cn-look-board .settings-section:not(.cn-look-nextcloud *) .textarea__input + .textarea__label {
|
|
607
|
+
inset-block-start: auto;
|
|
608
|
+
inset-block-end: calc(100% + 4px);
|
|
609
|
+
inset-inline: 0 auto;
|
|
610
|
+
max-width: 100%;
|
|
611
|
+
margin: 0;
|
|
612
|
+
padding: 0;
|
|
613
|
+
background: none;
|
|
614
|
+
color: var(--color-main-text);
|
|
615
|
+
font-size: 14px;
|
|
616
|
+
font-weight: 600;
|
|
617
|
+
line-height: 20px;
|
|
618
|
+
pointer-events: auto;
|
|
619
|
+
transition: none;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.cn-look-board .settings-section:not(.cn-look-nextcloud *) .input-field:has(.input-field__label) .input-field__input:not(:focus)::placeholder,
|
|
623
|
+
.cn-look-board .settings-section:not(.cn-look-nextcloud *) .textarea__input:not(:focus):not(.textarea__input--label-outside)::placeholder {
|
|
624
|
+
opacity: 1 !important;
|
|
625
|
+
}
|
|
626
|
+
|
|
594
627
|
/* The section footer is the save row: the hairline replaces its own rule. */
|
|
595
628
|
.cn-look-board .settings-section .cn-settings-section__footer {
|
|
596
629
|
margin: 0;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
|
|
4
4
|
"title": "Conduction App Manifest v2",
|
|
5
5
|
"description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.111.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -1651,6 +1651,59 @@
|
|
|
1651
1651
|
}
|
|
1652
1652
|
}
|
|
1653
1653
|
},
|
|
1654
|
+
"linkModule": {
|
|
1655
|
+
"type": "object",
|
|
1656
|
+
"required": ["id", "label"],
|
|
1657
|
+
"additionalProperties": false,
|
|
1658
|
+
"description": "One module on a type='links' page. The app decides whether it is in the menu (`enabled`, or `enabledIf` against runtime). Every card opens; a module that is off links to the admin settings (`settings`).",
|
|
1659
|
+
"properties": {
|
|
1660
|
+
"id": { "type": "string", "minLength": 1, "description": "Unique within the page's modules." },
|
|
1661
|
+
"label": { "type": "string", "minLength": 1, "description": "i18n key / the module's title." },
|
|
1662
|
+
"description": { "type": "string", "description": "i18n key / one or two lines under the title." },
|
|
1663
|
+
"icon": { "type": "string", "description": "MDI icon name." },
|
|
1664
|
+
"enabled": { "type": "boolean", "description": "Whether the module is in the menu. Wins over enabledIf." },
|
|
1665
|
+
"enabledIf": { "$ref": "#/$defs/visibleIfCondition", "description": "Condition, evaluated like visibleIf, that says the module is in the menu." },
|
|
1666
|
+
"route": { "type": "string", "minLength": 1, "description": "Where Open goes: a route name or a path starting with '/'. Defaults to the first page." },
|
|
1667
|
+
"params": { "type": "object", "additionalProperties": { "type": ["string", "number"] } },
|
|
1668
|
+
"query": { "type": "object", "additionalProperties": { "type": ["string", "number", "boolean"] } },
|
|
1669
|
+
"pages": {
|
|
1670
|
+
"type": "array",
|
|
1671
|
+
"description": "The module's pages, drawn as links and named in the 'N pages' line.",
|
|
1672
|
+
"items": {
|
|
1673
|
+
"type": "object",
|
|
1674
|
+
"required": ["label"],
|
|
1675
|
+
"additionalProperties": false,
|
|
1676
|
+
"anyOf": [{ "required": ["route"] }, { "required": ["href"] }],
|
|
1677
|
+
"properties": {
|
|
1678
|
+
"label": { "type": "string", "minLength": 1 },
|
|
1679
|
+
"route": { "type": "string", "minLength": 1 },
|
|
1680
|
+
"params": { "type": "object", "additionalProperties": { "type": ["string", "number"] } },
|
|
1681
|
+
"query": { "type": "object", "additionalProperties": { "type": ["string", "number", "boolean"] } },
|
|
1682
|
+
"href": { "type": "string", "minLength": 1 },
|
|
1683
|
+
"highlight": { "type": "boolean", "description": "Named in the line of a module with more than four pages ('15 pages, including ...')." }
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
},
|
|
1687
|
+
"settings": {
|
|
1688
|
+
"type": "object",
|
|
1689
|
+
"additionalProperties": false,
|
|
1690
|
+
"anyOf": [{ "required": ["route"] }, { "required": ["href"] }],
|
|
1691
|
+
"description": "Where an admin puts a module that is off in the menu (the admin settings), shown on its card as '<prefix> <label>'.",
|
|
1692
|
+
"properties": {
|
|
1693
|
+
"label": { "type": "string", "description": "i18n key / link text. Defaults to 'Admin settings'." },
|
|
1694
|
+
"prefix": { "type": "string", "description": "i18n key / text before the link. Defaults to 'Put it in the menu:'." },
|
|
1695
|
+
"route": { "type": "string", "minLength": 1 },
|
|
1696
|
+
"params": { "type": "object", "additionalProperties": { "type": ["string", "number"] } },
|
|
1697
|
+
"query": { "type": "object", "additionalProperties": { "type": ["string", "number", "boolean"] } },
|
|
1698
|
+
"href": { "type": "string", "minLength": 1 },
|
|
1699
|
+
"permission": { "type": "string" },
|
|
1700
|
+
"visibleIf": { "$ref": "#/$defs/visibleIfCondition" }
|
|
1701
|
+
}
|
|
1702
|
+
},
|
|
1703
|
+
"permission": { "type": "string", "description": "Permission the user must hold for the module to render." },
|
|
1704
|
+
"visibleIf": { "$ref": "#/$defs/visibleIfCondition", "description": "Display condition, evaluated the way a menu entry's is." }
|
|
1705
|
+
}
|
|
1706
|
+
},
|
|
1654
1707
|
"linkCard": {
|
|
1655
1708
|
"type": "object",
|
|
1656
1709
|
"required": ["id", "label"],
|
|
@@ -4232,10 +4285,16 @@
|
|
|
4232
4285
|
"greeting": {
|
|
4233
4286
|
"oneOf": [
|
|
4234
4287
|
{ "type": "boolean" },
|
|
4235
|
-
{ "type": "string", "enum": ["first", "full"] }
|
|
4288
|
+
{ "type": "string", "enum": ["first", "full", "date"] },
|
|
4289
|
+
{
|
|
4290
|
+
"type": "object",
|
|
4291
|
+
"required": ["label"],
|
|
4292
|
+
"additionalProperties": false,
|
|
4293
|
+
"properties": { "label": { "type": "string", "minLength": 1, "description": "i18n key drawn where the greeting would be." } }
|
|
4294
|
+
}
|
|
4236
4295
|
],
|
|
4237
4296
|
"default": false,
|
|
4238
|
-
"description": "type='dashboard': greet the reader in the header subtitle. `true` or \"first\" writes \"Good afternoon, Pieter · Monday 5 October 2026\", \"full\" uses the whole display name; the page `description` follows after another middle dot (screens-dashboard-greeting-header). Maps to CnDashboardPage `greeting`."
|
|
4297
|
+
"description": "type='dashboard': greet the reader in the header subtitle. `true` or \"first\" writes \"Good afternoon, Pieter · Monday 5 October 2026\", \"full\" uses the whole display name; the page `description` follows after another middle dot (screens-dashboard-greeting-header). Since 2.111.0 \"date\" leads with the date alone (\"Tuesday 7 October 2026 · ...\") and `{ \"label\": \"Customer contact\" }` puts the translated label where the greeting would be (dashboard-greeting-label). Maps to CnDashboardPage `greeting`."
|
|
4239
4298
|
},
|
|
4240
4299
|
"viewLinks": {
|
|
4241
4300
|
"type": "array",
|
|
@@ -4751,7 +4810,11 @@
|
|
|
4751
4810
|
"description": { "type": "string" },
|
|
4752
4811
|
"emptyLabel": { "type": "string" },
|
|
4753
4812
|
"categories": { "type": "object", "additionalProperties": { "type": "string", "minLength": 1 } },
|
|
4754
|
-
"cards": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/linkCard" } }
|
|
4813
|
+
"cards": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/linkCard" } },
|
|
4814
|
+
"modules": { "type": "array", "items": { "$ref": "#/$defs/linkModule" }, "description": "Module cards drawn above the link cards (board PqModules): per module an icon, title, in-the-menu/off badge, description, its pages, an Open button and, when off, a link to the admin settings." },
|
|
4815
|
+
"modulesTitle": { "type": "string", "description": "i18n key / heading of the modules section. Defaults to 'Modules'." },
|
|
4816
|
+
"footnote": { "type": "string", "description": "i18n key / a note under the module cards." },
|
|
4817
|
+
"groupStyle": { "type": "string", "enum": ["cards", "list"], "default": "cards", "description": "cards: a grid of link cards per group. list: one card per group with a row per link (icon, label, description, chevron or external-link icon)." }
|
|
4755
4818
|
}
|
|
4756
4819
|
}
|
|
4757
4820
|
},
|
package/src/utils/boardDate.js
CHANGED
|
@@ -88,6 +88,40 @@ export function formatBoardDate(value, now = new Date(), locale = boardDateLocal
|
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* A date with its short weekday, the way an agenda writes it: "wo 7 okt", or
|
|
93
|
+
* "wo 14 feb 2024" outside the current year. With `today` set, today's date
|
|
94
|
+
* reads "Vandaag, wo 7 okt".
|
|
95
|
+
*
|
|
96
|
+
* @param {unknown} value A Date, a timestamp or an ISO string.
|
|
97
|
+
* @param {Date} [now] The reference moment (defaults to the current time).
|
|
98
|
+
* @param {object} [options] Options.
|
|
99
|
+
* @param {boolean} [options.today] Put "Today, " before today's date.
|
|
100
|
+
* @param {string} [options.locale] BCP 47 tag (defaults to `boardDateLocale()`).
|
|
101
|
+
* @return {string} The date, or '' when the value is not a date.
|
|
102
|
+
* @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
|
|
103
|
+
*/
|
|
104
|
+
export function formatBoardWeekday(value, now = new Date(), options = {}) {
|
|
105
|
+
const date = parseDateValue(value)
|
|
106
|
+
if (date === null) {
|
|
107
|
+
return ''
|
|
108
|
+
}
|
|
109
|
+
const parts = { weekday: 'short', day: 'numeric', month: 'short' }
|
|
110
|
+
if (date.getFullYear() !== now.getFullYear()) {
|
|
111
|
+
parts.year = 'numeric'
|
|
112
|
+
}
|
|
113
|
+
let text
|
|
114
|
+
try {
|
|
115
|
+
text = new Intl.DateTimeFormat(options.locale || boardDateLocale(), parts).format(date)
|
|
116
|
+
} catch {
|
|
117
|
+
text = new Intl.DateTimeFormat(undefined, parts).format(date)
|
|
118
|
+
}
|
|
119
|
+
if (options.today === true && ageInDays(date, now) === 0) {
|
|
120
|
+
return t('nextcloud-vue', 'Today, {date}', { date: text })
|
|
121
|
+
}
|
|
122
|
+
return text
|
|
123
|
+
}
|
|
124
|
+
|
|
91
125
|
/**
|
|
92
126
|
* Whole calendar days from the date until `now` (0 today, 1 yesterday), or
|
|
93
127
|
* null when the value is not a date. A future date gives a negative number.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* featureCatalog: the feature list of the Features and roadmap page, read the
|
|
6
|
+
* way the board look draws it. A feature carries a status (available, beta,
|
|
7
|
+
* in progress or planned), an optional category and optional per-language
|
|
8
|
+
* titles (`title_nl`, `summary_nl`) next to the English ones.
|
|
9
|
+
*
|
|
10
|
+
* @module utils/featureCatalog
|
|
11
|
+
* @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The statuses, in the order the filter and the captions list them. */
|
|
15
|
+
export const FEATURE_STATUSES = Object.freeze(['available', 'beta', 'in_progress', 'planned'])
|
|
16
|
+
|
|
17
|
+
/** The badge variant of each status. */
|
|
18
|
+
export const FEATURE_STATUS_VARIANTS = Object.freeze({
|
|
19
|
+
available: 'success',
|
|
20
|
+
beta: 'warning',
|
|
21
|
+
in_progress: 'primary',
|
|
22
|
+
planned: 'purple',
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const STATUS_ALIASES = Object.freeze({
|
|
26
|
+
available: 'available',
|
|
27
|
+
stable: 'available',
|
|
28
|
+
done: 'available',
|
|
29
|
+
released: 'available',
|
|
30
|
+
shipped: 'available',
|
|
31
|
+
beta: 'beta',
|
|
32
|
+
preview: 'beta',
|
|
33
|
+
in_progress: 'in_progress',
|
|
34
|
+
'in-progress': 'in_progress',
|
|
35
|
+
inprogress: 'in_progress',
|
|
36
|
+
building: 'in_progress',
|
|
37
|
+
development: 'in_progress',
|
|
38
|
+
planned: 'planned',
|
|
39
|
+
roadmap: 'planned',
|
|
40
|
+
proposed: 'planned',
|
|
41
|
+
soon: 'planned',
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The status key of a feature. A feature without a status is one the app
|
|
46
|
+
* ships, as every entry of the list was before statuses existed.
|
|
47
|
+
*
|
|
48
|
+
* @param {unknown} raw The feature's `status`.
|
|
49
|
+
* @return {string} One of FEATURE_STATUSES.
|
|
50
|
+
* @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md#requirement-a-feature-has-one-of-four-statuses
|
|
51
|
+
*/
|
|
52
|
+
export function featureStatus(raw) {
|
|
53
|
+
if (typeof raw !== 'string' || raw.trim() === '') {
|
|
54
|
+
return 'available'
|
|
55
|
+
}
|
|
56
|
+
return STATUS_ALIASES[raw.trim().toLowerCase()] || 'available'
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A text field of a feature in the reader's language: `title_nl` for a Dutch
|
|
61
|
+
* reader when the feature has one, else `title`.
|
|
62
|
+
*
|
|
63
|
+
* @param {object} feature The feature.
|
|
64
|
+
* @param {string} field `title` or `summary`.
|
|
65
|
+
* @param {string} [language] The reader's language tag (`nl`, `nl_NL`, `de-DE`).
|
|
66
|
+
* @return {string} The text, or ''.
|
|
67
|
+
* @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md#requirement-a-feature-reads-in-the-readers-language
|
|
68
|
+
*/
|
|
69
|
+
export function featureText(feature, field, language = '') {
|
|
70
|
+
if (!feature || typeof feature !== 'object') {
|
|
71
|
+
return ''
|
|
72
|
+
}
|
|
73
|
+
const base = String(language || '').split(/[-_]/)[0].toLowerCase()
|
|
74
|
+
const own = base ? feature[`${field}_${base}`] : undefined
|
|
75
|
+
if (typeof own === 'string' && own.trim() !== '') {
|
|
76
|
+
return own
|
|
77
|
+
}
|
|
78
|
+
return typeof feature[field] === 'string' ? feature[field] : ''
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* How many features carry each status.
|
|
83
|
+
*
|
|
84
|
+
* @param {Array<{status: string}>} features Features with a normalised status.
|
|
85
|
+
* @return {{all: number, available: number, beta: number, in_progress: number, planned: number}} The counts.
|
|
86
|
+
* @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md#requirement-the-status-filter-shows-a-count-per-status
|
|
87
|
+
*/
|
|
88
|
+
export function countByStatus(features) {
|
|
89
|
+
const counts = { all: features.length, available: 0, beta: 0, in_progress: 0, planned: 0 }
|
|
90
|
+
for (const feature of features) {
|
|
91
|
+
counts[feature.status] += 1
|
|
92
|
+
}
|
|
93
|
+
return counts
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The features in the reader's language, grouped by category. Groups follow
|
|
98
|
+
* the order of `categories`; features without a known category come first,
|
|
99
|
+
* under no caption. Inside a group the features keep their given order.
|
|
100
|
+
*
|
|
101
|
+
* @param {Array<object>} features The raw features.
|
|
102
|
+
* @param {object} [categories] Category key to caption.
|
|
103
|
+
* @param {string} [language] The reader's language tag.
|
|
104
|
+
* @return {Array<{key: string, label: string, features: Array<object>}>} The groups; a group without features is left out.
|
|
105
|
+
* @spec openspec/changes/screens-roadmap-board-look/specs/features-roadmap-board-look/spec.md#requirement-features-are-grouped-by-category
|
|
106
|
+
*/
|
|
107
|
+
export function groupFeatures(features, categories = {}, language = '') {
|
|
108
|
+
const captions = categories && typeof categories === 'object' && !Array.isArray(categories) ? categories : {}
|
|
109
|
+
const groups = [{ key: '', label: '', features: [] }]
|
|
110
|
+
const byKey = { '': groups[0] }
|
|
111
|
+
for (const [key, label] of Object.entries(captions)) {
|
|
112
|
+
const group = { key, label: String(label), features: [] }
|
|
113
|
+
groups.push(group)
|
|
114
|
+
byKey[key] = group
|
|
115
|
+
}
|
|
116
|
+
for (const raw of Array.isArray(features) ? features : []) {
|
|
117
|
+
if (!raw || typeof raw !== 'object') {
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
const feature = {
|
|
121
|
+
slug: String(raw.slug || raw.id || ''),
|
|
122
|
+
title: featureText(raw, 'title', language) || String(raw.slug || ''),
|
|
123
|
+
summary: featureText(raw, 'summary', language),
|
|
124
|
+
status: featureStatus(raw.status),
|
|
125
|
+
expected: typeof raw.expected === 'string' ? raw.expected : '',
|
|
126
|
+
docsUrl: typeof raw.docsUrl === 'string' ? raw.docsUrl : '',
|
|
127
|
+
}
|
|
128
|
+
const group = byKey[typeof raw.category === 'string' ? raw.category : ''] || byKey['']
|
|
129
|
+
group.features.push(feature)
|
|
130
|
+
}
|
|
131
|
+
return groups.filter((group) => group.features.length > 0)
|
|
132
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
2
2
|
// SPDX-License-Identifier: EUPL-1.2
|
|
3
3
|
|
|
4
|
-
import { boardDateLocale, formatBoardDate, formatRelativeDate } from './boardDate.js'
|
|
4
|
+
import { boardDateLocale, formatBoardDate, formatBoardWeekday, formatRelativeDate, formatTimeOfDay } from './boardDate.js'
|
|
5
5
|
import { parseDateValue } from './dateVariant.js'
|
|
6
6
|
import { formatMoney } from './formatMetric.js'
|
|
7
7
|
import { readPath } from './readPath.js'
|
|
8
|
+
import { userDisplayName } from './userDisplayName.js'
|
|
8
9
|
|
|
9
10
|
const TOKEN = /\{([^{}\s|]+)(?:\|([a-z]+)(?::([^{}\s]+))?)?\}/g
|
|
10
11
|
|
|
@@ -32,11 +33,37 @@ export function formatBoardDateTime(value, now = new Date()) {
|
|
|
32
33
|
return `${day}, ${time}`
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* A date with the month written out: "7 oktober", or "6 oktober 2026"
|
|
38
|
+
* outside the current year or when `withYear` asks for it.
|
|
39
|
+
*
|
|
40
|
+
* @param {unknown} value A Date, a timestamp or an ISO string.
|
|
41
|
+
* @param {Date} now The reference moment.
|
|
42
|
+
* @param {boolean} [withYear] Always write the year.
|
|
43
|
+
* @return {string} The text, or '' when the value is not a date.
|
|
44
|
+
* @spec openspec/changes/screens-header-meta-tokens/specs/detail-page-board-look/spec.md#requirement-the-header-meta-line-formats-users-long-dates-and-enum-labels
|
|
45
|
+
*/
|
|
46
|
+
export function formatLongDate(value, now = new Date(), withYear = false) {
|
|
47
|
+
const date = parseDateValue(value)
|
|
48
|
+
if (date === null) {
|
|
49
|
+
return ''
|
|
50
|
+
}
|
|
51
|
+
const options = { day: 'numeric', month: 'long' }
|
|
52
|
+
if (withYear || date.getFullYear() !== now.getFullYear()) {
|
|
53
|
+
options.year = 'numeric'
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
return new Intl.DateTimeFormat(boardDateLocale(), options).format(date)
|
|
57
|
+
} catch {
|
|
58
|
+
return new Intl.DateTimeFormat(undefined, options).format(date)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
35
62
|
/**
|
|
36
63
|
* Format one token value.
|
|
37
64
|
*
|
|
38
65
|
* @param {unknown} value The raw value.
|
|
39
|
-
* @param {string} format `date`, `datetime`, `relative`, `relativetime`, `days` or '' (as is).
|
|
66
|
+
* @param {string} format `date`, `datetime`, `weekday`, `longdate`, `time`, `user`, `currency`, `relative`, `relativetime`, `days` or '' (as is).
|
|
40
67
|
* @param {Date} now The reference moment.
|
|
41
68
|
* @param {string} [arg] The format's argument (`currency:EUR`, or a row path holding the code).
|
|
42
69
|
* @param {object|null} [object] The row, for a currency read from a field.
|
|
@@ -53,6 +80,19 @@ function formatValue(value, format, now, arg = '', object = null, config = null)
|
|
|
53
80
|
if (format === 'datetime') {
|
|
54
81
|
return formatBoardDateTime(value, now)
|
|
55
82
|
}
|
|
83
|
+
if (format === 'weekday') {
|
|
84
|
+
return formatBoardWeekday(value, now, { today: arg === 'today' })
|
|
85
|
+
}
|
|
86
|
+
if (format === 'longdate') {
|
|
87
|
+
return formatLongDate(value, now, arg === 'year')
|
|
88
|
+
}
|
|
89
|
+
if (format === 'user') {
|
|
90
|
+
return userDisplayName(value)
|
|
91
|
+
}
|
|
92
|
+
if (format === 'time') {
|
|
93
|
+
const date = parseDateValue(value)
|
|
94
|
+
return date === null ? '' : formatTimeOfDay(date)
|
|
95
|
+
}
|
|
56
96
|
if (format === 'currency') {
|
|
57
97
|
// `{amount|currency:EUR}` names the code, `{amount|currency:price.code}`
|
|
58
98
|
// reads it from the row, `{amount|currency}` is the app's reporting
|
|
@@ -79,7 +119,12 @@ function formatValue(value, format, now, arg = '', object = null, config = null)
|
|
|
79
119
|
* `"Termijn {deadline|date}"` from an object.
|
|
80
120
|
*
|
|
81
121
|
* Every `{path}` token is a dot path into the object; `{path|date}` and
|
|
82
|
-
* `{path|datetime}` format it as the boards write dates, `{path|
|
|
122
|
+
* `{path|datetime}` format it as the boards write dates, `{path|weekday}` adds
|
|
123
|
+
* the short weekday ("wo 7 okt"; `{path|weekday:today}` reads "Vandaag, wo 7 okt"
|
|
124
|
+
* for today), `{path|longdate}` writes the month out ("7 oktober";
|
|
125
|
+
* `{path|longdate:year}` always adds the year), `{path|time}` is the time
|
|
126
|
+
* of day ("08:45"), `{path|user}` is a user id's display name (looked up
|
|
127
|
+
* once, '' until it arrives), `{path|currency}`,
|
|
83
128
|
* `{path|currency:EUR}` and `{path|currency:field}` write an amount in a
|
|
84
129
|
* currency (a code, or the row field that holds it; without either, or when
|
|
85
130
|
* the row field is empty, the app's reporting currency), `{path|relative}` and
|
|
@@ -138,3 +183,38 @@ export function firstFilled(templates, object, translate = (key) => key, now = n
|
|
|
138
183
|
}
|
|
139
184
|
return ''
|
|
140
185
|
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Fill a template such as `"{identifier} · {requester}"` field by field
|
|
189
|
+
* (a table column's `secondary` line, a detail page's meta line).
|
|
190
|
+
*
|
|
191
|
+
* A field without a value takes its separator with it: the text between two
|
|
192
|
+
* fields belongs to the field after it, so an empty requester leaves
|
|
193
|
+
* "2026-0002", not "2026-0002 ·", and an empty first field does not leave a
|
|
194
|
+
* leading separator either. Text before the first field and after the last
|
|
195
|
+
* one is kept as written. A template whose fields all have values fills
|
|
196
|
+
* exactly as before.
|
|
197
|
+
*
|
|
198
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-empty-field-takes-its-separator-with-it
|
|
199
|
+
* @param {string} template The template, with `{field}` placeholders.
|
|
200
|
+
* @param {(key: string) => string} valueOf The text of a field ('' when empty).
|
|
201
|
+
* @return {string} The filled template.
|
|
202
|
+
*/
|
|
203
|
+
export function joinTemplateParts(template, valueOf) {
|
|
204
|
+
// split() with a capture group alternates literal, field, literal, …
|
|
205
|
+
const parts = template.split(/\{([^}]+)\}/)
|
|
206
|
+
const lead = parts[0]
|
|
207
|
+
const tail = parts[parts.length - 1]
|
|
208
|
+
let body = ''
|
|
209
|
+
for (let i = 1; i < parts.length; i += 2) {
|
|
210
|
+
const value = valueOf(parts[i].trim())
|
|
211
|
+
if (value === '') {
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
// The literal before this field: the separator, unless nothing has
|
|
215
|
+
// been written yet (then there is nothing to separate from).
|
|
216
|
+
const separator = i === 1 ? '' : parts[i - 1]
|
|
217
|
+
body += (body === '' ? '' : separator) + value
|
|
218
|
+
}
|
|
219
|
+
return lead + body + tail
|
|
220
|
+
}
|
package/src/utils/headerMeta.js
CHANGED
|
@@ -1,27 +1,77 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
2
2
|
// SPDX-License-Identifier: EUPL-1.2
|
|
3
3
|
|
|
4
|
-
import { fillTemplate } from './fillTemplate.js'
|
|
4
|
+
import { fillTemplate, joinTemplateParts } from './fillTemplate.js'
|
|
5
|
+
import { readPath } from './readPath.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The text of one plain `{path}` token: an enum field reads as its label
|
|
9
|
+
* (`x-enum-labels` / `enumLabels` on the schema property, through the
|
|
10
|
+
* translate function), anything else as the stored value.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} object The record.
|
|
13
|
+
* @param {string} path The dotted path.
|
|
14
|
+
* @param {object|null} schema The record's schema.
|
|
15
|
+
* @param {(text: string) => string} translate The translate function.
|
|
16
|
+
* @return {string} The text, or '' when the field is empty.
|
|
17
|
+
*/
|
|
18
|
+
function plainTokenText(object, path, schema, translate) {
|
|
19
|
+
const value = readPath(object, path)
|
|
20
|
+
if (value === null || value === undefined || value === '' || typeof value === 'object') {
|
|
21
|
+
return ''
|
|
22
|
+
}
|
|
23
|
+
const property = schema && schema.properties && !path.includes('.') ? schema.properties[path] : null
|
|
24
|
+
const labels = property && (property.enumLabels || property['x-enum-labels'])
|
|
25
|
+
if (labels && typeof labels === 'object' && typeof labels[String(value)] === 'string') {
|
|
26
|
+
return translate(labels[String(value)])
|
|
27
|
+
}
|
|
28
|
+
return String(value)
|
|
29
|
+
}
|
|
5
30
|
|
|
6
31
|
/**
|
|
7
32
|
* Fill a header meta template such as `"via {channel}"` or
|
|
8
|
-
* `"
|
|
33
|
+
* `"{amount|currency:EUR} · {cashier|user} · {at|longdate} {at|time}"` from
|
|
34
|
+
* a record.
|
|
9
35
|
*
|
|
10
36
|
* Every `{path}` token (a dotted path into the record) is replaced by its
|
|
11
|
-
* value
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
37
|
+
* value, an enum field by its label. `{path|format}` tokens format as a
|
|
38
|
+
* record-list row does (the shared `fillTemplate`: date, datetime, weekday,
|
|
39
|
+
* longdate, time, user, currency, relative, relativetime, days). A token
|
|
40
|
+
* without a value drops itself and the separator before it, so a missing
|
|
41
|
+
* cashier leaves "€ 96,41 · 7 oktober"; a line whose every token is empty
|
|
42
|
+
* is '' ("via " is worse than nothing). A template without tokens is
|
|
43
|
+
* returned as written.
|
|
16
44
|
*
|
|
17
45
|
* @param {string} template The template, already translated.
|
|
18
46
|
* @param {object|null} object The record.
|
|
19
|
-
* @param {Date} [
|
|
20
|
-
* @return {string} The line, or '' when
|
|
21
|
-
* @spec openspec/changes/screens-
|
|
47
|
+
* @param {Date|{now?: Date, schema?: object, translate?: Function, config?: object}} [options] The reference moment, or options: `schema` for enum labels, `translate` for those labels, `config` for the reporting currency.
|
|
48
|
+
* @return {string} The line, or '' when nothing can be filled.
|
|
49
|
+
* @spec openspec/changes/screens-header-meta-tokens/specs/detail-page-board-look/spec.md#requirement-the-header-meta-line-formats-users-long-dates-and-enum-labels
|
|
22
50
|
*/
|
|
23
|
-
export function resolveHeaderMeta(template, object,
|
|
24
|
-
|
|
51
|
+
export function resolveHeaderMeta(template, object, options = {}) {
|
|
52
|
+
const settings = options instanceof Date ? { now: options } : (options || {})
|
|
53
|
+
const now = settings.now || new Date()
|
|
54
|
+
const translate = typeof settings.translate === 'function' ? settings.translate : (text) => text
|
|
55
|
+
if (typeof template !== 'string' || template.trim() === '') {
|
|
56
|
+
return ''
|
|
57
|
+
}
|
|
58
|
+
if (!/\{[^}]+\}/.test(template)) {
|
|
59
|
+
return template.trim()
|
|
60
|
+
}
|
|
61
|
+
if (!object || typeof object !== 'object') {
|
|
62
|
+
return ''
|
|
63
|
+
}
|
|
64
|
+
let filled = 0
|
|
65
|
+
const line = joinTemplateParts(template, (token) => {
|
|
66
|
+
const text = token.includes('|')
|
|
67
|
+
? fillTemplate(`{${token}}`, object, now, settings.config ? { config: settings.config } : null)
|
|
68
|
+
: plainTokenText(object, token, settings.schema || null, translate)
|
|
69
|
+
if (text !== '') {
|
|
70
|
+
filled += 1
|
|
71
|
+
}
|
|
72
|
+
return text
|
|
73
|
+
})
|
|
74
|
+
return filled === 0 ? '' : line.trim()
|
|
25
75
|
}
|
|
26
76
|
|
|
27
77
|
/**
|