@conduction/nextcloud-vue 2.62.0 → 2.64.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/CnAdminActionCard/CnAdminActionCard.vue.js +10 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js.map +1 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js +189 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js.map +1 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js +72 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js.map +1 -0
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +425 -39
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +119 -42
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +186 -9
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +65 -42
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +61 -16
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +20 -0
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -2
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +33 -2
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +152 -135
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +253 -9
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +126 -50
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +66 -14
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +23 -14
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +81 -4
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +22 -5
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +81 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +7 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
- package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +41 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +8 -3
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +63 -2
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +96 -37
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/stagesModel.js +5 -2
- package/dist/esm/components/CnStagesWidget/stagesModel.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +52 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -6
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +14 -2
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +2 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
- package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +73 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +14 -2
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
- package/dist/esm/composables/useDependencyCheck.js +65 -0
- package/dist/esm/composables/useDependencyCheck.js.map +1 -0
- package/dist/esm/composables/useWalkthrough.js +218 -6
- package/dist/esm/composables/useWalkthrough.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.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 +45 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +780 -81
- package/dist/esm/store/useObjectStore.js +8 -0
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/columnFilters.js +311 -0
- package/dist/esm/utils/columnFilters.js.map +1 -0
- package/dist/esm/utils/groupAutocomplete.js +123 -0
- package/dist/esm/utils/groupAutocomplete.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +2 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/pickerOptions.js +198 -0
- package/dist/esm/utils/pickerOptions.js.map +1 -0
- package/dist/esm/utils/schema.js +83 -7
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/schemaRefSlug.js +19 -3
- package/dist/esm/utils/schemaRefSlug.js.map +1 -1
- package/dist/esm/utils/timelineEvents.js +174 -0
- package/dist/esm/utils/timelineEvents.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/walkthroughSignals.js +88 -0
- package/dist/esm/utils/walkthroughSignals.js.map +1 -0
- package/dist/nextcloud-vue.cjs.js +13088 -7937
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +780 -81
- package/l10n/en.json +45 -0
- package/l10n/nl.json +45 -0
- package/package.json +1 -1
- package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
- package/src/components/CnAdminActionCard/index.js +4 -0
- package/src/components/CnAppNav/CnAppNav.vue +568 -43
- package/src/components/CnAppRoot/CnAppRoot.md +4 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +223 -16
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +61 -16
- package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +22 -1
- package/src/components/CnCellRenderer/CnCellRenderer.vue +33 -2
- package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
- package/src/components/CnDashboardPage/CnDashboardPage.vue +45 -4
- package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
- package/src/components/CnDataTable/CnDataTable.md +3 -0
- package/src/components/CnDataTable/CnDataTable.vue +305 -5
- package/src/components/CnDetailPage/CnDetailPage.md +4 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +60 -1
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
- package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
- package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +116 -6
- package/src/components/CnIndexPage/CnIndexPage.vue +87 -4
- package/src/components/CnIndexPage/selfModeActions.js +3 -3
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +46 -2
- package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
- package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
- package/src/components/CnStagesWidget/CnStagesWidget.vue +180 -2
- package/src/components/CnStagesWidget/stagesModel.js +5 -2
- package/src/components/CnStatWidget/CnStatWidget.vue +57 -3
- package/src/components/CnTabsWidget/CnTabsWidget.vue +15 -2
- package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
- package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
- package/src/components/CnTimelineWidget/index.js +4 -0
- package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
- package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +4 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +107 -1
- package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
- package/src/components/index.js +2 -0
- package/src/composables/useDependencyCheck.js +61 -0
- package/src/composables/useWalkthrough.js +217 -5
- package/src/css/dashboard.css +49 -0
- package/src/css/detail-page.css +29 -4
- package/src/css/kpi-card.css +24 -1
- package/src/css/table.css +99 -6
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +135 -3
- package/src/store/useObjectStore.js +8 -0
- package/src/utils/columnFilters.js +324 -0
- package/src/utils/groupAutocomplete.js +119 -0
- package/src/utils/libraryWidgetKeys.js +2 -0
- package/src/utils/pickerOptions.js +194 -0
- package/src/utils/schema.js +83 -7
- package/src/utils/schemaRefSlug.js +19 -3
- package/src/utils/timelineEvents.js +171 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
- package/src/utils/walkthroughSignals.js +94 -0
|
@@ -92,8 +92,23 @@
|
|
|
92
92
|
<!-- Decorative beside a name: the name already says whose
|
|
93
93
|
app this is, and an alt text would say it twice. A logo
|
|
94
94
|
on its own carries the name as its alt text. -->
|
|
95
|
+
<!-- An emblem (`nav.brand.emblem`) stands in for the logo:
|
|
96
|
+
the municipality's mark beside the app name, distinct
|
|
97
|
+
from the wordmark in the top bar. `true` draws the
|
|
98
|
+
theme's emblem from --nldesign-emblem-url. -->
|
|
95
99
|
<img
|
|
96
|
-
v-if="resolvedBrand.
|
|
100
|
+
v-if="typeof resolvedBrand.emblem === 'string' && resolvedBrand.emblem !== ''"
|
|
101
|
+
class="cn-app-nav__brand-emblem"
|
|
102
|
+
data-testid="cn-nav-brand-emblem"
|
|
103
|
+
:src="resolvedBrand.emblem"
|
|
104
|
+
:alt="resolvedBrand.name ? '' : resolvedBrand.alt">
|
|
105
|
+
<span
|
|
106
|
+
v-else-if="resolvedBrand.emblem === true"
|
|
107
|
+
class="cn-app-nav__brand-emblem cn-app-nav__brand-emblem--theme"
|
|
108
|
+
data-testid="cn-nav-brand-emblem"
|
|
109
|
+
aria-hidden="true" />
|
|
110
|
+
<img
|
|
111
|
+
v-else-if="resolvedBrand.logo"
|
|
97
112
|
class="cn-app-nav__brand-logo"
|
|
98
113
|
:src="resolvedBrand.logo"
|
|
99
114
|
:alt="resolvedBrand.name ? '' : resolvedBrand.alt">
|
|
@@ -118,17 +133,33 @@
|
|
|
118
133
|
OR nav root). Omitted entirely when neither the slot nor any
|
|
119
134
|
resolvable primaryAction is present. -->
|
|
120
135
|
<slot name="primary-action">
|
|
136
|
+
<!-- A primary action that carries an `action` runs a page action
|
|
137
|
+
(open-form opens the create dialog, navigate pushes a route)
|
|
138
|
+
through CnActionButtons, so "New case" can live in the
|
|
139
|
+
navigation without a create page. Drawn as one solid,
|
|
140
|
+
full-width primary button. -->
|
|
141
|
+
<div
|
|
142
|
+
v-if="activePrimaryAction && primaryDispatchEntry"
|
|
143
|
+
class="app-navigation-new cn-app-nav__primary-action cn-app-nav__primary-action--solid"
|
|
144
|
+
data-testid="cn-nav-primary-action">
|
|
145
|
+
<CnActionButtons
|
|
146
|
+
:actions="[primaryDispatchEntry]"
|
|
147
|
+
data-testid="cn-nav-primary-action-dispatch"
|
|
148
|
+
@created="onPrimaryActionCreated" />
|
|
149
|
+
</div>
|
|
121
150
|
<!-- A primary action with an href or route is a real link.
|
|
122
151
|
NcAppNavigationNew cannot render one, so this mirrors its
|
|
123
|
-
wrapper and button.
|
|
152
|
+
wrapper and button. `solid: true` takes this same solid,
|
|
153
|
+
full-width button for an action with neither. -->
|
|
124
154
|
<div
|
|
125
|
-
v-if="activePrimaryAction && primaryActionLink"
|
|
155
|
+
v-else-if="activePrimaryAction && (primaryActionLink || activePrimaryAction.solid === true)"
|
|
126
156
|
class="app-navigation-new cn-app-nav__primary-action"
|
|
157
|
+
:class="{ 'cn-app-nav__primary-action--solid': activePrimaryAction.solid === true }"
|
|
127
158
|
data-testid="cn-nav-primary-action">
|
|
128
159
|
<NcButton
|
|
129
160
|
variant="primary"
|
|
130
161
|
wide
|
|
131
|
-
v-bind="primaryActionLink"
|
|
162
|
+
v-bind="primaryActionLink || {}"
|
|
132
163
|
@click="onPrimaryActionClick">
|
|
133
164
|
<template #icon>
|
|
134
165
|
<component :is="primaryActionIconComponent" :size="20" />
|
|
@@ -215,7 +246,32 @@
|
|
|
215
246
|
</NcAppNavigationItem>
|
|
216
247
|
</template>
|
|
217
248
|
</template>
|
|
218
|
-
<template v-if="footerItems.length > 0 || showSettingsFoldout" #footer>
|
|
249
|
+
<template v-if="footerItems.length > 0 || showSettingsFoldout || resolvedCard || resolvedHelp" #footer>
|
|
250
|
+
<!--
|
|
251
|
+
@slot card
|
|
252
|
+
@description Replace the card above the footer entries
|
|
253
|
+
(`nav.card`). Default content: a title, a line of text and one
|
|
254
|
+
link. Renders nothing when neither the slot nor `nav.card` is set.
|
|
255
|
+
@binding {object|null} card The resolved card, or null.
|
|
256
|
+
-->
|
|
257
|
+
<slot name="card" :card="resolvedCard">
|
|
258
|
+
<div
|
|
259
|
+
v-if="resolvedCard"
|
|
260
|
+
class="cn-app-nav__card"
|
|
261
|
+
data-testid="cn-nav-card">
|
|
262
|
+
<strong class="cn-app-nav__card-title">{{ resolvedCard.title }}</strong>
|
|
263
|
+
<span v-if="resolvedCard.text" class="cn-app-nav__card-text">{{ resolvedCard.text }}</span>
|
|
264
|
+
<component
|
|
265
|
+
:is="resolvedCard.linkTag"
|
|
266
|
+
v-if="resolvedCard.linkTag"
|
|
267
|
+
class="cn-app-nav__card-link"
|
|
268
|
+
data-testid="cn-nav-card-link"
|
|
269
|
+
v-bind="resolvedCard.linkAttrs"
|
|
270
|
+
@click="onCardLinkClick">
|
|
271
|
+
{{ resolvedCard.linkLabel }}
|
|
272
|
+
</component>
|
|
273
|
+
</div>
|
|
274
|
+
</slot>
|
|
219
275
|
<!-- Footer-section entries (Documentation, Features & Roadmap,
|
|
220
276
|
About) live in NcAppNavigation's #footer slot — OUTSIDE the
|
|
221
277
|
scrollable list — so they stay visible above the settings
|
|
@@ -226,7 +282,20 @@
|
|
|
226
282
|
hover highlight are scoped to itself, so they survive being
|
|
227
283
|
slotted. The main list's inset comes from NcAppNavigation's
|
|
228
284
|
scope, which slot content does not carry. -->
|
|
229
|
-
<NcAppNavigationList v-if="footerItems.length > 0" class="cn-app-nav__footer-list">
|
|
285
|
+
<NcAppNavigationList v-if="footerItems.length > 0 || resolvedHelp" class="cn-app-nav__footer-list">
|
|
286
|
+
<!-- The help entry (`nav.help`): a link to the app's own help
|
|
287
|
+
with a help icon, above the footer entries. -->
|
|
288
|
+
<NcAppNavigationItem
|
|
289
|
+
v-if="resolvedHelp"
|
|
290
|
+
:name="resolvedHelp.label"
|
|
291
|
+
:to="resolvedHelp.to"
|
|
292
|
+
:href="resolvedHelp.href"
|
|
293
|
+
:target="resolvedHelp.href ? '_blank' : undefined"
|
|
294
|
+
data-testid="cn-nav-help">
|
|
295
|
+
<template #icon>
|
|
296
|
+
<HelpCircleOutline :size="20" />
|
|
297
|
+
</template>
|
|
298
|
+
</NcAppNavigationItem>
|
|
230
299
|
<NcAppNavigationItem
|
|
231
300
|
v-for="item in footerItems"
|
|
232
301
|
:key="item.id"
|
|
@@ -360,6 +429,7 @@ import CnMenuItemIcon from '../CnMenuWidget/CnMenuItemIcon.vue'
|
|
|
360
429
|
import { isAppInstalled } from '../../utils/appInstalled.js'
|
|
361
430
|
import { isSvgPath } from '../../utils/iconUtils.js'
|
|
362
431
|
import { passesContextPredicates } from '../../utils/visibleIfContext.js'
|
|
432
|
+
import { CnActionButtons } from '../CnActionButtons/index.js'
|
|
363
433
|
// The legacy `icon-*` → MDI bridge lives beside CnIcon now, so the menu EDITOR
|
|
364
434
|
// resolves a seeded `icon-comment` the same way this nav does. It used to be a
|
|
365
435
|
// map local to this file, which is why the nav drew a proper glyph and the
|
|
@@ -394,6 +464,44 @@ function byManifestOrder(a, b) {
|
|
|
394
464
|
return a.order - b.order
|
|
395
465
|
}
|
|
396
466
|
|
|
467
|
+
/** A `:name` page-path segment: name, optional `(regexp)`, optional modifier. */
|
|
468
|
+
const PARAM_SEGMENT = /^:(\w+)(\([^)]*\))?([?*+])?$/
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Whether a `PARAM_SEGMENT` match may be left out (`?` or `*`).
|
|
472
|
+
*
|
|
473
|
+
* @param {Array<string>} param The match.
|
|
474
|
+
* @return {boolean} True for an optional segment.
|
|
475
|
+
*/
|
|
476
|
+
function isOptionalParam(param) {
|
|
477
|
+
return param[3] === '?' || param[3] === '*'
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Whether a route param value fills its segment.
|
|
482
|
+
*
|
|
483
|
+
* @param {unknown} value The value.
|
|
484
|
+
* @return {boolean} False for undefined, null and the empty string.
|
|
485
|
+
*/
|
|
486
|
+
function isFilled(value) {
|
|
487
|
+
return value !== undefined && value !== null && value !== ''
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Decode one percent-encoded path segment, or keep it as is when it is not
|
|
492
|
+
* valid percent-encoding.
|
|
493
|
+
*
|
|
494
|
+
* @param {string} segment The segment.
|
|
495
|
+
* @return {string} The decoded segment.
|
|
496
|
+
*/
|
|
497
|
+
function decodeSegment(segment) {
|
|
498
|
+
try {
|
|
499
|
+
return decodeURIComponent(segment)
|
|
500
|
+
} catch {
|
|
501
|
+
return segment
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
397
505
|
export default {
|
|
398
506
|
name: 'CnAppNav',
|
|
399
507
|
|
|
@@ -406,6 +514,7 @@ export default {
|
|
|
406
514
|
NcAppNavigationSettings,
|
|
407
515
|
NcButton,
|
|
408
516
|
NcCounterBubble,
|
|
517
|
+
CnActionButtons,
|
|
409
518
|
CnMenuItemIcon,
|
|
410
519
|
Cog,
|
|
411
520
|
HelpCircleOutline,
|
|
@@ -452,6 +561,16 @@ export default {
|
|
|
452
561
|
* @type {{ [register: string]: { [schema: string]: number } }}
|
|
453
562
|
*/
|
|
454
563
|
cnMenuCounts: { default: () => ({}) },
|
|
564
|
+
/**
|
|
565
|
+
* Provided by CnAppRoot — reactive `{ [menuItemId]: number }` totals
|
|
566
|
+
* for every menu entry whose `count` is an object
|
|
567
|
+
* (`{ register, schema, filter? }`): the total of that filtered
|
|
568
|
+
* list, fetched under the entry's own key so an index page's
|
|
569
|
+
* whole-schema total is left alone. Empty outside a CnAppRoot.
|
|
570
|
+
*
|
|
571
|
+
* @type {{ [itemId: string]: number }}
|
|
572
|
+
*/
|
|
573
|
+
cnMenuItemCounts: { default: () => ({}) },
|
|
455
574
|
},
|
|
456
575
|
|
|
457
576
|
props: {
|
|
@@ -575,7 +694,7 @@ export default {
|
|
|
575
694
|
},
|
|
576
695
|
},
|
|
577
696
|
|
|
578
|
-
emits: ['primary-action', 'primary-action-click'],
|
|
697
|
+
emits: ['primary-action', 'primary-action-click', 'primary-action-created', 'card-action'],
|
|
579
698
|
|
|
580
699
|
/**
|
|
581
700
|
* The navigation's own state: which groups are open, and which entry
|
|
@@ -636,11 +755,70 @@ export default {
|
|
|
636
755
|
const text = (value) => (typeof value === 'string' && value !== '' ? this.effectiveTranslate(value) : '')
|
|
637
756
|
const brand = {
|
|
638
757
|
logo: typeof declared.logo === 'string' ? declared.logo : '',
|
|
758
|
+
// A URL, or `true` for the theme's emblem (--nldesign-emblem-url).
|
|
759
|
+
emblem: declared.emblem === true ? true : (typeof declared.emblem === 'string' ? declared.emblem : ''),
|
|
639
760
|
name: text(declared.name),
|
|
640
761
|
caption: text(declared.caption),
|
|
641
762
|
alt: text(declared.alt),
|
|
642
763
|
}
|
|
643
|
-
return (brand.logo || brand.name || brand.caption) ? brand : null
|
|
764
|
+
return (brand.logo || brand.emblem || brand.name || brand.caption) ? brand : null
|
|
765
|
+
},
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* The card above the footer entries (`nav.card`): `{ title, text?,
|
|
769
|
+
* link?: { label, route?, params?, href?, action? } }`. Title, text
|
|
770
|
+
* and link label go through the translate function. The link is a
|
|
771
|
+
* router link for a `route` (with a router), an anchor for an
|
|
772
|
+
* `href`, and a button emitting `card-action` with the `action` id
|
|
773
|
+
* otherwise. Null without a title.
|
|
774
|
+
*
|
|
775
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-navigation-carries-a-card-and-a-help-entry
|
|
776
|
+
* @return {object|null}
|
|
777
|
+
*/
|
|
778
|
+
resolvedCard() {
|
|
779
|
+
const declared = this.effectiveManifest?.nav?.card
|
|
780
|
+
if (!declared || typeof declared !== 'object' || typeof declared.title !== 'string' || declared.title === '') {
|
|
781
|
+
return null
|
|
782
|
+
}
|
|
783
|
+
const text = (value) => (typeof value === 'string' && value !== '' ? this.effectiveTranslate(value) : '')
|
|
784
|
+
const card = { title: text(declared.title), text: text(declared.text), linkTag: '', linkAttrs: {}, linkLabel: '', action: '' }
|
|
785
|
+
const link = declared.link
|
|
786
|
+
if (link && typeof link === 'object' && typeof link.label === 'string' && link.label !== '') {
|
|
787
|
+
card.linkLabel = text(link.label)
|
|
788
|
+
if (link.route && this.$router) {
|
|
789
|
+
card.linkTag = 'router-link'
|
|
790
|
+
card.linkAttrs = { to: { name: link.route, ...(link.params && typeof link.params === 'object' ? { params: link.params } : {}) } }
|
|
791
|
+
} else if (typeof link.href === 'string' && link.href !== '') {
|
|
792
|
+
card.linkTag = 'a'
|
|
793
|
+
card.linkAttrs = { href: link.href }
|
|
794
|
+
} else if (typeof link.action === 'string' && link.action !== '') {
|
|
795
|
+
card.linkTag = 'button'
|
|
796
|
+
card.linkAttrs = { type: 'button' }
|
|
797
|
+
card.action = link.action
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
return card
|
|
801
|
+
},
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* The help entry (`nav.help`): `{ label, route?, href? }` rendered as
|
|
805
|
+
* a footer entry with a help icon. Null without a label or a target.
|
|
806
|
+
*
|
|
807
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-navigation-carries-a-card-and-a-help-entry
|
|
808
|
+
* @return {{label: string, to?: object, href?: string}|null}
|
|
809
|
+
*/
|
|
810
|
+
resolvedHelp() {
|
|
811
|
+
const declared = this.effectiveManifest?.nav?.help
|
|
812
|
+
if (!declared || typeof declared !== 'object' || typeof declared.label !== 'string' || declared.label === '') {
|
|
813
|
+
return null
|
|
814
|
+
}
|
|
815
|
+
if (declared.route && this.$router) {
|
|
816
|
+
return { label: this.effectiveTranslate(declared.label), to: { name: declared.route }, href: undefined }
|
|
817
|
+
}
|
|
818
|
+
if (typeof declared.href === 'string' && declared.href !== '') {
|
|
819
|
+
return { label: this.effectiveTranslate(declared.label), to: undefined, href: declared.href }
|
|
820
|
+
}
|
|
821
|
+
return null
|
|
644
822
|
},
|
|
645
823
|
|
|
646
824
|
/**
|
|
@@ -661,20 +839,30 @@ export default {
|
|
|
661
839
|
* 2. `manifest.nav.primaryAction` as app-wide default
|
|
662
840
|
* 3. `null` — no primary-action button renders
|
|
663
841
|
*
|
|
664
|
-
* Page-scoped declarations always win over the nav-root default.
|
|
842
|
+
* Page-scoped declarations always win over the nav-root default. An
|
|
843
|
+
* action declaring `permission` or `visibleIf` is gated like a menu
|
|
844
|
+
* entry.
|
|
665
845
|
*
|
|
846
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-navigation-primary-action-runs-a-page-action
|
|
666
847
|
* @return {object|null}
|
|
667
848
|
*/
|
|
668
849
|
activePrimaryAction() {
|
|
669
850
|
const pages = this.effectiveManifest?.pages ?? []
|
|
670
851
|
const routeName = this.$route?.name
|
|
852
|
+
// The same `permission` / `visibleIf` gate a menu entry has: an
|
|
853
|
+
// action the reader may not take, or that a condition hides, is
|
|
854
|
+
// not drawn. A gated-out page action falls back to the nav one.
|
|
855
|
+
const gated = (action) => (action && this.passesPermission(action) && this.passesVisibleIf(action) ? action : null)
|
|
671
856
|
if (routeName) {
|
|
672
857
|
const page = pages.find((p) => p.id === routeName)
|
|
673
858
|
if (page && page.primaryAction) {
|
|
674
|
-
|
|
859
|
+
const pageAction = gated(page.primaryAction)
|
|
860
|
+
if (pageAction) {
|
|
861
|
+
return pageAction
|
|
862
|
+
}
|
|
675
863
|
}
|
|
676
864
|
}
|
|
677
|
-
return this.effectiveManifest?.nav?.primaryAction ?? null
|
|
865
|
+
return gated(this.effectiveManifest?.nav?.primaryAction ?? null)
|
|
678
866
|
},
|
|
679
867
|
|
|
680
868
|
/**
|
|
@@ -688,6 +876,31 @@ export default {
|
|
|
688
876
|
return this.mdiIconComponent(this.activePrimaryAction) ?? Plus
|
|
689
877
|
},
|
|
690
878
|
|
|
879
|
+
/**
|
|
880
|
+
* The CnActionButtons entry for a primary action that carries an
|
|
881
|
+
* `action` (`$defs/action`: open-form, navigate, api-call, ...):
|
|
882
|
+
* the action with the primary action's `id`, `label` and `icon`
|
|
883
|
+
* folded in and `variant: "primary"`. Null for a plain primary
|
|
884
|
+
* action, which keeps its link or button.
|
|
885
|
+
*
|
|
886
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-navigation-primary-action-runs-a-page-action
|
|
887
|
+
* @return {object|null}
|
|
888
|
+
*/
|
|
889
|
+
primaryDispatchEntry() {
|
|
890
|
+
const action = this.activePrimaryAction
|
|
891
|
+
const inner = action && action.action
|
|
892
|
+
if (!inner || typeof inner !== 'object' || typeof inner.type !== 'string') {
|
|
893
|
+
return null
|
|
894
|
+
}
|
|
895
|
+
return {
|
|
896
|
+
icon: action.icon || 'Plus',
|
|
897
|
+
...inner,
|
|
898
|
+
id: inner.id || action.id || 'primary-action',
|
|
899
|
+
label: action.label,
|
|
900
|
+
variant: 'primary',
|
|
901
|
+
}
|
|
902
|
+
},
|
|
903
|
+
|
|
691
904
|
/**
|
|
692
905
|
* Link props for the primary action's button: an `href` opens in a
|
|
693
906
|
* new tab, a `route` is a router link. Null when the action is
|
|
@@ -924,7 +1137,9 @@ export default {
|
|
|
924
1137
|
* `/expenses/:id`) light up — and auto-expand — their index entry
|
|
925
1138
|
* even though the detail route name is not in the menu. Longest
|
|
926
1139
|
* prefix disambiguates nested namespaces (e.g. `/pos` vs
|
|
927
|
-
* `/pos/refunds`).
|
|
1140
|
+
* `/pos/refunds`). An entry with `params` matches by its page path
|
|
1141
|
+
* with those params filled in (see `pageMatchDepth`). Returns null
|
|
1142
|
+
* when nothing matches.
|
|
928
1143
|
*
|
|
929
1144
|
* @return {string|null}
|
|
930
1145
|
*/
|
|
@@ -945,20 +1160,15 @@ export default {
|
|
|
945
1160
|
return routeName ?? null
|
|
946
1161
|
}
|
|
947
1162
|
let best = null
|
|
948
|
-
let
|
|
1163
|
+
let bestDepth = 0
|
|
949
1164
|
for (const it of flat) {
|
|
950
1165
|
if (!it.route) {
|
|
951
1166
|
continue
|
|
952
1167
|
}
|
|
953
|
-
const
|
|
954
|
-
if (
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
if (path === pagePath || path.startsWith(pagePath + '/')) {
|
|
958
|
-
if (pagePath.length > bestLen) {
|
|
959
|
-
best = it.route
|
|
960
|
-
bestLen = pagePath.length
|
|
961
|
-
}
|
|
1168
|
+
const depth = this.pageMatchDepth(it, path)
|
|
1169
|
+
if (depth > bestDepth) {
|
|
1170
|
+
best = it.route
|
|
1171
|
+
bestDepth = depth
|
|
962
1172
|
}
|
|
963
1173
|
}
|
|
964
1174
|
return best ?? routeName ?? null
|
|
@@ -1007,7 +1217,9 @@ export default {
|
|
|
1007
1217
|
* that route, else the first in menu order.
|
|
1008
1218
|
*
|
|
1009
1219
|
* Null whenever the existing rule already marks an entry, so a list
|
|
1010
|
-
* with an entry without a `query` behaves as it did.
|
|
1220
|
+
* with an entry without a `query` behaves as it did. An entry with
|
|
1221
|
+
* `params` is a candidate only when the current path sits below its
|
|
1222
|
+
* own filled-in page path, so another param value is never marked.
|
|
1011
1223
|
*
|
|
1012
1224
|
* @return {object|null} The entry to mark, or null.
|
|
1013
1225
|
* @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-a-page-below-a-list-marks-one-menu-entry
|
|
@@ -1017,7 +1229,9 @@ export default {
|
|
|
1017
1229
|
if (!active || this.$route?.name === active) {
|
|
1018
1230
|
return null
|
|
1019
1231
|
}
|
|
1020
|
-
const
|
|
1232
|
+
const path = this.$route?.path
|
|
1233
|
+
const entries = this.flatEntries.filter((entry) => !entry.href && entry.route === active
|
|
1234
|
+
&& (!this.hasParams(entry) || this.pageMatchDepth(entry, path) > 0))
|
|
1021
1235
|
if (entries.length === 0 || entries.some((entry) => this.isActiveByRule(entry))) {
|
|
1022
1236
|
return null
|
|
1023
1237
|
}
|
|
@@ -1067,6 +1281,8 @@ export default {
|
|
|
1067
1281
|
// stays out of `data()`: a reactive Set mutated inside render would
|
|
1068
1282
|
// re-trigger the render effect.
|
|
1069
1283
|
this._autoCountWarned = new Set()
|
|
1284
|
+
// Same latch, for warnUnfilledRouteParam() (`itemTo()` runs during render).
|
|
1285
|
+
this._unfilledParamWarned = new Set()
|
|
1070
1286
|
},
|
|
1071
1287
|
|
|
1072
1288
|
methods: {
|
|
@@ -1275,7 +1491,19 @@ export default {
|
|
|
1275
1491
|
.sort(byManifestOrder)
|
|
1276
1492
|
},
|
|
1277
1493
|
|
|
1494
|
+
/**
|
|
1495
|
+
* The label an entry shows: run through the translate function, or
|
|
1496
|
+
* as written when the entry sets `translateLabel: false` (a
|
|
1497
|
+
* user-authored title that may equal a translation key). Every
|
|
1498
|
+
* place the nav shows an entry's label reads it from here.
|
|
1499
|
+
*
|
|
1500
|
+
* @param {object} item Menu entry.
|
|
1501
|
+
* @return {string} The label to render as text.
|
|
1502
|
+
*/
|
|
1278
1503
|
resolveLabel(item) {
|
|
1504
|
+
if (item.translateLabel === false) {
|
|
1505
|
+
return item.label
|
|
1506
|
+
}
|
|
1279
1507
|
return this.effectiveTranslate(item.label)
|
|
1280
1508
|
},
|
|
1281
1509
|
|
|
@@ -1286,7 +1514,10 @@ export default {
|
|
|
1286
1514
|
* work" and "Queue" on one list). The route name alone lit both. An
|
|
1287
1515
|
* entry with a `query` is active only when the address carries that
|
|
1288
1516
|
* query; an entry without one steps back when a sibling's query
|
|
1289
|
-
* matches, so exactly one of them is marked.
|
|
1517
|
+
* matches, so exactly one of them is marked. `params` work the
|
|
1518
|
+
* same way against the route's params, so of several entries on a
|
|
1519
|
+
* `/items/:slug` route only the one whose `slug` is in the address
|
|
1520
|
+
* is marked.
|
|
1290
1521
|
*
|
|
1291
1522
|
* On a page below a list where that rule marks nothing, the one
|
|
1292
1523
|
* entry `subRouteParent` picks is active instead.
|
|
@@ -1305,9 +1536,9 @@ export default {
|
|
|
1305
1536
|
},
|
|
1306
1537
|
|
|
1307
1538
|
/**
|
|
1308
|
-
* The route and
|
|
1309
|
-
* page below a list. `subRouteParent` asks this to see whether
|
|
1310
|
-
* rule already marks an entry.
|
|
1539
|
+
* The route, query and params rule on its own, without the fallback
|
|
1540
|
+
* for a page below a list. `subRouteParent` asks this to see whether
|
|
1541
|
+
* the rule already marks an entry.
|
|
1311
1542
|
*
|
|
1312
1543
|
* @param {object} item Menu entry.
|
|
1313
1544
|
* @return {boolean} True when the rule marks the entry.
|
|
@@ -1321,17 +1552,155 @@ export default {
|
|
|
1321
1552
|
if (item.route !== this.activeRouteName) {
|
|
1322
1553
|
return false
|
|
1323
1554
|
}
|
|
1324
|
-
if (
|
|
1325
|
-
return this.
|
|
1326
|
-
}
|
|
1327
|
-
const siblings = []
|
|
1328
|
-
for (const entry of this.visibleItems) {
|
|
1329
|
-
siblings.push(entry, ...this.visibleChildren(entry))
|
|
1555
|
+
if (this.isNarrowed(item)) {
|
|
1556
|
+
return this.narrowingMatches(item)
|
|
1330
1557
|
}
|
|
1331
|
-
return !
|
|
1558
|
+
return !this.flatEntries.some((entry) => entry !== item
|
|
1332
1559
|
&& entry.route === item.route
|
|
1333
|
-
&&
|
|
1334
|
-
&& this.
|
|
1560
|
+
&& this.isNarrowed(entry)
|
|
1561
|
+
&& this.narrowingMatches(entry))
|
|
1562
|
+
},
|
|
1563
|
+
|
|
1564
|
+
/**
|
|
1565
|
+
* Whether an entry narrows its route with a `query` or `params`.
|
|
1566
|
+
*
|
|
1567
|
+
* @param {object} item Menu entry.
|
|
1568
|
+
* @return {boolean} True when the entry declares either.
|
|
1569
|
+
*/
|
|
1570
|
+
isNarrowed(item) {
|
|
1571
|
+
return Boolean(item.query && typeof item.query === 'object') || this.hasParams(item)
|
|
1572
|
+
},
|
|
1573
|
+
|
|
1574
|
+
/**
|
|
1575
|
+
* Whether the address carries every `query` key and every `params`
|
|
1576
|
+
* key the entry declares.
|
|
1577
|
+
*
|
|
1578
|
+
* @param {object} item Menu entry that narrows its route.
|
|
1579
|
+
* @return {boolean} True when everything it declares matches.
|
|
1580
|
+
*/
|
|
1581
|
+
narrowingMatches(item) {
|
|
1582
|
+
const queryOk = !(item.query && typeof item.query === 'object') || this.queryMatches(item)
|
|
1583
|
+
const paramsOk = !this.hasParams(item) || this.paramsMatch(item)
|
|
1584
|
+
return queryOk && paramsOk
|
|
1585
|
+
},
|
|
1586
|
+
|
|
1587
|
+
/**
|
|
1588
|
+
* Whether an entry declares a non-empty `params` object.
|
|
1589
|
+
*
|
|
1590
|
+
* @param {object} item Menu entry.
|
|
1591
|
+
* @return {boolean} True when it has route params.
|
|
1592
|
+
*/
|
|
1593
|
+
hasParams(item) {
|
|
1594
|
+
return Boolean(item?.params) && typeof item.params === 'object' && Object.keys(item.params).length > 0
|
|
1595
|
+
},
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* Whether the route's params carry every key of an entry's
|
|
1599
|
+
* `params`. Values are compared as strings.
|
|
1600
|
+
*
|
|
1601
|
+
* @param {object} item Menu entry with `params`.
|
|
1602
|
+
* @return {boolean} True when every declared key matches.
|
|
1603
|
+
*/
|
|
1604
|
+
paramsMatch(item) {
|
|
1605
|
+
const current = this.$route?.params ?? {}
|
|
1606
|
+
return Object.keys(item.params).every((key) => String(current[key] ?? '') === String(item.params[key]))
|
|
1607
|
+
},
|
|
1608
|
+
|
|
1609
|
+
/**
|
|
1610
|
+
* The segments of an entry's page path with its `params` filled in,
|
|
1611
|
+
* as plain values. A missing optional `:name?` segment is dropped;
|
|
1612
|
+
* null when a required one has no param, or without a page.
|
|
1613
|
+
*
|
|
1614
|
+
* @param {object} item Menu entry with `params`.
|
|
1615
|
+
* @return {Array<string>|null} The segments, or null.
|
|
1616
|
+
*/
|
|
1617
|
+
filledPageSegments(item) {
|
|
1618
|
+
const pagePath = this.pageForItem(item)?.route
|
|
1619
|
+
if (!pagePath) {
|
|
1620
|
+
return null
|
|
1621
|
+
}
|
|
1622
|
+
const segments = []
|
|
1623
|
+
for (const part of pagePath.split('/').filter(Boolean)) {
|
|
1624
|
+
const param = PARAM_SEGMENT.exec(part)
|
|
1625
|
+
if (!param) {
|
|
1626
|
+
segments.push(part)
|
|
1627
|
+
continue
|
|
1628
|
+
}
|
|
1629
|
+
const value = item.params?.[param[1]]
|
|
1630
|
+
if (isFilled(value)) {
|
|
1631
|
+
segments.push(String(value))
|
|
1632
|
+
} else if (!isOptionalParam(param)) {
|
|
1633
|
+
return null
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
return segments
|
|
1637
|
+
},
|
|
1638
|
+
|
|
1639
|
+
/**
|
|
1640
|
+
* The required route param the router cannot fill when it resolves
|
|
1641
|
+
* an entry's link on its manifest page. vue-router fills a named
|
|
1642
|
+
* link's required params from the current route, then overlays every
|
|
1643
|
+
* key the entry declares, empty or null included. So a declared key
|
|
1644
|
+
* decides on its own value, and only an undeclared one falls back
|
|
1645
|
+
* to the current route. Null without a known page, or when every
|
|
1646
|
+
* required segment is filled. Reads `$route`, so it follows
|
|
1647
|
+
* navigation.
|
|
1648
|
+
*
|
|
1649
|
+
* @param {object} item Menu entry.
|
|
1650
|
+
* @return {string|null} The param name, or null.
|
|
1651
|
+
*/
|
|
1652
|
+
unfilledRouteParam(item) {
|
|
1653
|
+
const pagePath = this.pageForItem(item)?.route
|
|
1654
|
+
if (!pagePath) {
|
|
1655
|
+
return null
|
|
1656
|
+
}
|
|
1657
|
+
const declared = item.params && typeof item.params === 'object' ? item.params : {}
|
|
1658
|
+
const current = this.$route?.params ?? {}
|
|
1659
|
+
for (const part of pagePath.split('/')) {
|
|
1660
|
+
const param = PARAM_SEGMENT.exec(part)
|
|
1661
|
+
if (!param || isOptionalParam(param)) {
|
|
1662
|
+
continue
|
|
1663
|
+
}
|
|
1664
|
+
const value = param[1] in declared ? declared[param[1]] : current[param[1]]
|
|
1665
|
+
if (!isFilled(value)) {
|
|
1666
|
+
return param[1]
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
return null
|
|
1670
|
+
},
|
|
1671
|
+
|
|
1672
|
+
/**
|
|
1673
|
+
* How deep a path sits on an entry's page path: the page path's
|
|
1674
|
+
* segment count when the path is that page or below it, else 0.
|
|
1675
|
+
* A `/` page path, or one with an unfilled `:name`, never matches.
|
|
1676
|
+
*
|
|
1677
|
+
* Without `params` the page path is compared as written. With
|
|
1678
|
+
* `params`, segments are compared decoded, because the router
|
|
1679
|
+
* percent-encodes param values in the path and the entry holds them
|
|
1680
|
+
* plain.
|
|
1681
|
+
*
|
|
1682
|
+
* @param {object} item Menu entry.
|
|
1683
|
+
* @param {string|undefined} path The path to test.
|
|
1684
|
+
* @return {number} The depth of the match, or 0.
|
|
1685
|
+
*/
|
|
1686
|
+
pageMatchDepth(item, path) {
|
|
1687
|
+
if (!path) {
|
|
1688
|
+
return 0
|
|
1689
|
+
}
|
|
1690
|
+
if (!this.hasParams(item)) {
|
|
1691
|
+
const pagePath = this.pageForItem(item)?.route
|
|
1692
|
+
if (!pagePath || pagePath === '/' || pagePath.includes(':')) {
|
|
1693
|
+
return 0
|
|
1694
|
+
}
|
|
1695
|
+
const below = path === pagePath || path.startsWith(pagePath + '/')
|
|
1696
|
+
return below ? pagePath.split('/').filter(Boolean).length : 0
|
|
1697
|
+
}
|
|
1698
|
+
const segments = this.filledPageSegments(item)
|
|
1699
|
+
if (!segments || segments.length === 0) {
|
|
1700
|
+
return 0
|
|
1701
|
+
}
|
|
1702
|
+
const current = path.split('/').filter(Boolean).map(decodeSegment)
|
|
1703
|
+
return segments.every((segment, index) => current[index] === segment) ? segments.length : 0
|
|
1335
1704
|
},
|
|
1336
1705
|
|
|
1337
1706
|
/**
|
|
@@ -1396,6 +1765,9 @@ export default {
|
|
|
1396
1765
|
* `NcAppNavigationItem`).
|
|
1397
1766
|
*
|
|
1398
1767
|
* - Literal positive integer in `item.count` → return as-is.
|
|
1768
|
+
* - `item.count` is an object (`{ register, schema, filter? }`) →
|
|
1769
|
+
* `cnMenuItemCounts[item.id] ?? null`, the filtered total CnAppRoot
|
|
1770
|
+
* fetched for this entry.
|
|
1399
1771
|
* - `item.count === "auto"` → look up the entry's resolved page;
|
|
1400
1772
|
* when that page is `type: "index"` with a `register`/`schema`
|
|
1401
1773
|
* in its `config`, return
|
|
@@ -1419,6 +1791,11 @@ export default {
|
|
|
1419
1791
|
if (typeof raw === 'number') {
|
|
1420
1792
|
return raw > 0 ? raw : null
|
|
1421
1793
|
}
|
|
1794
|
+
if (raw && typeof raw === 'object') {
|
|
1795
|
+
// A filtered count: CnAppRoot fetched it per entry id.
|
|
1796
|
+
const value = this.cnMenuItemCounts?.[item.id]
|
|
1797
|
+
return (typeof value === 'number' && value > 0) ? value : null
|
|
1798
|
+
}
|
|
1422
1799
|
if (raw !== 'auto') {
|
|
1423
1800
|
return null
|
|
1424
1801
|
}
|
|
@@ -1454,6 +1831,27 @@ export default {
|
|
|
1454
1831
|
console.warn(`[CnAppNav] Menu entry "${item.id}" declares count: "auto" but has no resolvable index-type page with register + schema config — no badge will render.`)
|
|
1455
1832
|
},
|
|
1456
1833
|
|
|
1834
|
+
/**
|
|
1835
|
+
* One-shot `console.warn` per menu entry whose link cannot fill a
|
|
1836
|
+
* required route param. Keyed by `id`, else route and label, so
|
|
1837
|
+
* entries without an id each get their own warning.
|
|
1838
|
+
*
|
|
1839
|
+
* @param {{ id?: string, route: string, label?: string }} item Menu entry descriptor.
|
|
1840
|
+
* @param {string} param The unfilled param name.
|
|
1841
|
+
* @return {void}
|
|
1842
|
+
* @private
|
|
1843
|
+
*/
|
|
1844
|
+
warnUnfilledRouteParam(item, param) {
|
|
1845
|
+
const key = item.id ?? `${item.route}|${item.label ?? ''}`
|
|
1846
|
+
if (this._unfilledParamWarned.has(key)) {
|
|
1847
|
+
return
|
|
1848
|
+
}
|
|
1849
|
+
this._unfilledParamWarned.add(key)
|
|
1850
|
+
const name = item.id ?? item.label ?? item.route
|
|
1851
|
+
// eslint-disable-next-line no-console
|
|
1852
|
+
console.warn(`[CnAppNav] Menu entry "${name}" cannot fill the required route param "${param}" of page "${item.route}" from its params or the current route — the entry renders without a link.`)
|
|
1853
|
+
},
|
|
1854
|
+
|
|
1457
1855
|
/**
|
|
1458
1856
|
* Look up an item's resolved page (`pages[]` entry whose `id`
|
|
1459
1857
|
* matches the menu item's `route`) — used to decide whether the
|
|
@@ -1476,11 +1874,17 @@ export default {
|
|
|
1476
1874
|
* items (`action: "user-settings"`) and `href` items return
|
|
1477
1875
|
* `null` so the entry is NOT a vue-router link: action items
|
|
1478
1876
|
* fall through to the click handler, `href` items render a real
|
|
1479
|
-
* anchor via `itemHref`. Route items return a named route
|
|
1877
|
+
* anchor via `itemHref`. Route items return a named route that
|
|
1878
|
+
* carries the item's `params` and `query` when it declares them.
|
|
1879
|
+
*
|
|
1880
|
+
* An item whose manifest page has a required segment that neither
|
|
1881
|
+
* its `params` nor the current route fill also returns null, with a
|
|
1882
|
+
* one-time warning: the router throws resolving such a link, which
|
|
1883
|
+
* would take the whole navigation down rather than that one entry.
|
|
1480
1884
|
*
|
|
1481
1885
|
* @param {object} item Menu item being rendered.
|
|
1482
|
-
* @return {object|null} A `{ name }` route object,
|
|
1483
|
-
* action / href / route-less items.
|
|
1886
|
+
* @return {object|null} A `{ name, params?, query? }` route object,
|
|
1887
|
+
* or null for action / href / route-less / unfillable items.
|
|
1484
1888
|
*/
|
|
1485
1889
|
itemTo(item) {
|
|
1486
1890
|
if (item.action) {
|
|
@@ -1492,9 +1896,22 @@ export default {
|
|
|
1492
1896
|
if (!item.route) {
|
|
1493
1897
|
return null
|
|
1494
1898
|
}
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1899
|
+
const unfilled = this.unfilledRouteParam(item)
|
|
1900
|
+
if (unfilled) {
|
|
1901
|
+
this.warnUnfilledRouteParam(item, unfilled)
|
|
1902
|
+
return null
|
|
1903
|
+
}
|
|
1904
|
+
const to = { name: item.route }
|
|
1905
|
+
// Params fill a parameterised route (one entry per catalog →
|
|
1906
|
+
// /catalogs/:slug); query deep-links to a pre-filtered index page
|
|
1907
|
+
// (one entry per case type → Cases?caseType=…).
|
|
1908
|
+
if (this.hasParams(item)) {
|
|
1909
|
+
to.params = item.params
|
|
1910
|
+
}
|
|
1911
|
+
if (item.query) {
|
|
1912
|
+
to.query = item.query
|
|
1913
|
+
}
|
|
1914
|
+
return to
|
|
1498
1915
|
},
|
|
1499
1916
|
|
|
1500
1917
|
/**
|
|
@@ -1646,6 +2063,11 @@ export default {
|
|
|
1646
2063
|
this.cnReplayWalkthrough(item.tourId)
|
|
1647
2064
|
return
|
|
1648
2065
|
}
|
|
2066
|
+
// An entry left without a link (see itemTo) renders a `#` anchor;
|
|
2067
|
+
// following it would change the address, so the click goes nowhere.
|
|
2068
|
+
if (!item.href && this.unfilledRouteParam(item) && event && typeof event.preventDefault === 'function') {
|
|
2069
|
+
event.preventDefault()
|
|
2070
|
+
}
|
|
1649
2071
|
if (this.visibleChildren(item).length === 0) {
|
|
1650
2072
|
return
|
|
1651
2073
|
}
|
|
@@ -1684,6 +2106,39 @@ export default {
|
|
|
1684
2106
|
*
|
|
1685
2107
|
* @return {void}
|
|
1686
2108
|
*/
|
|
2109
|
+
/**
|
|
2110
|
+
* Re-emit the object an `open-form` primary action created, so the
|
|
2111
|
+
* host can refresh a list or open the record.
|
|
2112
|
+
*
|
|
2113
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-navigation-primary-action-runs-a-page-action
|
|
2114
|
+
* @param {object} created The created object.
|
|
2115
|
+
* @return {void}
|
|
2116
|
+
*/
|
|
2117
|
+
onPrimaryActionCreated(created) {
|
|
2118
|
+
/**
|
|
2119
|
+
* @event primary-action-created Emitted after an `open-form` primary action saves. Payload: the created object.
|
|
2120
|
+
*/
|
|
2121
|
+
this.$emit('primary-action-created', created)
|
|
2122
|
+
},
|
|
2123
|
+
|
|
2124
|
+
/**
|
|
2125
|
+
* A card link that is a button (an `action` id) emits `card-action`
|
|
2126
|
+
* with that id for the host to run; links and anchors navigate on
|
|
2127
|
+
* their own.
|
|
2128
|
+
*
|
|
2129
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-navigation-carries-a-card-and-a-help-entry
|
|
2130
|
+
* @return {void}
|
|
2131
|
+
*/
|
|
2132
|
+
onCardLinkClick() {
|
|
2133
|
+
const card = this.resolvedCard
|
|
2134
|
+
if (card && card.action) {
|
|
2135
|
+
/**
|
|
2136
|
+
* @event card-action Emitted when the nav card's link is an action button. Payload: the action id.
|
|
2137
|
+
*/
|
|
2138
|
+
this.$emit('card-action', card.action)
|
|
2139
|
+
}
|
|
2140
|
+
},
|
|
2141
|
+
|
|
1687
2142
|
onPrimaryActionClick() {
|
|
1688
2143
|
const action = this.activePrimaryAction
|
|
1689
2144
|
if (!action) {
|
|
@@ -1713,6 +2168,76 @@ export default {
|
|
|
1713
2168
|
</script>
|
|
1714
2169
|
|
|
1715
2170
|
<style>
|
|
2171
|
+
/* The emblem beside the app name (`nav.brand.emblem`). A theme sets
|
|
2172
|
+
--nldesign-emblem-url for `emblem: true`, and --cn-nav-emblem-size for both. */
|
|
2173
|
+
.cn-app-nav__brand-emblem {
|
|
2174
|
+
flex: none;
|
|
2175
|
+
height: var(--cn-nav-emblem-size, 34px);
|
|
2176
|
+
width: auto;
|
|
2177
|
+
object-fit: contain;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
.cn-app-nav__brand-emblem--theme {
|
|
2181
|
+
display: inline-block;
|
|
2182
|
+
width: var(--cn-nav-emblem-size, 34px);
|
|
2183
|
+
background: var(--nldesign-emblem-url) center / contain no-repeat;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
/* The solid primary action (`solid: true`, or an action that dispatches):
|
|
2187
|
+
one full-width primary button, as the board draws "New case". */
|
|
2188
|
+
.cn-app-nav__primary-action--solid .cn-action-buttons,
|
|
2189
|
+
.cn-app-nav__primary-action--solid .cn-action-buttons .button-vue,
|
|
2190
|
+
.cn-app-nav__primary-action--solid .button-vue {
|
|
2191
|
+
width: 100%;
|
|
2192
|
+
justify-content: center;
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
/* The card above the footer entries (`nav.card`). Theme hooks:
|
|
2196
|
+
--cn-nav-card-background and --cn-nav-card-radius. */
|
|
2197
|
+
.cn-app-nav__card {
|
|
2198
|
+
display: flex;
|
|
2199
|
+
flex-direction: column;
|
|
2200
|
+
gap: calc(2 * var(--default-grid-baseline));
|
|
2201
|
+
margin: calc(2 * var(--default-grid-baseline));
|
|
2202
|
+
padding: calc(4 * var(--default-grid-baseline));
|
|
2203
|
+
border-radius: var(--cn-nav-card-radius, var(--border-radius-large));
|
|
2204
|
+
background: var(--cn-nav-card-background, var(--color-background-hover));
|
|
2205
|
+
}
|
|
2206
|
+
|
|
2207
|
+
.cn-app-nav__card-title {
|
|
2208
|
+
color: var(--color-main-text);
|
|
2209
|
+
font-weight: 700;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
.cn-app-nav__card-text {
|
|
2213
|
+
color: var(--color-text-maxcontrast);
|
|
2214
|
+
line-height: 1.4;
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
.cn-app-nav__card-link {
|
|
2218
|
+
align-self: flex-start;
|
|
2219
|
+
margin: 0;
|
|
2220
|
+
padding: 0;
|
|
2221
|
+
border: 0;
|
|
2222
|
+
background: none;
|
|
2223
|
+
color: var(--color-primary-element);
|
|
2224
|
+
font: inherit;
|
|
2225
|
+
font-weight: 600;
|
|
2226
|
+
text-decoration: none;
|
|
2227
|
+
cursor: pointer;
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
.cn-app-nav__card-link:hover,
|
|
2231
|
+
.cn-app-nav__card-link:focus-visible {
|
|
2232
|
+
text-decoration: underline;
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
.cn-app-nav__card-link:focus-visible {
|
|
2236
|
+
outline: 2px solid var(--color-primary-element);
|
|
2237
|
+
outline-offset: 2px;
|
|
2238
|
+
border-radius: var(--border-radius-small, 4px);
|
|
2239
|
+
}
|
|
2240
|
+
|
|
1716
2241
|
/* The brand block at the top of the navigation. */
|
|
1717
2242
|
.cn-app-nav__brand {
|
|
1718
2243
|
align-items: center;
|