@conduction/nextcloud-vue 2.63.0 → 2.65.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 +253 -10
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +139 -46
- 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/CnBreadcrumbs/CnBreadcrumbs.vue2.js +13 -0
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +2 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.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 +55 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +153 -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 +125 -14
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +87 -40
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +101 -7
- 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 +74 -5
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +15 -8
- 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 +495 -81
- package/dist/esm/store/useObjectStore.js +118 -33
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/actionsDispatcher.js +14 -0
- package/dist/esm/utils/actionsDispatcher.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/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +7388 -6021
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +495 -81
- package/l10n/en.json +2 -0
- package/l10n/nl.json +2 -0
- package/package.json +1 -1
- package/src/components/CnAppNav/CnAppNav.vue +441 -14
- 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/CnBreadcrumbs/CnBreadcrumbs.vue +14 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +33 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +72 -4
- package/src/components/CnDataTable/CnDataTable.vue +45 -1
- package/src/components/CnDetailPage/CnDetailPage.md +8 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +169 -2
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +147 -9
- package/src/components/CnIndexPage/CnIndexPage.vue +32 -2
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +7 -1
- package/src/components/CnStagesWidget/CnStagesWidget.vue +189 -2
- package/src/components/CnStagesWidget/stagesModel.js +5 -2
- package/src/components/CnStatWidget/CnStatWidget.vue +89 -7
- 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 +32 -4
- package/src/css/kpi-card.css +71 -1
- package/src/css/table.css +18 -6
- package/src/schemas/app-manifest-v2.schema.json +134 -3
- package/src/store/useObjectStore.js +118 -33
- package/src/utils/actionsDispatcher.js +14 -0
- package/src/utils/schemaRefSlug.js +19 -3
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -199,7 +199,13 @@
|
|
|
199
199
|
<!-- @binding {boolean} isOwner Whether the user owns this app (ADR-079 §3). -->
|
|
200
200
|
<!-- @binding {boolean} isAdmin Whether the user administers the instance; gates the Admin-settings link. -->
|
|
201
201
|
<!-- @binding {string} appId The app id used to build the Admin-settings target. -->
|
|
202
|
-
|
|
202
|
+
<!-- `hideMenu` renders no navigation at all, so the content starts
|
|
203
|
+
at the left edge. An EMPTY #menu override cannot do that: an
|
|
204
|
+
empty slot falls back to the default CnAppNav (ensureValidVNode),
|
|
205
|
+
which is why launchpad passed a hidden empty span. -->
|
|
206
|
+
<slot
|
|
207
|
+
v-if="!hideMenu"
|
|
208
|
+
name="menu"
|
|
203
209
|
:manifest="menuManifest"
|
|
204
210
|
:permissions="permissions"
|
|
205
211
|
:isOwner="isOwner"
|
|
@@ -700,6 +706,7 @@ import { BUILT_IN_FORMATTERS } from '../../utils/builtInFormatters.js'
|
|
|
700
706
|
import { DEFAULT_FORGE, resolveForge } from '../../utils/forge.js'
|
|
701
707
|
import { BUILT_IN_KB_PROVIDERS } from '../../utils/kbSearchProviders.js'
|
|
702
708
|
import { installModalStack, uninstallModalStack } from '../../utils/modalStack.js'
|
|
709
|
+
import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
703
710
|
import { passesContextPredicates } from '../../utils/visibleIfContext.js'
|
|
704
711
|
|
|
705
712
|
/**
|
|
@@ -1049,6 +1056,13 @@ export default {
|
|
|
1049
1056
|
* the counts.
|
|
1050
1057
|
*/
|
|
1051
1058
|
cnMenuCounts: this.cnMenuCounts,
|
|
1059
|
+
/**
|
|
1060
|
+
* Reactive `{ [menuItemId]: number }` totals for menu entries
|
|
1061
|
+
* whose `count` is an object (`{ register, schema, filter? }`),
|
|
1062
|
+
* populated by `_hydrateMenuItemCounts()`. Read by
|
|
1063
|
+
* `CnAppNav.resolveCount()`.
|
|
1064
|
+
*/
|
|
1065
|
+
cnMenuItemCounts: this.cnMenuItemCounts,
|
|
1052
1066
|
}
|
|
1053
1067
|
},
|
|
1054
1068
|
|
|
@@ -1192,6 +1206,19 @@ export default {
|
|
|
1192
1206
|
required: true,
|
|
1193
1207
|
},
|
|
1194
1208
|
|
|
1209
|
+
/**
|
|
1210
|
+
* Render no app navigation at all: neither the default CnAppNav nor
|
|
1211
|
+
* the `#menu` slot, so the content starts at the left edge (a start
|
|
1212
|
+
* page without a menu). Off by default; the navigation renders as it
|
|
1213
|
+
* always has.
|
|
1214
|
+
*
|
|
1215
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-an-app-root-can-hide-its-menu
|
|
1216
|
+
*/
|
|
1217
|
+
hideMenu: {
|
|
1218
|
+
type: Boolean,
|
|
1219
|
+
default: false,
|
|
1220
|
+
},
|
|
1221
|
+
|
|
1195
1222
|
/**
|
|
1196
1223
|
* Human-readable name shown in the Nextcloud top bar. When set it
|
|
1197
1224
|
* overrides the technical `appId` so a virtual app shows its own name
|
|
@@ -1707,6 +1734,13 @@ export default {
|
|
|
1707
1734
|
* picked up by CnAppNav's reactive render.
|
|
1708
1735
|
*/
|
|
1709
1736
|
cnMenuCounts: reactive({}),
|
|
1737
|
+
/**
|
|
1738
|
+
* Reactive `{ [menuItemId]: number }` map of filtered totals, one
|
|
1739
|
+
* per menu entry whose `count` is an object. Provided to
|
|
1740
|
+
* descendants as `cnMenuItemCounts`. Populated by
|
|
1741
|
+
* `_hydrateMenuItemCounts()` at mount.
|
|
1742
|
+
*/
|
|
1743
|
+
cnMenuItemCounts: reactive({}),
|
|
1710
1744
|
/**
|
|
1711
1745
|
* Open state of the host NcAppSettingsDialog. Toggled
|
|
1712
1746
|
* to `true` by the provided `cnOpenUserSettings()`
|
|
@@ -2925,6 +2959,7 @@ export default {
|
|
|
2925
2959
|
this._validateRegistry()
|
|
2926
2960
|
this._warnCustomComponentsDeprecation()
|
|
2927
2961
|
this._hydrateMenuCounts()
|
|
2962
|
+
this._hydrateMenuItemCounts()
|
|
2928
2963
|
return
|
|
2929
2964
|
}
|
|
2930
2965
|
|
|
@@ -2953,6 +2988,7 @@ export default {
|
|
|
2953
2988
|
this._validateRegistry()
|
|
2954
2989
|
this._warnCustomComponentsDeprecation()
|
|
2955
2990
|
this._hydrateMenuCounts()
|
|
2991
|
+
this._hydrateMenuItemCounts()
|
|
2956
2992
|
},
|
|
2957
2993
|
|
|
2958
2994
|
beforeUnmount() {
|
|
@@ -3609,6 +3645,63 @@ export default {
|
|
|
3609
3645
|
})
|
|
3610
3646
|
},
|
|
3611
3647
|
|
|
3648
|
+
/**
|
|
3649
|
+
* Fetch the total of every menu entry (top level and children) whose
|
|
3650
|
+
* `count` is an object `{ register, schema, filter? }`: one
|
|
3651
|
+
* `GET /api/objects/{register}/{schema}?_limit=1` per entry with the
|
|
3652
|
+
* filter's tokens (`@me`, `@today`, ...) resolved at fetch time, the
|
|
3653
|
+
* response `total` written to `cnMenuItemCounts[item.id]`. The
|
|
3654
|
+
* request goes straight through axios rather than the object store,
|
|
3655
|
+
* so the index page's whole-schema total in the store is left alone.
|
|
3656
|
+
* Failures leave the badge unrendered.
|
|
3657
|
+
*
|
|
3658
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-menu-entry-counts-a-filtered-list
|
|
3659
|
+
* @return {void}
|
|
3660
|
+
* @private
|
|
3661
|
+
*/
|
|
3662
|
+
_hydrateMenuItemCounts() {
|
|
3663
|
+
const collect = (items) => {
|
|
3664
|
+
const out = []
|
|
3665
|
+
for (const item of items ?? []) {
|
|
3666
|
+
const count = item?.count
|
|
3667
|
+
if (count && typeof count === 'object' && typeof item.id === 'string' && count.register && count.schema) {
|
|
3668
|
+
out.push(item)
|
|
3669
|
+
}
|
|
3670
|
+
if (Array.isArray(item?.children)) {
|
|
3671
|
+
out.push(...collect(item.children))
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
return out
|
|
3675
|
+
}
|
|
3676
|
+
for (const item of collect(this.manifest?.menu)) {
|
|
3677
|
+
this._fetchMenuItemCount(item)
|
|
3678
|
+
}
|
|
3679
|
+
},
|
|
3680
|
+
|
|
3681
|
+
/**
|
|
3682
|
+
* One filtered-count request for a menu entry; see
|
|
3683
|
+
* `_hydrateMenuItemCounts`.
|
|
3684
|
+
*
|
|
3685
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-menu-entry-counts-a-filtered-list
|
|
3686
|
+
* @param {{ id: string, count: { register: string, schema: string, filter?: object } }} item The menu entry.
|
|
3687
|
+
* @return {Promise<void>}
|
|
3688
|
+
* @private
|
|
3689
|
+
*/
|
|
3690
|
+
async _fetchMenuItemCount(item) {
|
|
3691
|
+
try {
|
|
3692
|
+
const { register, schema, filter } = item.count
|
|
3693
|
+
const resolved = resolveFilterTokens(filter && typeof filter === 'object' ? filter : {}, {})
|
|
3694
|
+
const url = generateUrl(`/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}`)
|
|
3695
|
+
const { data } = await axios.get(url, { params: { _limit: 1, ...resolved } })
|
|
3696
|
+
const total = data?.total
|
|
3697
|
+
if (typeof total === 'number' && total >= 0) {
|
|
3698
|
+
this.cnMenuItemCounts[item.id] = total
|
|
3699
|
+
}
|
|
3700
|
+
} catch {
|
|
3701
|
+
// Non-fatal: the badge stays unrendered.
|
|
3702
|
+
}
|
|
3703
|
+
},
|
|
3704
|
+
|
|
3612
3705
|
/**
|
|
3613
3706
|
* Hydrate all menu counts with a single `POST /api/objects/counts`
|
|
3614
3707
|
* (OpenRegister batched-counts endpoint). Distributes each returned
|
|
@@ -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
|
*
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
<NcBreadcrumbs
|
|
19
19
|
v-if="crumbs.length > 0"
|
|
20
20
|
:aria-label="ariaLabel"
|
|
21
|
+
:rootIcon="rootText ? '' : undefined"
|
|
21
22
|
data-testid="cn-breadcrumbs">
|
|
22
23
|
<!-- `name` falls back to '' because NcBreadcrumb declares it a REQUIRED
|
|
23
24
|
String and warns per render on the label-less icon-only crumb. The
|
|
@@ -97,6 +98,19 @@ export default {
|
|
|
97
98
|
type: String,
|
|
98
99
|
default: () => t('nextcloud-vue', 'Breadcrumbs'),
|
|
99
100
|
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Draw the FIRST crumb's label as text. NcBreadcrumbs gives the root
|
|
104
|
+
* crumb a home icon and prints its `label` only as the accessible
|
|
105
|
+
* name, so "All cases" read as a house. Off by default, which keeps
|
|
106
|
+
* the home icon. A first crumb with its own `icon` keeps that icon.
|
|
107
|
+
*
|
|
108
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-declared-breadcrumb-label-shows-as-text
|
|
109
|
+
*/
|
|
110
|
+
rootText: {
|
|
111
|
+
type: Boolean,
|
|
112
|
+
default: false,
|
|
113
|
+
},
|
|
100
114
|
},
|
|
101
115
|
|
|
102
116
|
methods: {
|
|
@@ -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)"
|
|
@@ -397,9 +404,11 @@
|
|
|
397
404
|
:iconUrl="getWidgetIconUrl(item)"
|
|
398
405
|
:iconClass="getWidgetIconClass(item)"
|
|
399
406
|
:showTitle="widgetShowTitle(item)"
|
|
407
|
+
:showActions="widgetShowActions(item)"
|
|
400
408
|
:borderless="widgetBorderless(item)"
|
|
401
409
|
:flush="item.flush !== false"
|
|
402
410
|
:buttons="getWidgetButtons(item)"
|
|
411
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
403
412
|
:styleConfig="item.styleConfig || {}"
|
|
404
413
|
:titleIconPosition="getWidgetTitleIconPosition(item)"
|
|
405
414
|
:titleIconColor="getWidgetTitleIconColor(item)"
|
|
@@ -478,6 +487,7 @@
|
|
|
478
487
|
:flush="item.flush !== false"
|
|
479
488
|
:class="{ 'cn-dashboard-page__card-fit': isCardWidget(item) }"
|
|
480
489
|
:buttons="getWidgetButtons(item)"
|
|
490
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
481
491
|
:styleConfig="item.styleConfig || {}"
|
|
482
492
|
:documentationUrl="getWidgetDocumentationUrl(item)"
|
|
483
493
|
:docsAnchor="getWidgetDocsAnchor(item)"
|
|
@@ -499,9 +509,11 @@
|
|
|
499
509
|
:iconUrl="getWidgetIconUrl(item)"
|
|
500
510
|
:iconClass="getWidgetIconClass(item)"
|
|
501
511
|
:showTitle="widgetShowTitle(item)"
|
|
512
|
+
:showActions="widgetShowActions(item)"
|
|
502
513
|
:borderless="widgetBorderless(item)"
|
|
503
514
|
:flush="item.flush !== false"
|
|
504
515
|
:buttons="getWidgetButtons(item)"
|
|
516
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
505
517
|
:styleConfig="item.styleConfig || {}"
|
|
506
518
|
:titleIconPosition="getWidgetTitleIconPosition(item)"
|
|
507
519
|
:titleIconColor="getWidgetTitleIconColor(item)"
|
|
@@ -536,7 +548,9 @@
|
|
|
536
548
|
:iconUrl="getWidgetIconUrl(item)"
|
|
537
549
|
:iconClass="getWidgetIconClass(item)"
|
|
538
550
|
:showTitle="widgetShowTitle(item)"
|
|
551
|
+
:showActions="widgetShowActions(item)"
|
|
539
552
|
:buttons="getWidgetButtons(item)"
|
|
553
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
540
554
|
:styleConfig="item.styleConfig || {}"
|
|
541
555
|
:showRefresh="getWidgetShowRefresh(item)"
|
|
542
556
|
:documentationUrl="getWidgetDocumentationUrl(item)"
|
|
@@ -566,6 +580,7 @@
|
|
|
566
580
|
:titleIconVariant="getWidgetTitleIconVariant(item)"
|
|
567
581
|
:class="{ 'cn-dashboard-page__card-fit': isCardWidget(item) }"
|
|
568
582
|
:buttons="getWidgetButtons(item)"
|
|
583
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
569
584
|
:styleConfig="item.styleConfig || {}"
|
|
570
585
|
:documentationUrl="getWidgetDocumentationUrl(item)"
|
|
571
586
|
:docsAnchor="getWidgetDocsAnchor(item)"
|
|
@@ -921,6 +936,34 @@ export default {
|
|
|
921
936
|
default: '',
|
|
922
937
|
},
|
|
923
938
|
|
|
939
|
+
/**
|
|
940
|
+
* Whether the header row renders: title, description, header actions
|
|
941
|
+
* and the edit toggle. `false` (manifest `config.showHeader: false`)
|
|
942
|
+
* drops it for a page whose first widget is its heading; the title
|
|
943
|
+
* then renders visually hidden so the page keeps an accessible name.
|
|
944
|
+
*
|
|
945
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-dashboard-page-can-hide-its-header
|
|
946
|
+
*/
|
|
947
|
+
showHeader: {
|
|
948
|
+
type: Boolean,
|
|
949
|
+
default: true,
|
|
950
|
+
},
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Whether a widget draws its overflow Actions menu when neither its
|
|
954
|
+
* placement nor its definition says (`showActions`). `false` (manifest
|
|
955
|
+
* `config.showWidgetActions: false`) drops the menu from every such
|
|
956
|
+
* widget, for a dashboard whose widgets carry a header link and
|
|
957
|
+
* nothing else. A widget that sets `showActions: true` keeps its menu.
|
|
958
|
+
* Card widgets have no menu either way.
|
|
959
|
+
*
|
|
960
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-dashboard-can-drop-the-widget-actions-menu
|
|
961
|
+
*/
|
|
962
|
+
showWidgetActions: {
|
|
963
|
+
type: Boolean,
|
|
964
|
+
default: true,
|
|
965
|
+
},
|
|
966
|
+
|
|
924
967
|
/**
|
|
925
968
|
* Widget definitions array. Each widget defines metadata for rendering.
|
|
926
969
|
*
|
|
@@ -3355,11 +3398,20 @@ export default {
|
|
|
3355
3398
|
* exactly that reason; there is now one rule for every family, so a
|
|
3356
3399
|
* custom slot and a registered widget cannot disagree about it.
|
|
3357
3400
|
*
|
|
3401
|
+
* A greeting header that declares `content.ground: true` sits on the
|
|
3402
|
+
* page ground, so it asks for no card either: one key on the widget
|
|
3403
|
+
* instead of a second one on its placement.
|
|
3404
|
+
*
|
|
3405
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-greeting-can-sit-on-the-page-ground
|
|
3358
3406
|
* @param {object} item the layout placement.
|
|
3359
3407
|
* @return {boolean}
|
|
3360
3408
|
*/
|
|
3361
3409
|
widgetBorderless(item) {
|
|
3362
|
-
|
|
3410
|
+
if (item.borderless === true) {
|
|
3411
|
+
return true
|
|
3412
|
+
}
|
|
3413
|
+
const def = this.getWidgetDef(item.widgetId)
|
|
3414
|
+
return Boolean(def && def.type === 'header' && this.getWidgetContent(item).ground === true)
|
|
3363
3415
|
},
|
|
3364
3416
|
|
|
3365
3417
|
/**
|
|
@@ -3376,7 +3428,7 @@ export default {
|
|
|
3376
3428
|
const def = this.getWidgetDef(item.widgetId)
|
|
3377
3429
|
const value = item.showActions !== undefined ? item.showActions : def?.showActions
|
|
3378
3430
|
if (value === undefined || value === null) {
|
|
3379
|
-
return !this.isCardWidget(item)
|
|
3431
|
+
return this.showWidgetActions && !this.isCardWidget(item)
|
|
3380
3432
|
}
|
|
3381
3433
|
return value !== false
|
|
3382
3434
|
},
|
|
@@ -3410,6 +3462,22 @@ export default {
|
|
|
3410
3462
|
return def?.buttons || []
|
|
3411
3463
|
},
|
|
3412
3464
|
|
|
3465
|
+
/**
|
|
3466
|
+
* The widget's header text link (`headerLink` on the layout entry,
|
|
3467
|
+
* else on the widget definition), forwarded to CnWidgetWrapper.
|
|
3468
|
+
*
|
|
3469
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link
|
|
3470
|
+
* @param {object} item Layout placement entry.
|
|
3471
|
+
* @return {object|null}
|
|
3472
|
+
*/
|
|
3473
|
+
getWidgetHeaderLink(item) {
|
|
3474
|
+
if (item?.headerLink && typeof item.headerLink === 'object') {
|
|
3475
|
+
return item.headerLink
|
|
3476
|
+
}
|
|
3477
|
+
const def = this.getWidgetDef(item.widgetId)
|
|
3478
|
+
return (def?.headerLink && typeof def.headerLink === 'object') ? def.headerLink : null
|
|
3479
|
+
},
|
|
3480
|
+
|
|
3413
3481
|
/**
|
|
3414
3482
|
* Whether a widget's overflow Actions menu shows the Refresh item.
|
|
3415
3483
|
* 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,11 @@ 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?, icon? }`) draws a breadcrumb line above the header: that crumb, then the record's name as the current crumb. The label shows as text; `icon` draws an icon instead.
|
|
437
|
+
|
|
438
|
+
### Header card and header widget
|
|
439
|
+
|
|
440
|
+
`headerCard` (default `false`) draws the header as a bordered card. `headerWidget` (default `''`) names a widget in `widgets` to render inside the header under the title, without a card of its own and out of the body grid: the stages bars of the board's case card.
|