@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
|
@@ -41,8 +41,20 @@
|
|
|
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
|
+
:rootText="breadcrumbCrumbs[0].icon === undefined"
|
|
51
|
+
data-testid="cn-detail-page-breadcrumbs" />
|
|
44
52
|
<!-- Header -->
|
|
45
|
-
<div
|
|
53
|
+
<div
|
|
54
|
+
v-if="!objectNotFound"
|
|
55
|
+
class="cn-detail-page__header"
|
|
56
|
+
:class="{ 'cn-detail-page__header--card': headerCard, 'cn-detail-page__header--with-widget': headerWidgetDef !== null }"
|
|
57
|
+
data-testid="cn-detail-page-header">
|
|
46
58
|
<!-- Header (left block) — overridable via #header slot. Default
|
|
47
59
|
renders the icon + title + description. The right-hand
|
|
48
60
|
#actions slot remains separate so headerComponent and
|
|
@@ -98,7 +110,7 @@
|
|
|
98
110
|
(ADR-062). Only shown once the object resolves to a
|
|
99
111
|
display name that differs from the type label. -->
|
|
100
112
|
<p
|
|
101
|
-
v-if="typeEyebrow"
|
|
113
|
+
v-if="typeEyebrow && showTypeEyebrow"
|
|
102
114
|
class="cn-detail-page__type-eyebrow"
|
|
103
115
|
data-testid="cn-detail-page-type-eyebrow">
|
|
104
116
|
{{ typeEyebrow }}
|
|
@@ -363,6 +375,50 @@
|
|
|
363
375
|
</template>
|
|
364
376
|
</CnActionsMenu>
|
|
365
377
|
</div>
|
|
378
|
+
<!-- A widget inside the header (manifest `config.headerWidget`), on
|
|
379
|
+
a row of its own under the title and the actions: the stages
|
|
380
|
+
bars of the board's case card. Drawn without a card of its own,
|
|
381
|
+
and taken out of the body grid. Nothing renders without the
|
|
382
|
+
key. -->
|
|
383
|
+
<div
|
|
384
|
+
v-if="headerWidgetDef !== null"
|
|
385
|
+
class="cn-detail-page__header-widget"
|
|
386
|
+
data-testid="cn-detail-page-header-widget">
|
|
387
|
+
<!--
|
|
388
|
+
@slot `widget-${headerWidgetDef.id}`
|
|
389
|
+
@description The same per-widget slot the body grid and the side
|
|
390
|
+
column offer, for the widget placed in the header
|
|
391
|
+
(`headerWidget`). Same name and same bindings.
|
|
392
|
+
-->
|
|
393
|
+
<slot
|
|
394
|
+
:name="`widget-${headerWidgetDef.id}`"
|
|
395
|
+
:item="{ id: headerWidgetDef.id, widgetId: headerWidgetDef.id }"
|
|
396
|
+
:widget="headerWidgetDef"
|
|
397
|
+
:objectId="objectId"
|
|
398
|
+
:object="resolvedObject"
|
|
399
|
+
:objectData="resolvedObject"
|
|
400
|
+
:objectType="resolvedObjectType"
|
|
401
|
+
:register="register"
|
|
402
|
+
:schema="schema">
|
|
403
|
+
<CnDetailWidgetHost
|
|
404
|
+
:widget="headerWidgetDef"
|
|
405
|
+
chrome="bare"
|
|
406
|
+
:objectId="objectId"
|
|
407
|
+
:object="currentObject"
|
|
408
|
+
:objectType="resolvedObjectType"
|
|
409
|
+
:schemaObject="currentSchema"
|
|
410
|
+
:register="register"
|
|
411
|
+
:schema="schema"
|
|
412
|
+
:store="effectiveObjectStore"
|
|
413
|
+
:surface="surface"
|
|
414
|
+
:integrationContext="effectiveIntegrationContext"
|
|
415
|
+
:hideEmpty="hideEmpty"
|
|
416
|
+
:cnRegistry="cnRegistry"
|
|
417
|
+
:availableWidgets="widgets"
|
|
418
|
+
@geoSaved="onGeoSaved"
|
|
419
|
+
@openIntegration="onAutoBodyOpenIntegration" />
|
|
420
|
+
</slot>
|
|
421
|
+
</div>
|
|
366
422
|
</div>
|
|
367
423
|
|
|
368
424
|
<!-- The locked card, under the title, for ANY active lock — not only a
|
|
@@ -1025,6 +1081,7 @@ import {
|
|
|
1025
1081
|
widgetTitleOf,
|
|
1026
1082
|
} from '../../utils/widgetDispatch.js'
|
|
1027
1083
|
import { CnActionButtons } from '../CnActionButtons/index.js'
|
|
1084
|
+
import { CnBreadcrumbs } from '../CnBreadcrumbs/index.js'
|
|
1028
1085
|
import { CnIcon } from '../CnIcon/index.js'
|
|
1029
1086
|
import { CnStatusBadge } from '../CnStatusBadge/index.js'
|
|
1030
1087
|
import { getWidgetTypeEntry } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
|
|
@@ -1172,6 +1229,7 @@ export default {
|
|
|
1172
1229
|
CnDashboardGrid,
|
|
1173
1230
|
CnLifecycleActions,
|
|
1174
1231
|
CnActionButtons,
|
|
1232
|
+
CnBreadcrumbs,
|
|
1175
1233
|
CnStatusBadge,
|
|
1176
1234
|
CnSummaryAggregates,
|
|
1177
1235
|
CnRelatedCollections,
|
|
@@ -2078,6 +2136,37 @@ export default {
|
|
|
2078
2136
|
default: null,
|
|
2079
2137
|
},
|
|
2080
2138
|
|
|
2139
|
+
/**
|
|
2140
|
+
* Whether the type eyebrow (the type label above the record name)
|
|
2141
|
+
* renders once the record resolves. `false` (manifest
|
|
2142
|
+
* `config.showTypeEyebrow: false`) drops it for a header that says
|
|
2143
|
+
* the type in a pill instead.
|
|
2144
|
+
*
|
|
2145
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb
|
|
2146
|
+
*/
|
|
2147
|
+
showTypeEyebrow: {
|
|
2148
|
+
type: Boolean,
|
|
2149
|
+
default: true,
|
|
2150
|
+
},
|
|
2151
|
+
|
|
2152
|
+
/**
|
|
2153
|
+
* A breadcrumb line above the header (manifest `config.breadcrumb`):
|
|
2154
|
+
* `{ label, route?, params?, href? }` names the list the record
|
|
2155
|
+
* belongs to; the record's display name follows as the current
|
|
2156
|
+
* crumb. `label` goes through the host translate function and shows
|
|
2157
|
+
* as text; `icon` (an MDI name, e.g. `Home`) draws that icon instead,
|
|
2158
|
+
* with the label as its accessible name. Null (the default) draws no
|
|
2159
|
+
* trail.
|
|
2160
|
+
*
|
|
2161
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb
|
|
2162
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-declared-breadcrumb-label-shows-as-text
|
|
2163
|
+
* @type {{label: string, route?: string, params?: object, href?: string, icon?: string}|null}
|
|
2164
|
+
*/
|
|
2165
|
+
breadcrumb: {
|
|
2166
|
+
type: Object,
|
|
2167
|
+
default: null,
|
|
2168
|
+
},
|
|
2169
|
+
|
|
2081
2170
|
/**
|
|
2082
2171
|
* A column of cards beside the body (manifest `config.sideColumn`),
|
|
2083
2172
|
* for the record's facts: deadline, requester, handler, links. Each
|
|
@@ -2093,6 +2182,33 @@ export default {
|
|
|
2093
2182
|
default: () => [],
|
|
2094
2183
|
},
|
|
2095
2184
|
|
|
2185
|
+
/**
|
|
2186
|
+
* Draw the header as a card (manifest `config.headerCard: true`): the
|
|
2187
|
+
* pills, the title and the actions on the page's surface colour inside
|
|
2188
|
+
* a bordered, rounded box, as the board's case header. Off by default,
|
|
2189
|
+
* which keeps the header on the page ground.
|
|
2190
|
+
*
|
|
2191
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget
|
|
2192
|
+
*/
|
|
2193
|
+
headerCard: {
|
|
2194
|
+
type: Boolean,
|
|
2195
|
+
default: false,
|
|
2196
|
+
},
|
|
2197
|
+
|
|
2198
|
+
/**
|
|
2199
|
+
* The id of a widget declared in `widgets` to render inside the header,
|
|
2200
|
+
* on its own row under the title and the actions (manifest
|
|
2201
|
+
* `config.headerWidget`), e.g. the stages bars. It renders without a
|
|
2202
|
+
* card of its own and leaves the body grid. Empty (the default), or an
|
|
2203
|
+
* id that names no widget, renders nothing extra.
|
|
2204
|
+
*
|
|
2205
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget
|
|
2206
|
+
*/
|
|
2207
|
+
headerWidget: {
|
|
2208
|
+
type: String,
|
|
2209
|
+
default: '',
|
|
2210
|
+
},
|
|
2211
|
+
|
|
2096
2212
|
/**
|
|
2097
2213
|
* Whether the current user administers this instance. Shows the
|
|
2098
2214
|
* `adminOnly` header actions. `null` (the default) reads it from
|
|
@@ -2632,6 +2748,21 @@ export default {
|
|
|
2632
2748
|
.filter(Boolean)
|
|
2633
2749
|
},
|
|
2634
2750
|
|
|
2751
|
+
/**
|
|
2752
|
+
* The widget the header holds (`headerWidget`): the definition in
|
|
2753
|
+
* `widgets` with that id, or null without the key or a match.
|
|
2754
|
+
*
|
|
2755
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget
|
|
2756
|
+
* @return {object|null} The widget definition, or null.
|
|
2757
|
+
*/
|
|
2758
|
+
headerWidgetDef() {
|
|
2759
|
+
if (!this.headerWidget) {
|
|
2760
|
+
return null
|
|
2761
|
+
}
|
|
2762
|
+
const declared = Array.isArray(this.widgets) ? this.widgets : []
|
|
2763
|
+
return declared.find((widget) => widget && widget.id === this.headerWidget) || null
|
|
2764
|
+
},
|
|
2765
|
+
|
|
2635
2766
|
/**
|
|
2636
2767
|
* The side column's widget definitions. A string names a widget
|
|
2637
2768
|
* declared in `widgets`; an object is a definition of its own.
|
|
@@ -3201,6 +3332,33 @@ export default {
|
|
|
3201
3332
|
return this.objectDisplayName || this.resolvedTitle
|
|
3202
3333
|
},
|
|
3203
3334
|
|
|
3335
|
+
/**
|
|
3336
|
+
* The breadcrumb trail: the declared crumb (a router target from
|
|
3337
|
+
* `route` + `params`, or an `href`), then the record's display name
|
|
3338
|
+
* as the current crumb. Empty without a `breadcrumb` label. The first
|
|
3339
|
+
* crumb carries `icon` only when the manifest declares one.
|
|
3340
|
+
*
|
|
3341
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb
|
|
3342
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-declared-breadcrumb-label-shows-as-text
|
|
3343
|
+
* @return {Array<{label: string, to?: object, href?: string, icon?: string}>}
|
|
3344
|
+
*/
|
|
3345
|
+
breadcrumbCrumbs() {
|
|
3346
|
+
const crumb = this.breadcrumb
|
|
3347
|
+
if (!crumb || typeof crumb.label !== 'string' || crumb.label === '') {
|
|
3348
|
+
return []
|
|
3349
|
+
}
|
|
3350
|
+
const first = { label: this.effectiveTranslate(crumb.label) }
|
|
3351
|
+
if (typeof crumb.icon === 'string' && crumb.icon !== '') {
|
|
3352
|
+
first.icon = crumb.icon
|
|
3353
|
+
}
|
|
3354
|
+
if (crumb.route) {
|
|
3355
|
+
first.to = { name: crumb.route, ...(crumb.params ? { params: crumb.params } : {}) }
|
|
3356
|
+
} else if (crumb.href) {
|
|
3357
|
+
first.href = crumb.href
|
|
3358
|
+
}
|
|
3359
|
+
return [first, { label: this.displayTitle }]
|
|
3360
|
+
},
|
|
3361
|
+
|
|
3204
3362
|
/**
|
|
3205
3363
|
* Effective translate function: the injected `cnTranslate` (the host
|
|
3206
3364
|
* app's bound `t()`), identity by default.
|
|
@@ -4652,6 +4810,10 @@ export default {
|
|
|
4652
4810
|
* it — CnPageRenderer's split pane forwards no slots, which is exactly
|
|
4653
4811
|
* that fault and wants fixing, not concealing.
|
|
4654
4812
|
*
|
|
4813
|
+
* The widget the header holds (`headerWidget`) draws nothing here
|
|
4814
|
+
* either: it is already drawn in the header.
|
|
4815
|
+
*
|
|
4816
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget
|
|
4655
4817
|
* @param {object} item A layout item.
|
|
4656
4818
|
* @return {boolean} False only when the item provably draws nothing.
|
|
4657
4819
|
*/
|
|
@@ -4662,6 +4824,11 @@ export default {
|
|
|
4662
4824
|
if (this.editingBody) {
|
|
4663
4825
|
return true
|
|
4664
4826
|
}
|
|
4827
|
+
// The widget the header holds (`headerWidget`) is drawn there, so
|
|
4828
|
+
// its grid cell would be the same widget twice.
|
|
4829
|
+
if (this.headerWidgetDef !== null && item.widgetId === this.headerWidgetDef.id) {
|
|
4830
|
+
return false
|
|
4831
|
+
}
|
|
4665
4832
|
const widget = this.findWidget(item)
|
|
4666
4833
|
if (!widget) {
|
|
4667
4834
|
return true
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
-->
|
|
5
5
|
|
|
6
6
|
<template>
|
|
7
|
-
<div class="cn-header-widget" :class="{ 'cn-header-widget--plain': isPlain }" :style="wrapperStyle">
|
|
7
|
+
<div class="cn-header-widget" :class="{ 'cn-header-widget--plain': isPlain, 'cn-header-widget--ground': isGround, 'cn-header-widget--with-views': viewOptions.length > 0 }" :style="wrapperStyle">
|
|
8
8
|
<div
|
|
9
9
|
v-if="hasOverlay"
|
|
10
10
|
class="cn-header-widget__overlay"
|
|
@@ -34,6 +34,16 @@
|
|
|
34
34
|
{{ ctaLabel }}
|
|
35
35
|
</a>
|
|
36
36
|
</div>
|
|
37
|
+
<!-- A view switch at the right of the heading (`content.views`): "My
|
|
38
|
+
work / My team" as a segmented control whose options are routes. -->
|
|
39
|
+
<div v-if="viewOptions.length > 0" class="cn-header-widget__views">
|
|
40
|
+
<CnSegmentedControl
|
|
41
|
+
:options="viewOptions"
|
|
42
|
+
:modelValue="activeView"
|
|
43
|
+
:ariaLabel="viewsLabel"
|
|
44
|
+
data-testid="cn-header-widget-views"
|
|
45
|
+
@update:modelValue="onViewChange" />
|
|
46
|
+
</div>
|
|
37
47
|
<img
|
|
38
48
|
v-if="hasBackgroundImage"
|
|
39
49
|
class="cn-header-widget__probe"
|
|
@@ -47,6 +57,7 @@
|
|
|
47
57
|
<script>
|
|
48
58
|
import { getCurrentUser } from '@nextcloud/auth'
|
|
49
59
|
import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
|
|
60
|
+
import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
|
|
50
61
|
import { resolveImageUrl } from '../../utils/resolveImageUrl.js'
|
|
51
62
|
|
|
52
63
|
const ALLOWED_OVERLAY_MODES = ['none', 'tint', 'gradient-bottom']
|
|
@@ -99,6 +110,8 @@ const VERTICAL_ALIGN_FLEX = Object.freeze({
|
|
|
99
110
|
export default {
|
|
100
111
|
name: 'CnHeaderWidget',
|
|
101
112
|
|
|
113
|
+
components: { CnSegmentedControl },
|
|
114
|
+
|
|
102
115
|
inject: {
|
|
103
116
|
/**
|
|
104
117
|
* Host translate function provided by CnAppRoot as
|
|
@@ -115,9 +128,15 @@ export default {
|
|
|
115
128
|
* Persisted widget content: `{title, subtitle, backgroundImageUrl,
|
|
116
129
|
* backgroundImageFileId, backgroundColor, overlayMode, overlayColor,
|
|
117
130
|
* overlayOpacity, textColor, textAlign, verticalAlign, height, cta,
|
|
118
|
-
* greeting, showDate, plain}`. All fields are optional except
|
|
119
|
-
* (or `greeting`); unknown enum values collapse to documented
|
|
120
|
-
* and the renderer never throws.
|
|
131
|
+
* greeting, showDate, plain, ground, views}`. All fields are optional except
|
|
132
|
+
* `title` (or `greeting`); unknown enum values collapse to documented
|
|
133
|
+
* defaults and the renderer never throws. `views` is
|
|
134
|
+
* `{ ariaLabel?, options: [{ label, route, params? }] }`: a segmented
|
|
135
|
+
* control at the right of the heading whose checked option is the
|
|
136
|
+
* current route; choosing another pushes its route. `ground: true`
|
|
137
|
+
* draws the greeting on the page ground: the plain look with no
|
|
138
|
+
* padding, a 32px heading and the date line 6px above it (a dashboard
|
|
139
|
+
* also drops the widget's card for it).
|
|
121
140
|
*
|
|
122
141
|
* @type {object}
|
|
123
142
|
*/
|
|
@@ -198,13 +217,75 @@ export default {
|
|
|
198
217
|
return this.now instanceof Date ? this.now : new Date()
|
|
199
218
|
},
|
|
200
219
|
|
|
220
|
+
/**
|
|
221
|
+
* The view switch's options from `content.views.options`: each
|
|
222
|
+
* `{ label, route, params? }` with a non-empty label and route becomes
|
|
223
|
+
* a segmented-control option keyed on its route name (the label goes
|
|
224
|
+
* through the host translate function). Empty without a router, so
|
|
225
|
+
* the switch cannot offer routes it cannot follow.
|
|
226
|
+
*
|
|
227
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
|
|
228
|
+
* @return {Array<{value: string, label: string}>}
|
|
229
|
+
*/
|
|
230
|
+
viewOptions() {
|
|
231
|
+
const raw = this.content && this.content.views && this.content.views.options
|
|
232
|
+
if (!Array.isArray(raw) || !this.$router) {
|
|
233
|
+
return []
|
|
234
|
+
}
|
|
235
|
+
return raw
|
|
236
|
+
.filter((o) => o && typeof o.label === 'string' && o.label !== '' && typeof o.route === 'string' && o.route !== '')
|
|
237
|
+
.map((o) => ({ value: o.route, label: this.cnTranslate(o.label) }))
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* The checked view: the option whose route is the current route, else
|
|
242
|
+
* the first option (a switch always has one checked option).
|
|
243
|
+
*
|
|
244
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
|
|
245
|
+
* @return {string|null}
|
|
246
|
+
*/
|
|
247
|
+
activeView() {
|
|
248
|
+
const options = this.viewOptions
|
|
249
|
+
if (options.length === 0) {
|
|
250
|
+
return null
|
|
251
|
+
}
|
|
252
|
+
const current = this.$route && this.$route.name
|
|
253
|
+
const match = options.find((o) => o.value === current)
|
|
254
|
+
return match ? match.value : options[0].value
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Accessible name of the view switch: `content.views.ariaLabel`
|
|
259
|
+
* through the host translate function, else "View".
|
|
260
|
+
*
|
|
261
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
|
|
262
|
+
* @return {string}
|
|
263
|
+
*/
|
|
264
|
+
viewsLabel() {
|
|
265
|
+
const label = this.content && this.content.views && this.content.views.ariaLabel
|
|
266
|
+
return label ? this.cnTranslate(label) : t('nextcloud-vue', 'View')
|
|
267
|
+
},
|
|
268
|
+
|
|
201
269
|
/**
|
|
202
270
|
* Whether the plain presentation is on (no coloured background).
|
|
203
271
|
*
|
|
204
272
|
* @return {boolean}
|
|
205
273
|
*/
|
|
206
274
|
isPlain() {
|
|
207
|
-
return Boolean(this.content && this.content.plain === true)
|
|
275
|
+
return Boolean(this.content && (this.content.plain === true || this.content.ground === true))
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Whether the greeting sits on the page ground (`content.ground`):
|
|
280
|
+
* the plain look without the card padding, so the date line and the
|
|
281
|
+
* heading align with the page edge as the board draws them. Off by
|
|
282
|
+
* default.
|
|
283
|
+
*
|
|
284
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-greeting-can-sit-on-the-page-ground
|
|
285
|
+
* @return {boolean}
|
|
286
|
+
*/
|
|
287
|
+
isGround() {
|
|
288
|
+
return Boolean(this.content && this.content.ground === true)
|
|
208
289
|
},
|
|
209
290
|
|
|
210
291
|
/**
|
|
@@ -490,7 +571,9 @@ export default {
|
|
|
490
571
|
return {
|
|
491
572
|
position: 'relative',
|
|
492
573
|
'z-index': 1,
|
|
493
|
-
|
|
574
|
+
// Beside a view switch the content shares the row instead of
|
|
575
|
+
// taking the whole width (see .cn-header-widget--with-views).
|
|
576
|
+
width: this.viewOptions.length > 0 ? 'auto' : '100%',
|
|
494
577
|
height: '100%',
|
|
495
578
|
display: 'flex',
|
|
496
579
|
'flex-direction': 'column',
|
|
@@ -498,9 +581,10 @@ export default {
|
|
|
498
581
|
'justify-content': VERTICAL_ALIGN_FLEX[this.verticalAlign],
|
|
499
582
|
// Plain drops the coloured background, not the card padding:
|
|
500
583
|
// at 0 the greeting sat flush against the card's left edge.
|
|
501
|
-
|
|
584
|
+
// Ground has no card, so it has no padding either.
|
|
585
|
+
padding: this.isGround ? '0' : (this.isPlain ? '16px' : '16px 24px'),
|
|
502
586
|
'box-sizing': 'border-box',
|
|
503
|
-
gap: '8px',
|
|
587
|
+
gap: this.isGround ? '6px' : '8px',
|
|
504
588
|
'text-align': this.textAlign,
|
|
505
589
|
}
|
|
506
590
|
},
|
|
@@ -604,6 +688,26 @@ export default {
|
|
|
604
688
|
},
|
|
605
689
|
|
|
606
690
|
methods: {
|
|
691
|
+
/**
|
|
692
|
+
* Follow the chosen view: push its route (with the option's `params`
|
|
693
|
+
* when declared). Nothing happens for the route already shown.
|
|
694
|
+
*
|
|
695
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-greeting-header-switches-views
|
|
696
|
+
* @param {string} route The chosen option's route name.
|
|
697
|
+
* @return {void}
|
|
698
|
+
*/
|
|
699
|
+
onViewChange(route) {
|
|
700
|
+
if (!this.$router || !route || route === (this.$route && this.$route.name)) {
|
|
701
|
+
return
|
|
702
|
+
}
|
|
703
|
+
const declared = (this.content.views.options || []).find((o) => o && o.route === route) || {}
|
|
704
|
+
const target = { name: route }
|
|
705
|
+
if (declared.params && typeof declared.params === 'object') {
|
|
706
|
+
target.params = declared.params
|
|
707
|
+
}
|
|
708
|
+
this.$router.push(target)
|
|
709
|
+
},
|
|
710
|
+
|
|
607
711
|
/**
|
|
608
712
|
* Mark the background image as broken so the renderer falls back to
|
|
609
713
|
* the solid colour.
|
|
@@ -655,6 +759,40 @@ export default {
|
|
|
655
759
|
border-radius: 0;
|
|
656
760
|
}
|
|
657
761
|
|
|
762
|
+
/* With a view switch the heading and the switch share one row, the switch
|
|
763
|
+
at the right and bottom-aligned with the heading; on a narrow card the
|
|
764
|
+
switch wraps under it. */
|
|
765
|
+
.cn-header-widget--with-views {
|
|
766
|
+
display: flex;
|
|
767
|
+
flex-wrap: wrap;
|
|
768
|
+
align-items: flex-end;
|
|
769
|
+
justify-content: space-between;
|
|
770
|
+
gap: calc(2 * var(--default-grid-baseline)) calc(4 * var(--default-grid-baseline));
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
.cn-header-widget--with-views .cn-header-widget__content {
|
|
774
|
+
flex: 1 1 320px;
|
|
775
|
+
width: auto;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
.cn-header-widget__views {
|
|
779
|
+
position: relative;
|
|
780
|
+
z-index: 1;
|
|
781
|
+
flex: 0 0 auto;
|
|
782
|
+
padding: 16px;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/* On the page ground (`content.ground`): no card, so no inset around the
|
|
786
|
+
view switch, and the heading at the board's 32px. */
|
|
787
|
+
.cn-header-widget--ground .cn-header-widget__views {
|
|
788
|
+
padding: 0;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
.cn-header-widget--ground .cn-header-widget__title {
|
|
792
|
+
font-size: var(--cn-header-ground-title-size, 32px);
|
|
793
|
+
letter-spacing: -0.01em;
|
|
794
|
+
}
|
|
795
|
+
|
|
658
796
|
.cn-header-widget__date {
|
|
659
797
|
font-size: 14px;
|
|
660
798
|
line-height: 1.4;
|
|
@@ -726,7 +864,7 @@ export default {
|
|
|
726
864
|
height: 1px;
|
|
727
865
|
opacity: 0;
|
|
728
866
|
pointer-events: none;
|
|
729
|
-
|
|
867
|
+
inset-inline-start: -9999px;
|
|
730
868
|
top: -9999px;
|
|
731
869
|
}
|
|
732
870
|
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
<slot
|
|
14
14
|
name="header"
|
|
15
15
|
:title="title"
|
|
16
|
-
:description="
|
|
16
|
+
:description="headerDescription"
|
|
17
17
|
:icon="resolvedIcon"
|
|
18
18
|
:showTitle="showTitle">
|
|
19
19
|
<CnPageHeader
|
|
20
20
|
:title="title"
|
|
21
|
-
:description="
|
|
21
|
+
:description="headerDescription"
|
|
22
22
|
:icon="resolvedIcon"
|
|
23
23
|
:visuallyHidden="!showTitle" />
|
|
24
24
|
</slot>
|
|
@@ -1177,6 +1177,21 @@ export default {
|
|
|
1177
1177
|
default: '',
|
|
1178
1178
|
},
|
|
1179
1179
|
|
|
1180
|
+
/**
|
|
1181
|
+
* A description that carries the collection's total: `{total}` in
|
|
1182
|
+
* the text is replaced by the current pagination total ("{total}
|
|
1183
|
+
* open cases" reads "48 open cases"). Rendered in place of
|
|
1184
|
+
* `description` once a total is known; before that, `description`
|
|
1185
|
+
* shows. Manifest key `config.countSubtitle`. Goes through the host
|
|
1186
|
+
* translate function.
|
|
1187
|
+
*
|
|
1188
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-an-index-page-title-is-a-manifest-key
|
|
1189
|
+
*/
|
|
1190
|
+
countSubtitle: {
|
|
1191
|
+
type: String,
|
|
1192
|
+
default: '',
|
|
1193
|
+
},
|
|
1194
|
+
|
|
1180
1195
|
/**
|
|
1181
1196
|
* Whether to show the page header (icon, title, description) inline.
|
|
1182
1197
|
* When false (default), only the sidebar header shows the title
|
|
@@ -4231,6 +4246,21 @@ export default {
|
|
|
4231
4246
|
return resolveDeepTokens(seed, {})
|
|
4232
4247
|
},
|
|
4233
4248
|
|
|
4249
|
+
/**
|
|
4250
|
+
* The header's description line: `countSubtitle` with the total
|
|
4251
|
+
* filled in when one is known, else `description`.
|
|
4252
|
+
*
|
|
4253
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-an-index-page-title-is-a-manifest-key
|
|
4254
|
+
* @return {string}
|
|
4255
|
+
*/
|
|
4256
|
+
headerDescription() {
|
|
4257
|
+
const total = this.effectivePagination?.total
|
|
4258
|
+
if (this.countSubtitle && typeof total === 'number' && total >= 0) {
|
|
4259
|
+
return this.cnTranslate(this.countSubtitle).replace('{total}', String(total))
|
|
4260
|
+
}
|
|
4261
|
+
return this.description
|
|
4262
|
+
},
|
|
4263
|
+
|
|
4234
4264
|
/** Add button label — derived from schema.title if not explicitly set */
|
|
4235
4265
|
resolvedAddLabel() {
|
|
4236
4266
|
if (this.addLabel) {
|
|
@@ -151,6 +151,7 @@
|
|
|
151
151
|
:sortKey="localSort.field || null"
|
|
152
152
|
:sortOrder="localSort.dir || 'asc'"
|
|
153
153
|
borderless
|
|
154
|
+
:hideHeader="content.hideHeader === true"
|
|
154
155
|
@rowClick="onRowClick"
|
|
155
156
|
@rowAuxClick="onRowAuxClick"
|
|
156
157
|
@select="onSelect"
|
|
@@ -172,6 +173,7 @@
|
|
|
172
173
|
:sortKey="localSort.field || null"
|
|
173
174
|
:sortOrder="localSort.dir || 'asc'"
|
|
174
175
|
borderless
|
|
176
|
+
:hideHeader="content.hideHeader === true"
|
|
175
177
|
@rowClick="onRowClick"
|
|
176
178
|
@rowAuxClick="onRowAuxClick"
|
|
177
179
|
@select="onSelect"
|
|
@@ -419,7 +421,11 @@ export default {
|
|
|
419
421
|
* those carrying a selected value. Client-side over the fetched page,
|
|
420
422
|
* like the rest of this widget's row set.
|
|
421
423
|
*
|
|
422
|
-
*
|
|
424
|
+
* `hideHeader: true` drops the table's column header row (CnDataTable's
|
|
425
|
+
* `hideHeader`), for a short list that reads as rows, not as a table.
|
|
426
|
+
*
|
|
427
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-small-opt-ins-from-the-decidiq-and-learniq-lanes
|
|
428
|
+
* @type {{register?: string, schema?: string, filter?: object, sort?: {field?: string, dir?: string}, limit?: number, extend?: Array<string>, columns?: Array, rowActions?: Array<object>, dropZone?: object, upload?: boolean, groupBy?: string, groupLabel?: string, groupLabelResolve?: {register: string, schema: string, labelField?: string}, selectable?: boolean, bulkActions?: Array<object>, sortable?: boolean, facet?: {field: string, label?: string}, rowRoute?: string, prompt?: string, emptyText?: string, viewAllRoute?: string, viewAllQuery?: object, hideHeader?: boolean}}
|
|
423
429
|
*/
|
|
424
430
|
content: {
|
|
425
431
|
type: Object,
|