@conduction/nextcloud-vue 2.63.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/CnAppNav/CnAppNav.vue2.js +154 -7
- 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 +87 -0
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +26 -24
- 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/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 +150 -133
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +37 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +12 -4
- 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/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 +30 -0
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +5 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.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/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/l10n/en.json.js +2 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +2 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +331 -81
- package/dist/esm/utils/schemaRefSlug.js +19 -3
- package/dist/esm/utils/schemaRefSlug.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +6933 -5958
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +331 -81
- package/l10n/en.json +2 -0
- package/l10n/nl.json +2 -0
- package/package.json +1 -1
- package/src/components/CnAppNav/CnAppNav.vue +297 -11
- package/src/components/CnAppRoot/CnAppRoot.md +4 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +94 -1
- 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/CnDashboardPage/CnDashboardPage.vue +43 -2
- package/src/components/CnDataTable/CnDataTable.vue +45 -1
- package/src/components/CnDetailPage/CnDetailPage.md +4 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +60 -1
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +116 -6
- package/src/components/CnIndexPage/CnIndexPage.vue +32 -2
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +7 -1
- 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/CnWidgetWrapper/CnWidgetWrapper.md +4 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +107 -1
- package/src/css/detail-page.css +10 -4
- package/src/css/kpi-card.css +24 -1
- package/src/css/table.css +18 -6
- package/src/schemas/app-manifest-v2.schema.json +113 -3
- package/src/utils/schemaRefSlug.js +19 -3
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -69,12 +69,27 @@ export default {
|
|
|
69
69
|
components: { CnDetailCard, NcLoadingIcon },
|
|
70
70
|
|
|
71
71
|
props: {
|
|
72
|
-
/** OpenRegister register id (slug or uuid). */
|
|
73
|
-
register: { type: String,
|
|
74
|
-
/** OpenRegister schema id (slug or uuid). */
|
|
75
|
-
schema: { type: String,
|
|
76
|
-
/** Parent object id. */
|
|
77
|
-
objectId: { type: String,
|
|
72
|
+
/** OpenRegister register id (slug or uuid). Optional for `scope: "app"`. */
|
|
73
|
+
register: { type: String, default: '' },
|
|
74
|
+
/** OpenRegister schema id (slug or uuid). Optional for `scope: "app"`. */
|
|
75
|
+
schema: { type: String, default: '' },
|
|
76
|
+
/** Parent object id. Required for `scope: "object"` (the default); unused for `scope: "app"`. */
|
|
77
|
+
objectId: { type: String, default: '' },
|
|
78
|
+
/**
|
|
79
|
+
* What the card lists. `object` (the default) is one object's trail
|
|
80
|
+
* and needs `register`, `schema` and `objectId`. `app` is the
|
|
81
|
+
* app-wide feed: the entries of every object the caller may read
|
|
82
|
+
* (OpenRegister's `/audit-trails/readable`), narrowed to `register`
|
|
83
|
+
* and `schema` when those are set.
|
|
84
|
+
*
|
|
85
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed
|
|
86
|
+
*/
|
|
87
|
+
scope: {
|
|
88
|
+
type: String,
|
|
89
|
+
default: 'object',
|
|
90
|
+
validator: (value) => ['object', 'app'].includes(value),
|
|
91
|
+
},
|
|
92
|
+
|
|
78
93
|
/** Rendering surface — passed for AD-19 surface fallback consumers. */
|
|
79
94
|
surface: {
|
|
80
95
|
type: String,
|
|
@@ -116,13 +131,38 @@ export default {
|
|
|
116
131
|
displayedEntries() {
|
|
117
132
|
return this.entries.slice(0, this.maxDisplay)
|
|
118
133
|
},
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The path the card reads (prefixed at fetch time), or '' when it has nothing to read: the
|
|
137
|
+
* object's own trail, or the app-wide readable feed narrowed to the
|
|
138
|
+
* register and schema when set.
|
|
139
|
+
*
|
|
140
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed
|
|
141
|
+
* @return {string}
|
|
142
|
+
*/
|
|
143
|
+
fetchUrl() {
|
|
144
|
+
const params = new URLSearchParams({ limit: String(this.maxDisplay) })
|
|
145
|
+
if (this.scope === 'app') {
|
|
146
|
+
if (this.register) {
|
|
147
|
+
params.set('register', this.register)
|
|
148
|
+
}
|
|
149
|
+
if (this.schema) {
|
|
150
|
+
params.set('schema', this.schema)
|
|
151
|
+
}
|
|
152
|
+
return `${this.apiBase}/audit-trails/readable?${params.toString()}`
|
|
153
|
+
}
|
|
154
|
+
if (!this.register || !this.schema || !this.objectId) {
|
|
155
|
+
return ''
|
|
156
|
+
}
|
|
157
|
+
return `${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trail?${params.toString()}`
|
|
158
|
+
},
|
|
119
159
|
},
|
|
120
160
|
|
|
121
161
|
watch: {
|
|
122
|
-
|
|
162
|
+
fetchUrl: {
|
|
123
163
|
immediate: true,
|
|
124
|
-
handler(
|
|
125
|
-
if (
|
|
164
|
+
handler(url) {
|
|
165
|
+
if (url) {
|
|
126
166
|
this.fetchEntries()
|
|
127
167
|
}
|
|
128
168
|
},
|
|
@@ -130,20 +170,25 @@ export default {
|
|
|
130
170
|
},
|
|
131
171
|
|
|
132
172
|
methods: {
|
|
173
|
+
/**
|
|
174
|
+
* Read the entries from `fetchUrl`. The readable feed answers
|
|
175
|
+
* `{ rows, nextCursor }`, the object trail `{ results }`.
|
|
176
|
+
*
|
|
177
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed
|
|
178
|
+
* @return {Promise<void>}
|
|
179
|
+
*/
|
|
133
180
|
async fetchEntries() {
|
|
134
|
-
|
|
181
|
+
const url = this.fetchUrl
|
|
182
|
+
if (!url) {
|
|
135
183
|
return
|
|
136
184
|
}
|
|
137
185
|
this.loading = true
|
|
138
186
|
try {
|
|
139
|
-
const
|
|
140
|
-
const response = await fetch(
|
|
141
|
-
prefixUrl(`${this.apiBase}/objects/${this.register}/${this.schema}/${this.objectId}/audit-trail?${params.toString()}`),
|
|
142
|
-
{ headers: buildHeaders() },
|
|
143
|
-
)
|
|
187
|
+
const response = await fetch(prefixUrl(url), { headers: buildHeaders() })
|
|
144
188
|
if (response.ok) {
|
|
145
189
|
const data = await response.json()
|
|
146
|
-
|
|
190
|
+
const list = data.results || data.rows || data
|
|
191
|
+
this.entries = Array.isArray(list) ? list : []
|
|
147
192
|
} else {
|
|
148
193
|
this.entries = []
|
|
149
194
|
}
|
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
7
|
<CnAuditTrailCard
|
|
8
|
-
v-if="resolvedRegister && resolvedSchema && resolvedObjectId"
|
|
8
|
+
v-if="resolvedScope === 'app' || (resolvedRegister && resolvedSchema && resolvedObjectId)"
|
|
9
9
|
:register="resolvedRegister"
|
|
10
10
|
:schema="resolvedSchema"
|
|
11
11
|
:objectId="resolvedObjectId"
|
|
12
|
+
:scope="resolvedScope"
|
|
12
13
|
:title="resolvedTitle"
|
|
13
14
|
:maxDisplay="resolvedMaxDisplay" />
|
|
14
15
|
</template>
|
|
@@ -70,6 +71,15 @@ export default {
|
|
|
70
71
|
title: { type: String, default: '' },
|
|
71
72
|
/** Maximum audit rows to render (0 falls back to the card default). */
|
|
72
73
|
maxDisplay: { type: Number, default: 0 },
|
|
74
|
+
/**
|
|
75
|
+
* `app` lists the app-wide feed (every object the caller may read,
|
|
76
|
+
* narrowed to `register` / `schema` when set) and needs no object;
|
|
77
|
+
* `object` (the default) is one object's trail. Also read from
|
|
78
|
+
* `content.scope`.
|
|
79
|
+
*
|
|
80
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed
|
|
81
|
+
*/
|
|
82
|
+
scope: { type: String, default: '' },
|
|
73
83
|
/** Stored widget content blob (CnDashboardPage registry branch): `{ title, maxDisplay, register?, schema?, objectId? }`. */
|
|
74
84
|
content: { type: Object, default: () => ({}) },
|
|
75
85
|
},
|
|
@@ -115,6 +125,17 @@ export default {
|
|
|
115
125
|
return typeof s === 'string' ? s : (s && (s.slug || s.name || s.id)) || ''
|
|
116
126
|
},
|
|
117
127
|
|
|
128
|
+
/**
|
|
129
|
+
* The feed scope: explicit prop → content → `object`.
|
|
130
|
+
*
|
|
131
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-the-audit-trail-widget-reads-the-app-wide-feed
|
|
132
|
+
* @return {('object'|'app')}
|
|
133
|
+
*/
|
|
134
|
+
resolvedScope() {
|
|
135
|
+
const s = this.scope || this.content.scope
|
|
136
|
+
return s === 'app' ? 'app' : 'object'
|
|
137
|
+
},
|
|
138
|
+
|
|
118
139
|
/**
|
|
119
140
|
* The card title: explicit prop → content → '' (card default).
|
|
120
141
|
*
|
|
@@ -177,6 +177,9 @@ import { safeHref } from '../../utils/safeHref.js'
|
|
|
177
177
|
import { formatValue } from '../../utils/schema.js'
|
|
178
178
|
import { CnStatusBadge } from '../CnStatusBadge/index.js'
|
|
179
179
|
|
|
180
|
+
/** A uuid in any version, the shape OpenRegister ids take. */
|
|
181
|
+
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
|
182
|
+
|
|
180
183
|
/**
|
|
181
184
|
* Module-level set of column keys already warned about for a
|
|
182
185
|
* `widget:"link"` declaration with no resolvable target — guarantees
|
|
@@ -483,13 +486,20 @@ export default {
|
|
|
483
486
|
*
|
|
484
487
|
* @return {string}
|
|
485
488
|
* @spec openspec/changes/workplace-dashboard-primitives/specs/workplace-dashboard-primitives/spec.md#requirement-avatar-and-date-cells
|
|
489
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-small-opt-ins-from-the-decidiq-and-learniq-lanes
|
|
486
490
|
*/
|
|
487
491
|
dateLabel() {
|
|
488
492
|
const date = parseDateValue(this.value)
|
|
489
493
|
if (date === null) {
|
|
490
494
|
return ''
|
|
491
495
|
}
|
|
492
|
-
|
|
496
|
+
// `widgetProps.showTime: true` adds the time of day ("5 Oct 2026,
|
|
497
|
+
// 08:30"); `widgetProps.timeOnly: true` shows just the time
|
|
498
|
+
// ("08:30"), for a timetable row. Default: the date alone.
|
|
499
|
+
const props = this.widgetProps || {}
|
|
500
|
+
const options = props.timeOnly === true
|
|
501
|
+
? { hour: '2-digit', minute: '2-digit' }
|
|
502
|
+
: { day: 'numeric', month: 'short', year: 'numeric', ...(props.showTime === true ? { hour: '2-digit', minute: '2-digit' } : {}) }
|
|
493
503
|
let locale
|
|
494
504
|
try {
|
|
495
505
|
locale = getCanonicalLocale()
|
|
@@ -738,6 +748,27 @@ export default {
|
|
|
738
748
|
return undefined
|
|
739
749
|
},
|
|
740
750
|
|
|
751
|
+
/**
|
|
752
|
+
* Whether the cell draws a uuid, and so takes the monospace uuid
|
|
753
|
+
* style. The property's `format: "uuid"` alone used to decide this,
|
|
754
|
+
* so a reference that a formatter or a widget had resolved to its
|
|
755
|
+
* name was still drawn as code. Now the value has to be a uuid AND
|
|
756
|
+
* nothing resolves it: a resolving widget or formatter, or a value
|
|
757
|
+
* that is not a uuid, renders in the normal text style.
|
|
758
|
+
*
|
|
759
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-resolved-reference-is-not-drawn-as-a-uuid
|
|
760
|
+
* @return {boolean}
|
|
761
|
+
*/
|
|
762
|
+
showsUuid() {
|
|
763
|
+
if (this.property?.format !== 'uuid') {
|
|
764
|
+
return false
|
|
765
|
+
}
|
|
766
|
+
if (this.widgetComponent || this.widget || this.hasFormatter) {
|
|
767
|
+
return false
|
|
768
|
+
}
|
|
769
|
+
return typeof this.value === 'string' && UUID_PATTERN.test(this.value)
|
|
770
|
+
},
|
|
771
|
+
|
|
741
772
|
cellClass() {
|
|
742
773
|
const classes = []
|
|
743
774
|
if (this.propertyType === 'boolean') {
|
|
@@ -749,7 +780,7 @@ export default {
|
|
|
749
780
|
if (this.property?.format === 'date-time' || this.property?.format === 'date') {
|
|
750
781
|
classes.push('cn-cell-renderer--date')
|
|
751
782
|
}
|
|
752
|
-
if (this.
|
|
783
|
+
if (this.showsUuid) {
|
|
753
784
|
classes.push('cn-cell-renderer--uuid')
|
|
754
785
|
}
|
|
755
786
|
if (this.propertyType === 'integer' || this.propertyType === 'number') {
|
|
@@ -11,8 +11,14 @@
|
|
|
11
11
|
-->
|
|
12
12
|
<template>
|
|
13
13
|
<div class="cn-dashboard-page" data-testid="cn-dashboard-page">
|
|
14
|
-
<!-- Header
|
|
15
|
-
|
|
14
|
+
<!-- Header. `showHeader: false` drops the whole row (title, description,
|
|
15
|
+
header actions, edit toggle) for a page that opens with its own
|
|
16
|
+
heading, such as a greeting; the title then stays as a visually
|
|
17
|
+
hidden heading so the main landmark keeps its name. -->
|
|
18
|
+
<h2 v-if="!showHeader && title" class="hidden-visually" data-testid="cn-dashboard-page-hidden-title">
|
|
19
|
+
{{ resolvedTitle }}
|
|
20
|
+
</h2>
|
|
21
|
+
<div v-if="showHeader" class="cn-dashboard-page__header" data-testid="cn-dashboard-page-header">
|
|
16
22
|
<div class="cn-dashboard-page__header-left">
|
|
17
23
|
<div v-if="title || $slots['title-meta']" class="cn-dashboard-page__title-row">
|
|
18
24
|
<h2 v-if="title" class="cn-dashboard-page__title">
|
|
@@ -320,6 +326,7 @@
|
|
|
320
326
|
:borderless="widgetBorderless(item)"
|
|
321
327
|
:flush="item.flush !== false"
|
|
322
328
|
:buttons="getWidgetButtons(item)"
|
|
329
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
323
330
|
:styleConfig="item.styleConfig || {}"
|
|
324
331
|
:titleIconPosition="getWidgetTitleIconPosition(item)"
|
|
325
332
|
:titleIconColor="getWidgetTitleIconColor(item)"
|
|
@@ -400,6 +407,7 @@
|
|
|
400
407
|
:borderless="widgetBorderless(item)"
|
|
401
408
|
:flush="item.flush !== false"
|
|
402
409
|
:buttons="getWidgetButtons(item)"
|
|
410
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
403
411
|
:styleConfig="item.styleConfig || {}"
|
|
404
412
|
:titleIconPosition="getWidgetTitleIconPosition(item)"
|
|
405
413
|
:titleIconColor="getWidgetTitleIconColor(item)"
|
|
@@ -478,6 +486,7 @@
|
|
|
478
486
|
:flush="item.flush !== false"
|
|
479
487
|
:class="{ 'cn-dashboard-page__card-fit': isCardWidget(item) }"
|
|
480
488
|
:buttons="getWidgetButtons(item)"
|
|
489
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
481
490
|
:styleConfig="item.styleConfig || {}"
|
|
482
491
|
:documentationUrl="getWidgetDocumentationUrl(item)"
|
|
483
492
|
:docsAnchor="getWidgetDocsAnchor(item)"
|
|
@@ -502,6 +511,7 @@
|
|
|
502
511
|
:borderless="widgetBorderless(item)"
|
|
503
512
|
:flush="item.flush !== false"
|
|
504
513
|
:buttons="getWidgetButtons(item)"
|
|
514
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
505
515
|
:styleConfig="item.styleConfig || {}"
|
|
506
516
|
:titleIconPosition="getWidgetTitleIconPosition(item)"
|
|
507
517
|
:titleIconColor="getWidgetTitleIconColor(item)"
|
|
@@ -537,6 +547,7 @@
|
|
|
537
547
|
:iconClass="getWidgetIconClass(item)"
|
|
538
548
|
:showTitle="widgetShowTitle(item)"
|
|
539
549
|
:buttons="getWidgetButtons(item)"
|
|
550
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
540
551
|
:styleConfig="item.styleConfig || {}"
|
|
541
552
|
:showRefresh="getWidgetShowRefresh(item)"
|
|
542
553
|
:documentationUrl="getWidgetDocumentationUrl(item)"
|
|
@@ -566,6 +577,7 @@
|
|
|
566
577
|
:titleIconVariant="getWidgetTitleIconVariant(item)"
|
|
567
578
|
:class="{ 'cn-dashboard-page__card-fit': isCardWidget(item) }"
|
|
568
579
|
:buttons="getWidgetButtons(item)"
|
|
580
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
569
581
|
:styleConfig="item.styleConfig || {}"
|
|
570
582
|
:documentationUrl="getWidgetDocumentationUrl(item)"
|
|
571
583
|
:docsAnchor="getWidgetDocsAnchor(item)"
|
|
@@ -921,6 +933,19 @@ export default {
|
|
|
921
933
|
default: '',
|
|
922
934
|
},
|
|
923
935
|
|
|
936
|
+
/**
|
|
937
|
+
* Whether the header row renders: title, description, header actions
|
|
938
|
+
* and the edit toggle. `false` (manifest `config.showHeader: false`)
|
|
939
|
+
* drops it for a page whose first widget is its heading; the title
|
|
940
|
+
* then renders visually hidden so the page keeps an accessible name.
|
|
941
|
+
*
|
|
942
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-dashboard-page-can-hide-its-header
|
|
943
|
+
*/
|
|
944
|
+
showHeader: {
|
|
945
|
+
type: Boolean,
|
|
946
|
+
default: true,
|
|
947
|
+
},
|
|
948
|
+
|
|
924
949
|
/**
|
|
925
950
|
* Widget definitions array. Each widget defines metadata for rendering.
|
|
926
951
|
*
|
|
@@ -3410,6 +3435,22 @@ export default {
|
|
|
3410
3435
|
return def?.buttons || []
|
|
3411
3436
|
},
|
|
3412
3437
|
|
|
3438
|
+
/**
|
|
3439
|
+
* The widget's header text link (`headerLink` on the layout entry,
|
|
3440
|
+
* else on the widget definition), forwarded to CnWidgetWrapper.
|
|
3441
|
+
*
|
|
3442
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link
|
|
3443
|
+
* @param {object} item Layout placement entry.
|
|
3444
|
+
* @return {object|null}
|
|
3445
|
+
*/
|
|
3446
|
+
getWidgetHeaderLink(item) {
|
|
3447
|
+
if (item?.headerLink && typeof item.headerLink === 'object') {
|
|
3448
|
+
return item.headerLink
|
|
3449
|
+
}
|
|
3450
|
+
const def = this.getWidgetDef(item.widgetId)
|
|
3451
|
+
return (def?.headerLink && typeof def.headerLink === 'object') ? def.headerLink : null
|
|
3452
|
+
},
|
|
3453
|
+
|
|
3413
3454
|
/**
|
|
3414
3455
|
* Whether a widget's overflow Actions menu shows the Refresh item.
|
|
3415
3456
|
* An explicit per-widget flag wins — on the widget definition or
|
|
@@ -278,6 +278,14 @@
|
|
|
278
278
|
:row="row"
|
|
279
279
|
:rowKey="rowKey" />
|
|
280
280
|
</slot>
|
|
281
|
+
<!-- A second, muted line under the value, from `col.secondary`:
|
|
282
|
+
a field key, a "{field} · {other}" template, or a function
|
|
283
|
+
of the row. Lets one column read "title" over "number ·
|
|
284
|
+
requester" without a custom cell. -->
|
|
285
|
+
<span
|
|
286
|
+
v-if="secondaryValue(row, col)"
|
|
287
|
+
class="cn-table-cell__secondary"
|
|
288
|
+
data-testid="cn-cell-secondary">{{ secondaryValue(row, col) }}</span>
|
|
281
289
|
</td>
|
|
282
290
|
|
|
283
291
|
<!-- Row actions -->
|
|
@@ -444,7 +452,13 @@ export default {
|
|
|
444
452
|
* reader is looking. `columnsFromSchema` fills it from the JSON Schema property
|
|
445
453
|
* description automatically, so schema-driven tables get it for free.
|
|
446
454
|
*
|
|
447
|
-
*
|
|
455
|
+
* `secondary` draws a second, muted line under the cell's value: a field
|
|
456
|
+
* key (`"number"`), a template with `{field}` placeholders
|
|
457
|
+
* (`"{number} · {requester}"`, dotted paths allowed), or a function of
|
|
458
|
+
* the row. Empty fields leave their placeholder blank; a line that
|
|
459
|
+
* resolves to nothing is not drawn.
|
|
460
|
+
*
|
|
461
|
+
* @type {Array<{key: string, label: string, description: string, sortable: boolean, width: string, class: string, cellClass: string, secondary: (string|((row: object) => string))}|string>}
|
|
448
462
|
*/
|
|
449
463
|
columns: {
|
|
450
464
|
type: Array,
|
|
@@ -1415,6 +1429,36 @@ export default {
|
|
|
1415
1429
|
* @param {object} col The column definition.
|
|
1416
1430
|
* @return {unknown} The value handed to the slot / CnCellRenderer.
|
|
1417
1431
|
*/
|
|
1432
|
+
/**
|
|
1433
|
+
* The secondary line of a cell, from `col.secondary`: a function is
|
|
1434
|
+
* called with the row; a string with `{field}` placeholders is a
|
|
1435
|
+
* template filled from the row (dotted paths allowed); any other
|
|
1436
|
+
* string is a field key. Returns '' when there is nothing to draw.
|
|
1437
|
+
*
|
|
1438
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-table-column-carries-a-secondary-line
|
|
1439
|
+
* @param {object} row The row.
|
|
1440
|
+
* @param {object} col The column definition.
|
|
1441
|
+
* @return {string} The secondary text, or ''.
|
|
1442
|
+
*/
|
|
1443
|
+
secondaryValue(row, col) {
|
|
1444
|
+
const spec = col?.secondary
|
|
1445
|
+
if (typeof spec === 'function') {
|
|
1446
|
+
const out = spec(row)
|
|
1447
|
+
return out === null || out === undefined ? '' : String(out)
|
|
1448
|
+
}
|
|
1449
|
+
if (typeof spec !== 'string' || spec === '') {
|
|
1450
|
+
return ''
|
|
1451
|
+
}
|
|
1452
|
+
const asText = (v) => (v === null || v === undefined ? '' : String(v))
|
|
1453
|
+
if (spec.includes('{')) {
|
|
1454
|
+
const filled = spec.replace(/\{([^}]+)\}/g, (_, key) => asText(this.getCellValue(row, key.trim())))
|
|
1455
|
+
// A template whose every field is empty leaves only separators:
|
|
1456
|
+
// nothing worth a line.
|
|
1457
|
+
return /[\p{L}\p{N}]/u.test(filled) ? filled.trim() : ''
|
|
1458
|
+
}
|
|
1459
|
+
return asText(this.getCellValue(row, spec))
|
|
1460
|
+
},
|
|
1461
|
+
|
|
1418
1462
|
cellValue(row, col) {
|
|
1419
1463
|
if (col && col.aggregate) {
|
|
1420
1464
|
const cached = this.aggregateValues[String(row[this.rowKey])]
|
|
@@ -430,3 +430,7 @@ Four levels of action and the case surfaces around them. All opt in. See [the re
|
|
|
430
430
|
## Not found
|
|
431
431
|
|
|
432
432
|
When the schema-driven fetch answers 404 the page shows a not-found state whose back button is a router link to `notFoundRoute` (default `null`, which means the app root `/`). `notFoundRouteLabel` names that page in the button's "Back to …" label.
|
|
433
|
+
|
|
434
|
+
### Type eyebrow and breadcrumb
|
|
435
|
+
|
|
436
|
+
`showTypeEyebrow` (default `true`) can drop the type label above the record name. `breadcrumb` (`{ label, route?, params?, href? }`) draws a breadcrumb line above the header: that crumb, then the record's name as the current crumb.
|
|
@@ -41,6 +41,13 @@
|
|
|
41
41
|
{{ skipLinkLabel }}
|
|
42
42
|
</a>
|
|
43
43
|
|
|
44
|
+
<!-- Breadcrumb line (manifest `config.breadcrumb`): the list the record
|
|
45
|
+
belongs to, then the record itself as the current crumb. -->
|
|
46
|
+
<CnBreadcrumbs
|
|
47
|
+
v-if="breadcrumbCrumbs.length > 0 && !objectNotFound"
|
|
48
|
+
class="cn-detail-page__breadcrumbs"
|
|
49
|
+
:crumbs="breadcrumbCrumbs"
|
|
50
|
+
data-testid="cn-detail-page-breadcrumbs" />
|
|
44
51
|
<!-- Header -->
|
|
45
52
|
<div v-if="!objectNotFound" class="cn-detail-page__header" data-testid="cn-detail-page-header">
|
|
46
53
|
<!-- Header (left block) — overridable via #header slot. Default
|
|
@@ -98,7 +105,7 @@
|
|
|
98
105
|
(ADR-062). Only shown once the object resolves to a
|
|
99
106
|
display name that differs from the type label. -->
|
|
100
107
|
<p
|
|
101
|
-
v-if="typeEyebrow"
|
|
108
|
+
v-if="typeEyebrow && showTypeEyebrow"
|
|
102
109
|
class="cn-detail-page__type-eyebrow"
|
|
103
110
|
data-testid="cn-detail-page-type-eyebrow">
|
|
104
111
|
{{ typeEyebrow }}
|
|
@@ -1025,6 +1032,7 @@ import {
|
|
|
1025
1032
|
widgetTitleOf,
|
|
1026
1033
|
} from '../../utils/widgetDispatch.js'
|
|
1027
1034
|
import { CnActionButtons } from '../CnActionButtons/index.js'
|
|
1035
|
+
import { CnBreadcrumbs } from '../CnBreadcrumbs/index.js'
|
|
1028
1036
|
import { CnIcon } from '../CnIcon/index.js'
|
|
1029
1037
|
import { CnStatusBadge } from '../CnStatusBadge/index.js'
|
|
1030
1038
|
import { getWidgetTypeEntry } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
|
|
@@ -1172,6 +1180,7 @@ export default {
|
|
|
1172
1180
|
CnDashboardGrid,
|
|
1173
1181
|
CnLifecycleActions,
|
|
1174
1182
|
CnActionButtons,
|
|
1183
|
+
CnBreadcrumbs,
|
|
1175
1184
|
CnStatusBadge,
|
|
1176
1185
|
CnSummaryAggregates,
|
|
1177
1186
|
CnRelatedCollections,
|
|
@@ -2078,6 +2087,34 @@ export default {
|
|
|
2078
2087
|
default: null,
|
|
2079
2088
|
},
|
|
2080
2089
|
|
|
2090
|
+
/**
|
|
2091
|
+
* Whether the type eyebrow (the type label above the record name)
|
|
2092
|
+
* renders once the record resolves. `false` (manifest
|
|
2093
|
+
* `config.showTypeEyebrow: false`) drops it for a header that says
|
|
2094
|
+
* the type in a pill instead.
|
|
2095
|
+
*
|
|
2096
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb
|
|
2097
|
+
*/
|
|
2098
|
+
showTypeEyebrow: {
|
|
2099
|
+
type: Boolean,
|
|
2100
|
+
default: true,
|
|
2101
|
+
},
|
|
2102
|
+
|
|
2103
|
+
/**
|
|
2104
|
+
* A breadcrumb line above the header (manifest `config.breadcrumb`):
|
|
2105
|
+
* `{ label, route?, params?, href? }` names the list the record
|
|
2106
|
+
* belongs to; the record's display name follows as the current
|
|
2107
|
+
* crumb. `label` goes through the host translate function. Null (the
|
|
2108
|
+
* default) draws no trail.
|
|
2109
|
+
*
|
|
2110
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb
|
|
2111
|
+
* @type {{label: string, route?: string, params?: object, href?: string}|null}
|
|
2112
|
+
*/
|
|
2113
|
+
breadcrumb: {
|
|
2114
|
+
type: Object,
|
|
2115
|
+
default: null,
|
|
2116
|
+
},
|
|
2117
|
+
|
|
2081
2118
|
/**
|
|
2082
2119
|
* A column of cards beside the body (manifest `config.sideColumn`),
|
|
2083
2120
|
* for the record's facts: deadline, requester, handler, links. Each
|
|
@@ -3201,6 +3238,28 @@ export default {
|
|
|
3201
3238
|
return this.objectDisplayName || this.resolvedTitle
|
|
3202
3239
|
},
|
|
3203
3240
|
|
|
3241
|
+
/**
|
|
3242
|
+
* The breadcrumb trail: the declared crumb (a router target from
|
|
3243
|
+
* `route` + `params`, or an `href`), then the record's display name
|
|
3244
|
+
* as the current crumb. Empty without a `breadcrumb` label.
|
|
3245
|
+
*
|
|
3246
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb
|
|
3247
|
+
* @return {Array<{label: string, to?: object, href?: string}>}
|
|
3248
|
+
*/
|
|
3249
|
+
breadcrumbCrumbs() {
|
|
3250
|
+
const crumb = this.breadcrumb
|
|
3251
|
+
if (!crumb || typeof crumb.label !== 'string' || crumb.label === '') {
|
|
3252
|
+
return []
|
|
3253
|
+
}
|
|
3254
|
+
const first = { label: this.effectiveTranslate(crumb.label) }
|
|
3255
|
+
if (crumb.route) {
|
|
3256
|
+
first.to = { name: crumb.route, ...(crumb.params ? { params: crumb.params } : {}) }
|
|
3257
|
+
} else if (crumb.href) {
|
|
3258
|
+
first.href = crumb.href
|
|
3259
|
+
}
|
|
3260
|
+
return [first, { label: this.displayTitle }]
|
|
3261
|
+
},
|
|
3262
|
+
|
|
3204
3263
|
/**
|
|
3205
3264
|
* Effective translate function: the injected `cnTranslate` (the host
|
|
3206
3265
|
* app's bound `t()`), identity by default.
|