@conduction/nextcloud-vue 2.92.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 +2 -36
- 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 +21 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +402 -391
- 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/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/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 +24 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +24 -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 +887 -247
- package/dist/esm/utils/featureCatalog.js +135 -0
- package/dist/esm/utils/featureCatalog.js.map +1 -0
- package/dist/esm/utils/fillTemplate.js +74 -3
- 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 +14820 -12966
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +887 -247
- package/l10n/en.json +24 -1
- package/l10n/nl.json +24 -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 +2 -36
- 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.vue +606 -562
- 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/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 +9 -3
- package/src/utils/featureCatalog.js +132 -0
- package/src/utils/fillTemplate.js +73 -2
- 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/l10n/en.json
CHANGED
|
@@ -1494,7 +1494,30 @@
|
|
|
1494
1494
|
"{on} of %n are in the menu"
|
|
1495
1495
|
],
|
|
1496
1496
|
"Put it in the menu:": "Put it in the menu:",
|
|
1497
|
-
"Admin settings": "Admin settings"
|
|
1497
|
+
"Admin settings": "Admin settings",
|
|
1498
|
+
"Support this app": "Support this app",
|
|
1499
|
+
"No features with this status": "No features with this status",
|
|
1500
|
+
"Learn more about {title}": "Learn more about {title}",
|
|
1501
|
+
"Learn more": "Learn more",
|
|
1502
|
+
"Next on the roadmap": "Next on the roadmap",
|
|
1503
|
+
"Coming next": "Coming next",
|
|
1504
|
+
"The most requested items from the issue tracker.": "The most requested items from the issue tracker.",
|
|
1505
|
+
"Opened {date}": "Opened {date}",
|
|
1506
|
+
"See the whole roadmap": "See the whole roadmap",
|
|
1507
|
+
"Features and roadmap": "Features and roadmap",
|
|
1508
|
+
"Beta": "Beta",
|
|
1509
|
+
"Planned": "Planned",
|
|
1510
|
+
"{status}, {expected}": "{status}, {expected}",
|
|
1511
|
+
"Build on this app": "Build on this app",
|
|
1512
|
+
"AI skills": "AI skills",
|
|
1513
|
+
"%n feature": [
|
|
1514
|
+
"%n feature",
|
|
1515
|
+
"%n features"
|
|
1516
|
+
],
|
|
1517
|
+
"Your account": "Your account",
|
|
1518
|
+
"Only for you. Colleagues keep their own settings.": "Only for you. Colleagues keep their own settings.",
|
|
1519
|
+
"Sections of the settings": "Sections of the settings",
|
|
1520
|
+
"Changes are saved straight away": "Changes are saved straight away"
|
|
1498
1521
|
},
|
|
1499
1522
|
"plurals": {
|
|
1500
1523
|
"{count} selected": [
|
package/l10n/nl.json
CHANGED
|
@@ -1680,7 +1680,30 @@
|
|
|
1680
1680
|
"{on} van %n staat in het menu",
|
|
1681
1681
|
"{on} van %n staan in het menu"
|
|
1682
1682
|
],
|
|
1683
|
-
"Put it in the menu:": "In het menu zetten:"
|
|
1683
|
+
"Put it in the menu:": "In het menu zetten:",
|
|
1684
|
+
"Support this app": "Steun deze app",
|
|
1685
|
+
"No features with this status": "Geen functies met deze status",
|
|
1686
|
+
"Learn more about {title}": "Lees meer over {title}",
|
|
1687
|
+
"Learn more": "Lees meer",
|
|
1688
|
+
"Next on the roadmap": "Hierna op de roadmap",
|
|
1689
|
+
"Coming next": "Hierna",
|
|
1690
|
+
"The most requested items from the issue tracker.": "De meest gevraagde items uit de issuetracker.",
|
|
1691
|
+
"Opened {date}": "Geopend {date}",
|
|
1692
|
+
"See the whole roadmap": "Bekijk de hele roadmap",
|
|
1693
|
+
"Features and roadmap": "Functies en roadmap",
|
|
1694
|
+
"Beta": "Beta",
|
|
1695
|
+
"Planned": "Gepland",
|
|
1696
|
+
"{status}, {expected}": "{status}, {expected}",
|
|
1697
|
+
"Build on this app": "Bouw verder op deze app",
|
|
1698
|
+
"AI skills": "AI-vaardigheden",
|
|
1699
|
+
"%n feature": [
|
|
1700
|
+
"%n functie",
|
|
1701
|
+
"%n functies"
|
|
1702
|
+
],
|
|
1703
|
+
"Your account": "Uw account",
|
|
1704
|
+
"Only for you. Colleagues keep their own settings.": "Alleen voor jou. Collega's houden hun eigen instellingen.",
|
|
1705
|
+
"Sections of the settings": "Onderdelen van de instellingen",
|
|
1706
|
+
"Changes are saved straight away": "Wijzigingen worden meteen bewaard"
|
|
1684
1707
|
},
|
|
1685
1708
|
"plurals": {
|
|
1686
1709
|
"{count} selected": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.93.0",
|
|
4
4
|
"description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
|
|
5
5
|
"homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -85,6 +85,8 @@ With loading state, custom components, translate, permissions, and custom page t
|
|
|
85
85
|
| `permissions` | Array | No | `[]` | Permission strings for the current user; forwarded to `CnAppNav` for menu filtering |
|
|
86
86
|
| `pageTypes` | Object | No | `null` | Page-type registry map (`type → component`). Falls back to the library's `defaultPageTypes`. Extend with `{ ...defaultPageTypes, report: MyReportPage }` |
|
|
87
87
|
| `userSettingsTitle` | String | No | `''` | Title shown at the top of the hosted `NcAppSettingsDialog`. Empty (the default) resolves to "User settings" from the library catalogue ("Persoonlijke instellingen" in Dutch), then the app's `translate`. Override per app to brand the modal (e.g. `'Decidiq preferences'`). |
|
|
88
|
+
| `userSettingsEyebrow` | String | No | `''` | Board look: the line above the user-settings title. Empty uses "Your account" ("Uw account"). Goes through the app's `translate`. |
|
|
89
|
+
| `userSettingsSubtitle` | String | No | `''` | Board look: the sentence under the user-settings title. Empty uses "Only for you. Colleagues keep their own settings." Goes through the app's `translate`. |
|
|
88
90
|
| `userSettingsSections` | Array | No | `null` | The user-settings sections to draw, in order: `app` (the app's slot content), `notifications`, `credentials`, `walkthrough`, `integrations`. A key left out is not drawn. `null` keeps the defaults: every section after `#user-settings-sections` content, or only the `#user-settings` content and the integrations section. |
|
|
89
91
|
| `adminSettingsTitle` | String | No | `''` | Title shown at the top of the admin-settings `NcAppSettingsDialog`. Empty (the default) resolves to `translate('Administration')`. Override per app (e.g. `'Pipelinq administration'`). |
|
|
90
92
|
| `requiresApps` | Array | No | `['openregister']` | App ids that MUST be installed for the host app to function. Checked against the OCS capabilities API on mount. When any required app is missing, CnAppRoot renders the empty state from the `or-missing` slot (default: `<NcEmptyContent>`) instead of the renderer. Pass `[]` to opt out (e.g. launchpad, the docs/styleguide app) — useful for any consumer that does not depend on OpenRegister. |
|
|
@@ -550,10 +550,9 @@
|
|
|
550
550
|
integrations section are drawn, unless `userSettingsSections`
|
|
551
551
|
names more.
|
|
552
552
|
-->
|
|
553
|
-
<
|
|
554
|
-
:
|
|
555
|
-
|
|
556
|
-
:name="resolvedUserSettingsTitle"
|
|
553
|
+
<component
|
|
554
|
+
:is="resolvedLook === 'board' ? 'CnUserSettingsDialog' : 'NcAppSettingsDialog'"
|
|
555
|
+
v-bind="userSettingsDialogProps"
|
|
557
556
|
@update:open="userSettingsOpen = $event">
|
|
558
557
|
<template v-for="section in userSettingsSectionOrder()" :key="section">
|
|
559
558
|
<template v-if="section === 'app'">
|
|
@@ -573,6 +572,9 @@
|
|
|
573
572
|
<NcAppSettingsSection v-else-if="section === 'credentials' && userSettingsOpen"
|
|
574
573
|
id="credentials"
|
|
575
574
|
:name="libraryCopy('Credentials')">
|
|
575
|
+
<template v-if="resolvedLook === 'board'" #icon>
|
|
576
|
+
<KeyOutline :size="18" />
|
|
577
|
+
</template>
|
|
576
578
|
<CnCredentials
|
|
577
579
|
scope="personal"
|
|
578
580
|
:appId="appId"
|
|
@@ -587,6 +589,9 @@
|
|
|
587
589
|
<NcAppSettingsSection v-else-if="section === 'walkthrough' && walkthroughEnabled"
|
|
588
590
|
id="cn-walkthrough"
|
|
589
591
|
:name="restartWalkthroughSectionName">
|
|
592
|
+
<template v-if="resolvedLook === 'board'" #icon>
|
|
593
|
+
<Play :size="18" />
|
|
594
|
+
</template>
|
|
590
595
|
<p class="cn-app-root__walkthrough-hint">
|
|
591
596
|
{{ restartWalkthroughHint }}
|
|
592
597
|
</p>
|
|
@@ -628,6 +633,9 @@
|
|
|
628
633
|
<NcAppSettingsSection v-else-if="section === 'integrations' && userSettingsOpen && integrationEntries.length"
|
|
629
634
|
id="cn-integrations"
|
|
630
635
|
:name="translate('Integrations')">
|
|
636
|
+
<template v-if="resolvedLook === 'board'" #icon>
|
|
637
|
+
<OpenInNew :size="18" />
|
|
638
|
+
</template>
|
|
631
639
|
<p class="cn-app-root__integrations-hint">
|
|
632
640
|
{{ libraryCopy('These open in another app.') }}
|
|
633
641
|
</p>
|
|
@@ -653,7 +661,7 @@
|
|
|
653
661
|
</ul>
|
|
654
662
|
</NcAppSettingsSection>
|
|
655
663
|
</template>
|
|
656
|
-
</
|
|
664
|
+
</component>
|
|
657
665
|
|
|
658
666
|
<!--
|
|
659
667
|
ADR-079 Step 2: the generic admin-settings modal has been REMOVED.
|
|
@@ -693,6 +701,7 @@ import { generateUrl } from '@nextcloud/router'
|
|
|
693
701
|
import { NcAppContent, NcAppSettingsDialog, NcAppSettingsSection, NcButton, NcContent, NcEmptyContent, NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
|
|
694
702
|
import { computed, reactive, shallowRef, watch } from 'vue'
|
|
695
703
|
import DatabaseSearchOutline from 'vue-material-design-icons/DatabaseSearchOutline.vue'
|
|
704
|
+
import KeyOutline from 'vue-material-design-icons/KeyOutline.vue'
|
|
696
705
|
import OpenInNew from 'vue-material-design-icons/OpenInNew.vue'
|
|
697
706
|
import Play from 'vue-material-design-icons/Play.vue'
|
|
698
707
|
import Restart from 'vue-material-design-icons/Restart.vue'
|
|
@@ -709,6 +718,7 @@ import CnObjectSidebar from '../CnObjectSidebar/CnObjectSidebar.vue'
|
|
|
709
718
|
import CnSetupWizard from '../CnSetupWizard/CnSetupWizard.vue'
|
|
710
719
|
import CnSupportDialog from '../CnSupportDialog/CnSupportDialog.vue'
|
|
711
720
|
import CnTenantBadge from '../CnTenantBadge/CnTenantBadge.vue'
|
|
721
|
+
import CnUserSettingsDialog from '../CnUserSettingsDialog/CnUserSettingsDialog.vue'
|
|
712
722
|
import CnWalkthrough from '../CnWalkthrough/CnWalkthrough.vue'
|
|
713
723
|
import { DEFAULT_CHAT_APP_ID } from '../../composables/aiChatConfig.js'
|
|
714
724
|
import { useAppInstaller } from '../../composables/useAppInstaller.js'
|
|
@@ -818,6 +828,7 @@ export default {
|
|
|
818
828
|
NcLoadingIcon,
|
|
819
829
|
NcNoteCard,
|
|
820
830
|
DatabaseSearchOutline,
|
|
831
|
+
KeyOutline,
|
|
821
832
|
OpenInNew,
|
|
822
833
|
Play,
|
|
823
834
|
Restart,
|
|
@@ -832,6 +843,7 @@ export default {
|
|
|
832
843
|
CnObjectSidebar,
|
|
833
844
|
CnSupportDialog,
|
|
834
845
|
CnNotificationPreferences,
|
|
846
|
+
CnUserSettingsDialog,
|
|
835
847
|
CnCredentials,
|
|
836
848
|
CnTenantBadge,
|
|
837
849
|
},
|
|
@@ -1648,6 +1660,29 @@ export default {
|
|
|
1648
1660
|
default: '',
|
|
1649
1661
|
},
|
|
1650
1662
|
|
|
1663
|
+
/**
|
|
1664
|
+
* Board look: the line above the user-settings title. Empty uses
|
|
1665
|
+
* the library's "Your account".
|
|
1666
|
+
*
|
|
1667
|
+
* @type {string}
|
|
1668
|
+
*/
|
|
1669
|
+
userSettingsEyebrow: {
|
|
1670
|
+
type: String,
|
|
1671
|
+
default: '',
|
|
1672
|
+
},
|
|
1673
|
+
|
|
1674
|
+
/**
|
|
1675
|
+
* Board look: the sentence under the user-settings title. Empty
|
|
1676
|
+
* uses the library's "Only for you. Colleagues keep their own
|
|
1677
|
+
* settings."
|
|
1678
|
+
*
|
|
1679
|
+
* @type {string}
|
|
1680
|
+
*/
|
|
1681
|
+
userSettingsSubtitle: {
|
|
1682
|
+
type: String,
|
|
1683
|
+
default: '',
|
|
1684
|
+
},
|
|
1685
|
+
|
|
1651
1686
|
/**
|
|
1652
1687
|
* The sections of the user-settings modal, in the order they are
|
|
1653
1688
|
* drawn. Keys: `app` (the content of the `#user-settings-sections`
|
|
@@ -3065,6 +3100,32 @@ export default {
|
|
|
3065
3100
|
return this.userSettingsTitle || this.libraryCopy('User settings')
|
|
3066
3101
|
},
|
|
3067
3102
|
|
|
3103
|
+
/**
|
|
3104
|
+
* The props of the user-settings dialog: `NcAppSettingsDialog`'s, or
|
|
3105
|
+
* under the board look CnUserSettingsDialog's (eyebrow, subtitle and
|
|
3106
|
+
* the app's name for the footer note).
|
|
3107
|
+
*
|
|
3108
|
+
* @spec openspec/changes/user-settings-board-dialog/specs/user-settings-board-dialog/spec.md#requirement-the-user-settings-dialog-takes-the-board-look
|
|
3109
|
+
* @return {object}
|
|
3110
|
+
*/
|
|
3111
|
+
userSettingsDialogProps() {
|
|
3112
|
+
if (this.resolvedLook !== 'board') {
|
|
3113
|
+
return { open: this.userSettingsOpen, showNavigation: true, name: this.resolvedUserSettingsTitle }
|
|
3114
|
+
}
|
|
3115
|
+
const props = {
|
|
3116
|
+
open: this.userSettingsOpen,
|
|
3117
|
+
title: this.resolvedUserSettingsTitle,
|
|
3118
|
+
appName: this.appDisplayName || (this.manifest && this.manifest.name) || this.appId || '',
|
|
3119
|
+
}
|
|
3120
|
+
if (this.userSettingsEyebrow) {
|
|
3121
|
+
props.eyebrow = this.translate(this.userSettingsEyebrow)
|
|
3122
|
+
}
|
|
3123
|
+
if (this.userSettingsSubtitle) {
|
|
3124
|
+
props.subtitle = this.translate(this.userSettingsSubtitle)
|
|
3125
|
+
}
|
|
3126
|
+
return props
|
|
3127
|
+
},
|
|
3128
|
+
|
|
3068
3129
|
/**
|
|
3069
3130
|
* Title for the admin-settings modal. Prop override, else the
|
|
3070
3131
|
* translated "Administration". Mirrors `resolvedUserSettingsTitle`.
|
|
@@ -61,3 +61,30 @@ four lines `description-lines` shows by default:
|
|
|
61
61
|
]" />
|
|
62
62
|
</div>
|
|
63
63
|
```
|
|
64
|
+
|
|
65
|
+
One option per row with `layout="rows"`, an option that cannot be picked yet
|
|
66
|
+
(`disabled` with a `disabledReason`), and fields for the chosen option right
|
|
67
|
+
under it through the `option-actions` slot (scope `{ option, selected }`):
|
|
68
|
+
|
|
69
|
+
```vue
|
|
70
|
+
<div style="max-width: 560px;">
|
|
71
|
+
<CnChoiceCards
|
|
72
|
+
label="Next step"
|
|
73
|
+
layout="rows"
|
|
74
|
+
:model-value="'decide'"
|
|
75
|
+
:options="[
|
|
76
|
+
{ value: 'decide', label: 'Take a decision',
|
|
77
|
+
description: 'Record the decision and inform the applicant.' },
|
|
78
|
+
{ value: 'advice', label: 'Ask for advice',
|
|
79
|
+
description: 'Send the case to the advisory committee.' },
|
|
80
|
+
{ value: 'close', label: 'Close the case', disabled: true,
|
|
81
|
+
disabledReason: 'A decision has to be taken first.' },
|
|
82
|
+
]">
|
|
83
|
+
<template #option-actions="{ option, selected }">
|
|
84
|
+
<p v-if="selected && option.value === 'decide'" style="margin: 0 0 0 32px;">Decision fields go here.</p>
|
|
85
|
+
</template>
|
|
86
|
+
</CnChoiceCards>
|
|
87
|
+
</div>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`columns` fixes the card grid's column count; `:columns="1"` stacks the cards in one column.
|
|
@@ -14,6 +14,45 @@
|
|
|
14
14
|
{{ emptyText }}
|
|
15
15
|
</NcNoteCard>
|
|
16
16
|
|
|
17
|
+
<!-- Rows: one full-width option per row, the input on the left and the
|
|
18
|
+
title over a muted description, no card chrome. -->
|
|
19
|
+
<div v-else-if="layout === 'rows'" class="cn-choice-cards__rows" data-testid="cn-choice-cards-rows">
|
|
20
|
+
<div
|
|
21
|
+
v-for="option in normalizedOptions"
|
|
22
|
+
:key="String(option.value)"
|
|
23
|
+
class="cn-choice-cards__cell">
|
|
24
|
+
<label
|
|
25
|
+
class="cn-choice-cards__row"
|
|
26
|
+
:class="{ 'cn-choice-cards__row--selected': isSelected(option), 'cn-choice-cards__row--disabled': isDisabled(option) }">
|
|
27
|
+
<input
|
|
28
|
+
class="cn-choice-cards__row-input"
|
|
29
|
+
:type="multiple ? 'checkbox' : 'radio'"
|
|
30
|
+
:name="groupName"
|
|
31
|
+
:value="String(option.value)"
|
|
32
|
+
:checked="isSelected(option)"
|
|
33
|
+
:disabled="isDisabled(option)"
|
|
34
|
+
:aria-describedby="option.disabled && option.disabledReason ? reasonId(option) : null"
|
|
35
|
+
@change="onToggle(option)">
|
|
36
|
+
<CnIcon v-if="option.icon"
|
|
37
|
+
:name="option.icon"
|
|
38
|
+
:size="20"
|
|
39
|
+
class="cn-choice-cards__row-icon" />
|
|
40
|
+
<span class="cn-choice-cards__row-text">
|
|
41
|
+
<span class="cn-choice-cards__row-title">{{ option.label }}</span>
|
|
42
|
+
<span v-if="option.description" class="cn-choice-cards__row-description">{{ option.description }}</span>
|
|
43
|
+
<span
|
|
44
|
+
v-if="option.disabled && option.disabledReason"
|
|
45
|
+
:id="reasonId(option)"
|
|
46
|
+
class="cn-choice-cards__reason"
|
|
47
|
+
data-testid="cn-choice-cards-reason">{{ option.disabledReason }}</span>
|
|
48
|
+
</span>
|
|
49
|
+
</label>
|
|
50
|
+
<div v-if="$slots['option-actions']" class="cn-choice-cards__actions">
|
|
51
|
+
<slot name="option-actions" :option="option" :selected="isSelected(option)" />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
17
56
|
<div v-else class="cn-choice-cards__grid" :style="gridStyle">
|
|
18
57
|
<!-- One cell per option: the card's label, and below it whatever the
|
|
19
58
|
`#option-actions` slot adds. The slot sits OUTSIDE the label on
|
|
@@ -37,7 +76,8 @@
|
|
|
37
76
|
:name="groupName"
|
|
38
77
|
:value="String(option.value)"
|
|
39
78
|
:checked="isSelected(option)"
|
|
40
|
-
:disabled="
|
|
79
|
+
:disabled="isDisabled(option)"
|
|
80
|
+
:aria-describedby="option.disabled && option.disabledReason ? reasonId(option) : null"
|
|
41
81
|
@change="onToggle(option)">
|
|
42
82
|
<CnCard
|
|
43
83
|
class="cn-choice-cards__card"
|
|
@@ -53,6 +93,11 @@
|
|
|
53
93
|
<CnIcon :name="option.icon" :size="20" />
|
|
54
94
|
</template>
|
|
55
95
|
</CnCard>
|
|
96
|
+
<span
|
|
97
|
+
v-if="option.disabled && option.disabledReason"
|
|
98
|
+
:id="reasonId(option)"
|
|
99
|
+
class="cn-choice-cards__reason cn-choice-cards__reason--card"
|
|
100
|
+
data-testid="cn-choice-cards-reason">{{ option.disabledReason }}</span>
|
|
56
101
|
</label>
|
|
57
102
|
<div v-if="$slots['option-actions']" class="cn-choice-cards__actions">
|
|
58
103
|
<!-- @slot option-actions Content below one card, outside its label
|
|
@@ -109,7 +154,9 @@ export default {
|
|
|
109
154
|
props: {
|
|
110
155
|
/**
|
|
111
156
|
* The options to offer. Each: `{ value, label, description?, stats?,
|
|
112
|
-
* tags?, icon? }` — `icon` is a PascalCase
|
|
157
|
+
* tags?, icon?, disabled?, disabledReason? }` — `icon` is a PascalCase
|
|
158
|
+
* MDI name resolved by CnIcon; a `disabled` option is greyed out and
|
|
159
|
+
* cannot be picked, with `disabledReason` shown under it.
|
|
113
160
|
*/
|
|
114
161
|
options: {
|
|
115
162
|
type: Array,
|
|
@@ -156,6 +203,23 @@ export default {
|
|
|
156
203
|
default: () => t('nextcloud-vue', 'Nothing to choose from here.'),
|
|
157
204
|
},
|
|
158
205
|
|
|
206
|
+
/**
|
|
207
|
+
* `cards`: a grid of cards with stats and tags. `rows`: one option per
|
|
208
|
+
* row, full width, the input on the left and the title over a muted
|
|
209
|
+
* description; with `multiple` and one option it is a checkbox card.
|
|
210
|
+
*/
|
|
211
|
+
layout: {
|
|
212
|
+
type: String,
|
|
213
|
+
default: 'cards',
|
|
214
|
+
validator: (value) => ['cards', 'rows'].includes(value),
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
/** Columns of the card grid; 0 lets the grid fit as many as the width allows. */
|
|
218
|
+
columns: {
|
|
219
|
+
type: Number,
|
|
220
|
+
default: 0,
|
|
221
|
+
},
|
|
222
|
+
|
|
159
223
|
/** Lines of description shown before clamping. */
|
|
160
224
|
descriptionLines: {
|
|
161
225
|
type: Number,
|
|
@@ -226,6 +290,9 @@ export default {
|
|
|
226
290
|
* @return {object} A `gridTemplateColumns` override, or `{}`.
|
|
227
291
|
*/
|
|
228
292
|
gridStyle() {
|
|
293
|
+
if (this.columns > 0) {
|
|
294
|
+
return { gridTemplateColumns: `repeat(${this.columns}, minmax(0, 1fr))` }
|
|
295
|
+
}
|
|
229
296
|
const count = this.normalizedOptions.length
|
|
230
297
|
if (count > 0 && count <= 2) {
|
|
231
298
|
return { gridTemplateColumns: `repeat(${count}, minmax(0, 1fr))` }
|
|
@@ -244,6 +311,26 @@ export default {
|
|
|
244
311
|
* @param {object} option The normalised option.
|
|
245
312
|
* @return {boolean} True when selected.
|
|
246
313
|
*/
|
|
314
|
+
/**
|
|
315
|
+
* Whether an option cannot be picked: the whole group is disabled, or the option is.
|
|
316
|
+
*
|
|
317
|
+
* @param {object} option The normalised option.
|
|
318
|
+
* @return {boolean} True when disabled.
|
|
319
|
+
*/
|
|
320
|
+
isDisabled(option) {
|
|
321
|
+
return this.disabled || option.disabled === true
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Id of an option's disabled reason, for `aria-describedby`.
|
|
326
|
+
*
|
|
327
|
+
* @param {object} option The normalised option.
|
|
328
|
+
* @return {string} The id.
|
|
329
|
+
*/
|
|
330
|
+
reasonId(option) {
|
|
331
|
+
return `${this.groupName}-reason-${String(option.value).replace(/[^A-Za-z0-9_-]/g, '-')}`
|
|
332
|
+
},
|
|
333
|
+
|
|
247
334
|
isSelected(option) {
|
|
248
335
|
return this.selectedValues.some((v) => String(v) === String(option.value))
|
|
249
336
|
},
|
|
@@ -255,7 +342,7 @@ export default {
|
|
|
255
342
|
* @return {void}
|
|
256
343
|
*/
|
|
257
344
|
onToggle(option) {
|
|
258
|
-
if (this.
|
|
345
|
+
if (this.isDisabled(option)) {
|
|
259
346
|
return
|
|
260
347
|
}
|
|
261
348
|
if (!this.multiple) {
|
|
@@ -368,4 +455,82 @@ export default {
|
|
|
368
455
|
.cn-choice-cards__input:disabled + :deep(.cn-card) {
|
|
369
456
|
opacity: 0.6;
|
|
370
457
|
}
|
|
458
|
+
|
|
459
|
+
.cn-choice-cards__rows {
|
|
460
|
+
display: flex;
|
|
461
|
+
flex-direction: column;
|
|
462
|
+
gap: 8px;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
.cn-choice-cards__row {
|
|
466
|
+
display: flex;
|
|
467
|
+
align-items: flex-start;
|
|
468
|
+
gap: 12px;
|
|
469
|
+
padding: 14px 16px;
|
|
470
|
+
border: 1px solid var(--color-border);
|
|
471
|
+
border-radius: var(--border-radius-large, 8px);
|
|
472
|
+
background: var(--color-main-background);
|
|
473
|
+
cursor: pointer;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
.cn-choice-cards__row:hover {
|
|
477
|
+
border-color: var(--color-border-dark);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.cn-choice-cards__row--selected,
|
|
481
|
+
.cn-choice-cards__row--selected:hover {
|
|
482
|
+
border-color: var(--color-primary-element);
|
|
483
|
+
/* The second pixel of the 2px selected border, without moving the content. */
|
|
484
|
+
box-shadow: inset 0 0 0 1px var(--color-primary-element);
|
|
485
|
+
background: var(--color-primary-element-light);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.cn-choice-cards__row--disabled {
|
|
489
|
+
cursor: default;
|
|
490
|
+
opacity: 0.6;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
.cn-choice-cards__row:has(.cn-choice-cards__row-input:focus-visible) {
|
|
494
|
+
outline: 2px solid var(--color-primary-element);
|
|
495
|
+
outline-offset: 2px;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
.cn-choice-cards__row-input {
|
|
499
|
+
flex: none;
|
|
500
|
+
margin: 2px 0 0;
|
|
501
|
+
accent-color: var(--color-primary-element);
|
|
502
|
+
cursor: inherit;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.cn-choice-cards__row-icon {
|
|
506
|
+
flex: none;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
.cn-choice-cards__row-text {
|
|
510
|
+
display: flex;
|
|
511
|
+
flex-direction: column;
|
|
512
|
+
gap: 2px;
|
|
513
|
+
min-width: 0;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
.cn-choice-cards__row-title {
|
|
517
|
+
font-size: 15px;
|
|
518
|
+
font-weight: 600;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.cn-choice-cards__row-description {
|
|
522
|
+
font-size: 13px;
|
|
523
|
+
color: var(--color-text-maxcontrast);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
.cn-choice-cards__reason {
|
|
527
|
+
font-size: 13px;
|
|
528
|
+
color: var(--color-text-maxcontrast);
|
|
529
|
+
font-style: italic;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
.cn-choice-cards__reason--card {
|
|
533
|
+
display: block;
|
|
534
|
+
margin-block-start: 4px;
|
|
535
|
+
}
|
|
371
536
|
</style>
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
<div
|
|
14
14
|
class="cn-dashboard-page"
|
|
15
15
|
:class="{ 'cn-dashboard-page--board': isBoardLook }"
|
|
16
|
+
:style="{ '--cn-dashboard-grid-margin': `${gridMargin}px` }"
|
|
16
17
|
data-testid="cn-dashboard-page">
|
|
17
18
|
<!-- Header. `showHeader: false` drops the whole row (title, description,
|
|
18
19
|
header actions, edit toggle) for a page that opens with its own
|
|
@@ -1252,12 +1253,16 @@ export default {
|
|
|
1252
1253
|
* October 2026", `"full"` uses the whole display name. The page
|
|
1253
1254
|
* `description`, when set, follows after another middle dot. Off by
|
|
1254
1255
|
* default: the subtitle is the description alone, as before.
|
|
1256
|
+
* `"date"` leads with the date alone, and `{ label }` puts the
|
|
1257
|
+
* translated label where the greeting would be ("Customer contact ·
|
|
1258
|
+
* Wednesday 7 October 2026 · ...").
|
|
1255
1259
|
*
|
|
1256
1260
|
* @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-greet-the-reader
|
|
1257
|
-
* @
|
|
1261
|
+
* @spec openspec/changes/dashboard-greeting-label/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-lead-with-a-label-or-the-date
|
|
1262
|
+
* @type {boolean|string|object}
|
|
1258
1263
|
*/
|
|
1259
1264
|
greeting: {
|
|
1260
|
-
type: [Boolean, String],
|
|
1265
|
+
type: [Boolean, String, Object],
|
|
1261
1266
|
default: false,
|
|
1262
1267
|
},
|
|
1263
1268
|
|
|
@@ -1992,6 +1997,10 @@ export default {
|
|
|
1992
1997
|
* @return {string}
|
|
1993
1998
|
*/
|
|
1994
1999
|
greetingLine() {
|
|
2000
|
+
if (this.greeting && typeof this.greeting === 'object') {
|
|
2001
|
+
const label = typeof this.greeting.label === 'string' ? this.greeting.label.trim() : ''
|
|
2002
|
+
return label ? this.effectiveTranslate(label) : ''
|
|
2003
|
+
}
|
|
1995
2004
|
if (this.greeting !== true && this.greeting !== 'first' && this.greeting !== 'full') {
|
|
1996
2005
|
return ''
|
|
1997
2006
|
}
|
|
@@ -2014,17 +2023,20 @@ export default {
|
|
|
2014
2023
|
},
|
|
2015
2024
|
|
|
2016
2025
|
/**
|
|
2017
|
-
* The header subtitle: with `greeting` on, the greeting
|
|
2018
|
-
* written out and the description, joined by a
|
|
2019
|
-
*
|
|
2026
|
+
* The header subtitle: with `greeting` on, the greeting (or the
|
|
2027
|
+
* label), today's date written out and the description, joined by a
|
|
2028
|
+
* middle dot; with `greeting: "date"` the date leads, capitalised;
|
|
2029
|
+
* else the description alone, as before.
|
|
2020
2030
|
*
|
|
2021
2031
|
* @spec openspec/changes/screens-dashboard-greeting-header/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-greet-the-reader
|
|
2032
|
+
* @spec openspec/changes/dashboard-greeting-label/specs/dashboard-page/spec.md#requirement-the-header-subtitle-can-lead-with-a-label-or-the-date
|
|
2022
2033
|
* @return {string}
|
|
2023
2034
|
*/
|
|
2024
2035
|
subtitleText() {
|
|
2025
2036
|
const description = this.description ? this.resolvedDescription : ''
|
|
2037
|
+
const dateOnly = this.greeting === 'date'
|
|
2026
2038
|
const greeting = this.greetingLine
|
|
2027
|
-
if (greeting === '') {
|
|
2039
|
+
if (greeting === '' && !dateOnly) {
|
|
2028
2040
|
return description
|
|
2029
2041
|
}
|
|
2030
2042
|
const options = { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }
|
|
@@ -2034,6 +2046,9 @@ export default {
|
|
|
2034
2046
|
} catch {
|
|
2035
2047
|
date = new Intl.DateTimeFormat(undefined, options).format(new Date())
|
|
2036
2048
|
}
|
|
2049
|
+
if (dateOnly) {
|
|
2050
|
+
date = date.charAt(0).toLocaleUpperCase() + date.slice(1)
|
|
2051
|
+
}
|
|
2037
2052
|
return [greeting, date, description].filter((part) => part !== '').join(' · ')
|
|
2038
2053
|
},
|
|
2039
2054
|
|
|
@@ -4899,6 +4914,15 @@ export default {
|
|
|
4899
4914
|
margin-bottom: var(--cn-board-section-gap, 16px);
|
|
4900
4915
|
}
|
|
4901
4916
|
|
|
4917
|
+
/* Board look: GridStack insets every card by the grid margin on each side, so
|
|
4918
|
+
the cards started a margin to the right of the KPI row and the header. The
|
|
4919
|
+
grid reaches out by that margin, so the card edges line up with the page's
|
|
4920
|
+
(dashboard-grid-flush). */
|
|
4921
|
+
.cn-dashboard-page--board :deep(.cn-dashboard-grid) {
|
|
4922
|
+
width: auto;
|
|
4923
|
+
margin-inline: calc(-1 * var(--cn-dashboard-grid-margin, 12px));
|
|
4924
|
+
}
|
|
4925
|
+
|
|
4902
4926
|
.cn-dashboard-page__kpi-cell {
|
|
4903
4927
|
display: flex;
|
|
4904
4928
|
min-width: 0;
|
|
@@ -5129,6 +5153,21 @@ export default {
|
|
|
5129
5153
|
.cn-dashboard-page--board .cn-dashboard-page__kpi-cell {
|
|
5130
5154
|
min-height: 44px;
|
|
5131
5155
|
}
|
|
5156
|
+
|
|
5157
|
+
/* The widget Actions menu shows its icon only, so the card title keeps
|
|
5158
|
+
the room; the name stays for assistive tech. */
|
|
5159
|
+
.cn-dashboard-page--board :deep(.cn-widget-wrapper__actions .action-item__menutoggle) {
|
|
5160
|
+
min-width: 44px;
|
|
5161
|
+
}
|
|
5162
|
+
|
|
5163
|
+
.cn-dashboard-page--board :deep(.cn-widget-wrapper__actions .action-item__menutoggle .button-vue__text) {
|
|
5164
|
+
position: absolute;
|
|
5165
|
+
width: 1px;
|
|
5166
|
+
height: 1px;
|
|
5167
|
+
overflow: hidden;
|
|
5168
|
+
clip-path: inset(50%);
|
|
5169
|
+
white-space: nowrap;
|
|
5170
|
+
}
|
|
5132
5171
|
}
|
|
5133
5172
|
|
|
5134
5173
|
/* What a registry widget reports for its card header (`header` event): the
|
|
@@ -404,7 +404,7 @@ import CnColumnFilterPopover from './CnColumnFilterPopover.vue'
|
|
|
404
404
|
import { useClickDragGuard } from '../../composables/useClickDragGuard.js'
|
|
405
405
|
import { normalizeLook } from '../../composables/useLook.js'
|
|
406
406
|
import { clearedColumnFilterParams, columnFilterDef, columnFilterParams, columnFilterState, isColumnFilterActive, isColumnSortable } from '../../utils/columnFilters.js'
|
|
407
|
-
import { fillTemplate } from '../../utils/fillTemplate.js'
|
|
407
|
+
import { fillTemplate, joinTemplateParts } from '../../utils/fillTemplate.js'
|
|
408
408
|
import { lensUnavailableText, readLensReports } from '../../utils/lensAvailability.js'
|
|
409
409
|
import { followLinkClick, openRowTarget, resolveHref } from '../../utils/linkNavigation.js'
|
|
410
410
|
import { nextSortState } from '../../utils/multiColumnSort.js'
|
|
@@ -421,40 +421,6 @@ import { CnUnreadMarker } from '../CnUnreadMarker/index.js'
|
|
|
421
421
|
// consuming app does not pull in the library's global css/index.css.
|
|
422
422
|
import '../../css/table.css'
|
|
423
423
|
|
|
424
|
-
/**
|
|
425
|
-
* Fill a `secondary` template such as `"{identifier} · {requester}"`.
|
|
426
|
-
*
|
|
427
|
-
* A field without a value takes its separator with it: the text between two
|
|
428
|
-
* fields belongs to the field after it, so an empty requester leaves
|
|
429
|
-
* "2026-0002", not "2026-0002 ·", and an empty first field does not leave a
|
|
430
|
-
* leading separator either. Text before the first field and after the last
|
|
431
|
-
* one is kept as written. A template whose fields all have values fills
|
|
432
|
-
* exactly as before.
|
|
433
|
-
*
|
|
434
|
-
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-empty-field-takes-its-separator-with-it
|
|
435
|
-
* @param {string} template The template, with `{field}` placeholders.
|
|
436
|
-
* @param {(key: string) => string} valueOf The text of a field ('' when empty).
|
|
437
|
-
* @return {string} The filled template.
|
|
438
|
-
*/
|
|
439
|
-
function fillSecondaryTemplate(template, valueOf) {
|
|
440
|
-
// split() with a capture group alternates literal, field, literal, …
|
|
441
|
-
const parts = template.split(/\{([^}]+)\}/)
|
|
442
|
-
const lead = parts[0]
|
|
443
|
-
const tail = parts[parts.length - 1]
|
|
444
|
-
let body = ''
|
|
445
|
-
for (let i = 1; i < parts.length; i += 2) {
|
|
446
|
-
const value = valueOf(parts[i].trim())
|
|
447
|
-
if (value === '') {
|
|
448
|
-
continue
|
|
449
|
-
}
|
|
450
|
-
// The literal before this field: the separator, unless nothing has
|
|
451
|
-
// been written yet (then there is nothing to separate from).
|
|
452
|
-
const separator = i === 1 ? '' : parts[i - 1]
|
|
453
|
-
body += (body === '' ? '' : separator) + value
|
|
454
|
-
}
|
|
455
|
-
return lead + body + tail
|
|
456
|
-
}
|
|
457
|
-
|
|
458
424
|
/**
|
|
459
425
|
* CnDataTable — Generic sortable data table for list views.
|
|
460
426
|
*
|
|
@@ -1790,7 +1756,7 @@ export default {
|
|
|
1790
1756
|
return ''
|
|
1791
1757
|
}
|
|
1792
1758
|
if (spec.includes('{')) {
|
|
1793
|
-
const filled =
|
|
1759
|
+
const filled = joinTemplateParts(spec, (key) => (key.includes('|')
|
|
1794
1760
|
? fillTemplate(`{${key}}`, row)
|
|
1795
1761
|
: this.secondaryFieldText(row, key)))
|
|
1796
1762
|
// A template whose every field is empty leaves only separators:
|