@conduction/nextcloud-vue 2.56.0-beta.11 → 2.56.0-beta.13
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 +154 -4
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +555 -480
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +9 -3
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.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 +187 -19
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +215 -135
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js +3 -1
- package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +144 -4
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +157 -7
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +23 -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/CnSegmentedControl/CnSegmentedControl.vue2.js +11 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +2 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.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 +5 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +5 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/mixins/pageViews.js +400 -0
- package/dist/esm/mixins/pageViews.js.map +1 -0
- package/dist/esm/nextcloud-vue.css +513 -81
- package/dist/esm/store/useObjectStore.js +118 -33
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/actionsDispatcher.js +25 -2
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/columnFilters.js +27 -3
- package/dist/esm/utils/columnFilters.js.map +1 -1
- package/dist/esm/utils/schema.js +40 -2
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/schemaRefSlug.js +19 -3
- package/dist/esm/utils/schemaRefSlug.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +8858 -6566
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +513 -81
- package/l10n/en.json +5 -0
- package/l10n/nl.json +5 -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 +237 -12
- package/src/components/CnDataTable/CnColumnFilterPopover.vue +11 -5
- package/src/components/CnDataTable/CnDataTable.vue +45 -1
- package/src/components/CnDetailPage/CnDetailPage.md +8 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +263 -7
- package/src/components/CnFkResolveCell/CnFkResolveCell.vue +3 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +144 -4
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +206 -9
- package/src/components/CnIndexPage/CnIndexPage.vue +32 -2
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +7 -1
- package/src/components/CnSegmentedControl/CnSegmentedControl.md +1 -1
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +12 -0
- 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 +37 -4
- package/src/css/kpi-card.css +71 -1
- package/src/css/table.css +18 -6
- package/src/mixins/pageViews.js +396 -0
- package/src/schemas/app-manifest-v2.schema.json +162 -3
- package/src/store/useObjectStore.js +118 -33
- package/src/utils/actionsDispatcher.js +25 -2
- package/src/utils/columnFilters.js +29 -2
- package/src/utils/schema.js +40 -2
- package/src/utils/schemaRefSlug.js +19 -3
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -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">
|
|
@@ -26,6 +32,18 @@
|
|
|
26
32
|
</p>
|
|
27
33
|
</div>
|
|
28
34
|
<div class="cn-dashboard-page__header-actions">
|
|
35
|
+
<!-- The page's view switch (`views`), unless a greeting header
|
|
36
|
+
widget on the page draws it. Its options control the view
|
|
37
|
+
region below the grid. -->
|
|
38
|
+
<CnSegmentedControl
|
|
39
|
+
v-if="showsPageViewSwitch"
|
|
40
|
+
class="cn-page-view-switch"
|
|
41
|
+
:options="viewSwitchOptions"
|
|
42
|
+
:modelValue="activeViewId"
|
|
43
|
+
:ariaLabel="viewSwitchLabel"
|
|
44
|
+
:controls="viewRegionId"
|
|
45
|
+
data-testid="cn-dashboard-page-view-switch"
|
|
46
|
+
@update:modelValue="selectView" />
|
|
29
47
|
<!-- Declarative header actions (#91 Wave 3): a manifest
|
|
30
48
|
`headerActions[]` renders as buttons (open-form / api-call /
|
|
31
49
|
toggle / navigate / refresh) with visibleWhen gating,
|
|
@@ -212,7 +230,7 @@
|
|
|
212
230
|
sections (before-grid / after-grid / end): a page can legitimately
|
|
213
231
|
have no grid widgets yet still show bodyWidget content, so
|
|
214
232
|
"No widgets configured" would be a false negative. -->
|
|
215
|
-
<div v-else-if="!hasWidgets && !hasBodyWidgets" class="cn-dashboard-page__empty">
|
|
233
|
+
<div v-else-if="!hasWidgets && !hasBodyWidgets && !hasViews" class="cn-dashboard-page__empty">
|
|
216
234
|
<!-- @slot empty Replaces the default empty state shown when the dashboard has no widgets. Defaults to an `NcEmptyContent` block. -->
|
|
217
235
|
<slot name="empty">
|
|
218
236
|
<NcEmptyContent :description="emptyLabel">
|
|
@@ -247,15 +265,34 @@
|
|
|
247
265
|
to the layout/widgets arrays — in-place pushes (Add widget) keep
|
|
248
266
|
the same array identity and can't re-render it. One remount per
|
|
249
267
|
edit flip re-subscribes against the now-reactive graph. -->
|
|
268
|
+
<!-- The view switch for a page without a header row, when no greeting
|
|
269
|
+
header widget draws it. -->
|
|
270
|
+
<div
|
|
271
|
+
v-if="gridSectionsShown && !showHeader && showsPageViewSwitch"
|
|
272
|
+
class="cn-dashboard-page__view-switch">
|
|
273
|
+
<CnSegmentedControl
|
|
274
|
+
class="cn-page-view-switch"
|
|
275
|
+
:options="viewSwitchOptions"
|
|
276
|
+
:modelValue="activeViewId"
|
|
277
|
+
:ariaLabel="viewSwitchLabel"
|
|
278
|
+
:controls="viewRegionId"
|
|
279
|
+
data-testid="cn-dashboard-page-view-switch"
|
|
280
|
+
@update:modelValue="selectView" />
|
|
281
|
+
</div>
|
|
282
|
+
<!-- One grid per section: the page's own layout, then the chosen
|
|
283
|
+
view (`views`) in a region the view switch controls. Both are the
|
|
284
|
+
same grid with the same widget rendering below. A view grid is
|
|
285
|
+
keyed on its view, so switching mounts the other view's grid. -->
|
|
250
286
|
<CnDashboardGrid
|
|
251
|
-
v-
|
|
252
|
-
:key="`cn-dashboard-grid-${gridEditable ? 'editing' : 'live'}`"
|
|
253
|
-
|
|
254
|
-
:
|
|
287
|
+
v-for="section in gridSections"
|
|
288
|
+
:key="`cn-dashboard-grid-${section.key}-${gridEditable ? 'editing' : 'live'}`"
|
|
289
|
+
v-bind="section.attrs"
|
|
290
|
+
:layout="section.layout"
|
|
291
|
+
:editable="section.editable"
|
|
255
292
|
:columns="columns"
|
|
256
293
|
:cellHeight="cellHeight"
|
|
257
294
|
:margin="gridMargin"
|
|
258
|
-
@layoutChange="onLayoutChange">
|
|
295
|
+
@layoutChange="section.view ? onViewLayoutChange($event) : onLayoutChange($event)">
|
|
259
296
|
<template #widget="{ item }">
|
|
260
297
|
<!-- In-app edit overlay (ADR-041): a single launchpad-style
|
|
261
298
|
configure cog opens the per-widget style/config editor,
|
|
@@ -320,6 +357,7 @@
|
|
|
320
357
|
:borderless="widgetBorderless(item)"
|
|
321
358
|
:flush="item.flush !== false"
|
|
322
359
|
:buttons="getWidgetButtons(item)"
|
|
360
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
323
361
|
:styleConfig="item.styleConfig || {}"
|
|
324
362
|
:titleIconPosition="getWidgetTitleIconPosition(item)"
|
|
325
363
|
:titleIconColor="getWidgetTitleIconColor(item)"
|
|
@@ -397,9 +435,11 @@
|
|
|
397
435
|
:iconUrl="getWidgetIconUrl(item)"
|
|
398
436
|
:iconClass="getWidgetIconClass(item)"
|
|
399
437
|
:showTitle="widgetShowTitle(item)"
|
|
438
|
+
:showActions="widgetShowActions(item)"
|
|
400
439
|
:borderless="widgetBorderless(item)"
|
|
401
440
|
:flush="item.flush !== false"
|
|
402
441
|
:buttons="getWidgetButtons(item)"
|
|
442
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
403
443
|
:styleConfig="item.styleConfig || {}"
|
|
404
444
|
:titleIconPosition="getWidgetTitleIconPosition(item)"
|
|
405
445
|
:titleIconColor="getWidgetTitleIconColor(item)"
|
|
@@ -478,6 +518,7 @@
|
|
|
478
518
|
:flush="item.flush !== false"
|
|
479
519
|
:class="{ 'cn-dashboard-page__card-fit': isCardWidget(item) }"
|
|
480
520
|
:buttons="getWidgetButtons(item)"
|
|
521
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
481
522
|
:styleConfig="item.styleConfig || {}"
|
|
482
523
|
:documentationUrl="getWidgetDocumentationUrl(item)"
|
|
483
524
|
:docsAnchor="getWidgetDocsAnchor(item)"
|
|
@@ -499,9 +540,11 @@
|
|
|
499
540
|
:iconUrl="getWidgetIconUrl(item)"
|
|
500
541
|
:iconClass="getWidgetIconClass(item)"
|
|
501
542
|
:showTitle="widgetShowTitle(item)"
|
|
543
|
+
:showActions="widgetShowActions(item)"
|
|
502
544
|
:borderless="widgetBorderless(item)"
|
|
503
545
|
:flush="item.flush !== false"
|
|
504
546
|
:buttons="getWidgetButtons(item)"
|
|
547
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
505
548
|
:styleConfig="item.styleConfig || {}"
|
|
506
549
|
:titleIconPosition="getWidgetTitleIconPosition(item)"
|
|
507
550
|
:titleIconColor="getWidgetTitleIconColor(item)"
|
|
@@ -536,7 +579,9 @@
|
|
|
536
579
|
:iconUrl="getWidgetIconUrl(item)"
|
|
537
580
|
:iconClass="getWidgetIconClass(item)"
|
|
538
581
|
:showTitle="widgetShowTitle(item)"
|
|
582
|
+
:showActions="widgetShowActions(item)"
|
|
539
583
|
:buttons="getWidgetButtons(item)"
|
|
584
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
540
585
|
:styleConfig="item.styleConfig || {}"
|
|
541
586
|
:showRefresh="getWidgetShowRefresh(item)"
|
|
542
587
|
:documentationUrl="getWidgetDocumentationUrl(item)"
|
|
@@ -566,6 +611,7 @@
|
|
|
566
611
|
:titleIconVariant="getWidgetTitleIconVariant(item)"
|
|
567
612
|
:class="{ 'cn-dashboard-page__card-fit': isCardWidget(item) }"
|
|
568
613
|
:buttons="getWidgetButtons(item)"
|
|
614
|
+
:headerLink="getWidgetHeaderLink(item)"
|
|
569
615
|
:styleConfig="item.styleConfig || {}"
|
|
570
616
|
:documentationUrl="getWidgetDocumentationUrl(item)"
|
|
571
617
|
:docsAnchor="getWidgetDocsAnchor(item)"
|
|
@@ -640,6 +686,22 @@
|
|
|
640
686
|
</template>
|
|
641
687
|
</CnDashboardGrid>
|
|
642
688
|
|
|
689
|
+
<!-- The chosen view has nothing to draw: say so in the region, never
|
|
690
|
+
leave a blank area under the switch. -->
|
|
691
|
+
<div
|
|
692
|
+
v-if="gridSectionsShown && viewIsEmpty"
|
|
693
|
+
:id="viewRegionId"
|
|
694
|
+
role="region"
|
|
695
|
+
:aria-label="activeView ? activeView.label : null"
|
|
696
|
+
class="cn-page-view-region cn-page-view-region--empty"
|
|
697
|
+
data-testid="cn-dashboard-page-view-empty">
|
|
698
|
+
<NcEmptyContent :description="viewEmptyText">
|
|
699
|
+
<template #icon>
|
|
700
|
+
<ViewDashboardOutline :size="48" />
|
|
701
|
+
</template>
|
|
702
|
+
</NcEmptyContent>
|
|
703
|
+
</div>
|
|
704
|
+
|
|
643
705
|
<!-- Declarative in-body sections, `placement: "after-grid"` — host-app
|
|
644
706
|
section components rendered BELOW the widget grid. -->
|
|
645
707
|
<CnBodySections
|
|
@@ -699,6 +761,7 @@ import CnBuildiqEditButton from '../CnBuildiqEditButton/CnBuildiqEditButton.vue'
|
|
|
699
761
|
import CnChartWidget from '../CnChartWidget/CnChartWidget.vue'
|
|
700
762
|
import CnDashboardGrid from '../CnDashboardGrid/CnDashboardGrid.vue'
|
|
701
763
|
import CnDateRangePicker, { DEFAULT_DATE_RANGE_PRESETS, resolvePresetWindow } from '../CnDateRangePicker/CnDateRangePicker.vue'
|
|
764
|
+
import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
|
|
702
765
|
import CnStatsBlockWidget from '../CnStatsBlockWidget/CnStatsBlockWidget.vue'
|
|
703
766
|
import CnTileWidget from '../CnTileWidget/CnTileWidget.vue'
|
|
704
767
|
import CnWidgetRefItem from '../CnWidgetRefItem/CnWidgetRefItem.vue'
|
|
@@ -706,6 +769,7 @@ import CnWidgetRenderer from '../CnWidgetRenderer/CnWidgetRenderer.vue'
|
|
|
706
769
|
import CnWidgetWrapper from '../CnWidgetWrapper/CnWidgetWrapper.vue'
|
|
707
770
|
import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
|
|
708
771
|
import { readUserPreference, writeUserPreference } from '../../composables/useUserPreferences.js'
|
|
772
|
+
import { pageViews } from '../../mixins/pageViews.js'
|
|
709
773
|
import { dashboardLayoutKey, mergeUserLayout } from '../../store/plugins/dashboardLayouts.js'
|
|
710
774
|
import { isAppInstalled } from '../../utils/appInstalled.js'
|
|
711
775
|
import { compareVisibleWhen, readVisibleWhenValue } from '../../utils/visibleWhen.js'
|
|
@@ -860,8 +924,11 @@ export default {
|
|
|
860
924
|
CnBuildiqEditButton,
|
|
861
925
|
CnWidgetStyleEditorModal,
|
|
862
926
|
CnLeafMountHost,
|
|
927
|
+
CnSegmentedControl,
|
|
863
928
|
},
|
|
864
929
|
|
|
930
|
+
mixins: [pageViews],
|
|
931
|
+
|
|
865
932
|
inject: {
|
|
866
933
|
/**
|
|
867
934
|
* Buildiq in-app edit state (ADR-041), provided by CnAppRoot as a ref.
|
|
@@ -921,6 +988,34 @@ export default {
|
|
|
921
988
|
default: '',
|
|
922
989
|
},
|
|
923
990
|
|
|
991
|
+
/**
|
|
992
|
+
* Whether the header row renders: title, description, header actions
|
|
993
|
+
* and the edit toggle. `false` (manifest `config.showHeader: false`)
|
|
994
|
+
* drops it for a page whose first widget is its heading; the title
|
|
995
|
+
* then renders visually hidden so the page keeps an accessible name.
|
|
996
|
+
*
|
|
997
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-dashboard-page-can-hide-its-header
|
|
998
|
+
*/
|
|
999
|
+
showHeader: {
|
|
1000
|
+
type: Boolean,
|
|
1001
|
+
default: true,
|
|
1002
|
+
},
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Whether a widget draws its overflow Actions menu when neither its
|
|
1006
|
+
* placement nor its definition says (`showActions`). `false` (manifest
|
|
1007
|
+
* `config.showWidgetActions: false`) drops the menu from every such
|
|
1008
|
+
* widget, for a dashboard whose widgets carry a header link and
|
|
1009
|
+
* nothing else. A widget that sets `showActions: true` keeps its menu.
|
|
1010
|
+
* Card widgets have no menu either way.
|
|
1011
|
+
*
|
|
1012
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-dashboard-can-drop-the-widget-actions-menu
|
|
1013
|
+
*/
|
|
1014
|
+
showWidgetActions: {
|
|
1015
|
+
type: Boolean,
|
|
1016
|
+
default: true,
|
|
1017
|
+
},
|
|
1018
|
+
|
|
924
1019
|
/**
|
|
925
1020
|
* Widget definitions array. Each widget defines metadata for rendering.
|
|
926
1021
|
*
|
|
@@ -1836,6 +1931,89 @@ export default {
|
|
|
1836
1931
|
return this.layout.length > 0 || this.widgetRefItems.length > 0
|
|
1837
1932
|
},
|
|
1838
1933
|
|
|
1934
|
+
/**
|
|
1935
|
+
* Whether the grid sections render at all: not while loading, not
|
|
1936
|
+
* while the widget predicates settle, not on the empty page and not
|
|
1937
|
+
* on a widget-ref content page.
|
|
1938
|
+
*
|
|
1939
|
+
* @return {boolean}
|
|
1940
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid
|
|
1941
|
+
*/
|
|
1942
|
+
gridSectionsShown() {
|
|
1943
|
+
if (this.loading || (!this.widgetConditionsSettled && !this.gridEditable)) {
|
|
1944
|
+
return false
|
|
1945
|
+
}
|
|
1946
|
+
if (!this.hasWidgets && !this.hasBodyWidgets && !this.hasViews) {
|
|
1947
|
+
return false
|
|
1948
|
+
}
|
|
1949
|
+
return this.widgetRefItems.length === 0
|
|
1950
|
+
},
|
|
1951
|
+
|
|
1952
|
+
/**
|
|
1953
|
+
* The chosen view's layout as the grid draws it: in live mode the
|
|
1954
|
+
* widgets that would draw nothing leave and the rest close up, as
|
|
1955
|
+
* for the page's own layout.
|
|
1956
|
+
*
|
|
1957
|
+
* @return {Array<object>}
|
|
1958
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so
|
|
1959
|
+
*/
|
|
1960
|
+
viewDisplayLayout() {
|
|
1961
|
+
const items = this.activeView ? this.activeView.layout : []
|
|
1962
|
+
if (this.gridEditable) {
|
|
1963
|
+
return items
|
|
1964
|
+
}
|
|
1965
|
+
const visible = items.filter((item) => !this.isCollapsedWidget(item))
|
|
1966
|
+
return visible.length === items.length ? items : this.compactDisplayLayout(visible)
|
|
1967
|
+
},
|
|
1968
|
+
|
|
1969
|
+
/**
|
|
1970
|
+
* Whether the chosen view has nothing to draw.
|
|
1971
|
+
*
|
|
1972
|
+
* @return {boolean}
|
|
1973
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so
|
|
1974
|
+
*/
|
|
1975
|
+
viewIsEmpty() {
|
|
1976
|
+
return this.hasViews && this.viewDisplayLayout.length === 0
|
|
1977
|
+
},
|
|
1978
|
+
|
|
1979
|
+
/**
|
|
1980
|
+
* The grids this page draws. Without views: the page's own layout,
|
|
1981
|
+
* as it always was. With views: the page's own layout when it has
|
|
1982
|
+
* items, then the chosen view's layout in the region the switch
|
|
1983
|
+
* controls (left out when the view is empty; the empty state takes
|
|
1984
|
+
* the region then).
|
|
1985
|
+
*
|
|
1986
|
+
* @return {Array<{key: string, view: boolean, layout: Array<object>, editable: boolean, attrs: object}>}
|
|
1987
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid
|
|
1988
|
+
*/
|
|
1989
|
+
gridSections() {
|
|
1990
|
+
if (!this.gridSectionsShown) {
|
|
1991
|
+
return []
|
|
1992
|
+
}
|
|
1993
|
+
const page = { key: 'page', view: false, layout: this.displayLayout, editable: this.gridEditable, attrs: {} }
|
|
1994
|
+
if (!this.hasViews) {
|
|
1995
|
+
return [page]
|
|
1996
|
+
}
|
|
1997
|
+
const sections = this.displayLayout.length > 0 ? [page] : []
|
|
1998
|
+
if (!this.viewIsEmpty) {
|
|
1999
|
+
sections.push({
|
|
2000
|
+
key: `view-${this.activeViewId}`,
|
|
2001
|
+
view: true,
|
|
2002
|
+
layout: this.viewDisplayLayout,
|
|
2003
|
+
// A user arrangement covers the page's own layout only.
|
|
2004
|
+
editable: this.gridEditable && !this.userLayout,
|
|
2005
|
+
attrs: {
|
|
2006
|
+
id: this.viewRegionId,
|
|
2007
|
+
role: 'region',
|
|
2008
|
+
'aria-label': this.activeView.label,
|
|
2009
|
+
class: 'cn-page-view-region',
|
|
2010
|
+
'data-testid': 'cn-dashboard-page-view-region',
|
|
2011
|
+
},
|
|
2012
|
+
})
|
|
2013
|
+
}
|
|
2014
|
+
return sections
|
|
2015
|
+
},
|
|
2016
|
+
|
|
1839
2017
|
/**
|
|
1840
2018
|
* The widget settings object handed to CnWidgetStyleEditorModal,
|
|
1841
2019
|
* built from the widget definition currently being configured
|
|
@@ -1966,6 +2144,14 @@ export default {
|
|
|
1966
2144
|
this.evaluateWidgetConditions()
|
|
1967
2145
|
},
|
|
1968
2146
|
},
|
|
2147
|
+
|
|
2148
|
+
/** A view's widgets carry predicates too. */
|
|
2149
|
+
views: {
|
|
2150
|
+
deep: true,
|
|
2151
|
+
handler() {
|
|
2152
|
+
this.evaluateWidgetConditions()
|
|
2153
|
+
},
|
|
2154
|
+
},
|
|
1969
2155
|
},
|
|
1970
2156
|
|
|
1971
2157
|
created() {
|
|
@@ -2830,7 +3016,8 @@ export default {
|
|
|
2830
3016
|
* @spec openspec/changes/live-check-follow-ups/specs/live-check-follow-ups/spec.md#requirement-an-attention-card-says-when-it-could-not-check
|
|
2831
3017
|
*/
|
|
2832
3018
|
async evaluateWidgetConditions() {
|
|
2833
|
-
const
|
|
3019
|
+
const defs = [...(this.widgets || []), ...this.normalizedViews.flatMap((view) => view.widgets)]
|
|
3020
|
+
const conditional = defs.filter((def) => def && def.id && def.type
|
|
2834
3021
|
&& this.widgetDisplayConfig(def).visibleWhen)
|
|
2835
3022
|
// Skip when the CONDITIONAL SET is unchanged: the deep `widgets`
|
|
2836
3023
|
// watch fires on any def edit, but only an added/removed/edited
|
|
@@ -2964,7 +3151,7 @@ export default {
|
|
|
2964
3151
|
*/
|
|
2965
3152
|
getWidgetDef(widgetId) {
|
|
2966
3153
|
const list = Array.isArray(this.widgets) ? this.widgets : []
|
|
2967
|
-
return list.find((w) => w && w.id === widgetId) ||
|
|
3154
|
+
return list.find((w) => w && w.id === widgetId) || this.findViewWidget(widgetId)
|
|
2968
3155
|
},
|
|
2969
3156
|
|
|
2970
3157
|
/**
|
|
@@ -3355,11 +3542,20 @@ export default {
|
|
|
3355
3542
|
* exactly that reason; there is now one rule for every family, so a
|
|
3356
3543
|
* custom slot and a registered widget cannot disagree about it.
|
|
3357
3544
|
*
|
|
3545
|
+
* A greeting header that declares `content.ground: true` sits on the
|
|
3546
|
+
* page ground, so it asks for no card either: one key on the widget
|
|
3547
|
+
* instead of a second one on its placement.
|
|
3548
|
+
*
|
|
3549
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-greeting-can-sit-on-the-page-ground
|
|
3358
3550
|
* @param {object} item the layout placement.
|
|
3359
3551
|
* @return {boolean}
|
|
3360
3552
|
*/
|
|
3361
3553
|
widgetBorderless(item) {
|
|
3362
|
-
|
|
3554
|
+
if (item.borderless === true) {
|
|
3555
|
+
return true
|
|
3556
|
+
}
|
|
3557
|
+
const def = this.getWidgetDef(item.widgetId)
|
|
3558
|
+
return Boolean(def && def.type === 'header' && this.getWidgetContent(item).ground === true)
|
|
3363
3559
|
},
|
|
3364
3560
|
|
|
3365
3561
|
/**
|
|
@@ -3376,7 +3572,7 @@ export default {
|
|
|
3376
3572
|
const def = this.getWidgetDef(item.widgetId)
|
|
3377
3573
|
const value = item.showActions !== undefined ? item.showActions : def?.showActions
|
|
3378
3574
|
if (value === undefined || value === null) {
|
|
3379
|
-
return !this.isCardWidget(item)
|
|
3575
|
+
return this.showWidgetActions && !this.isCardWidget(item)
|
|
3380
3576
|
}
|
|
3381
3577
|
return value !== false
|
|
3382
3578
|
},
|
|
@@ -3410,6 +3606,22 @@ export default {
|
|
|
3410
3606
|
return def?.buttons || []
|
|
3411
3607
|
},
|
|
3412
3608
|
|
|
3609
|
+
/**
|
|
3610
|
+
* The widget's header text link (`headerLink` on the layout entry,
|
|
3611
|
+
* else on the widget definition), forwarded to CnWidgetWrapper.
|
|
3612
|
+
*
|
|
3613
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-widget-header-carries-a-text-link
|
|
3614
|
+
* @param {object} item Layout placement entry.
|
|
3615
|
+
* @return {object|null}
|
|
3616
|
+
*/
|
|
3617
|
+
getWidgetHeaderLink(item) {
|
|
3618
|
+
if (item?.headerLink && typeof item.headerLink === 'object') {
|
|
3619
|
+
return item.headerLink
|
|
3620
|
+
}
|
|
3621
|
+
const def = this.getWidgetDef(item.widgetId)
|
|
3622
|
+
return (def?.headerLink && typeof def.headerLink === 'object') ? def.headerLink : null
|
|
3623
|
+
},
|
|
3624
|
+
|
|
3413
3625
|
/**
|
|
3414
3626
|
* Whether a widget's overflow Actions menu shows the Refresh item.
|
|
3415
3627
|
* An explicit per-widget flag wins — on the widget definition or
|
|
@@ -3953,6 +4165,19 @@ export default {
|
|
|
3953
4165
|
padding: 60px 20px;
|
|
3954
4166
|
}
|
|
3955
4167
|
|
|
4168
|
+
/* The page's view switch on a page without a header row: at the end of its
|
|
4169
|
+
own row, above the grids, where the header would have put it. */
|
|
4170
|
+
.cn-dashboard-page__view-switch {
|
|
4171
|
+
display: flex;
|
|
4172
|
+
justify-content: flex-end;
|
|
4173
|
+
margin-bottom: 12px;
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
/* A view the switch shows with nothing to draw. */
|
|
4177
|
+
.cn-page-view-region--empty {
|
|
4178
|
+
padding: 40px 20px;
|
|
4179
|
+
}
|
|
4180
|
+
|
|
3956
4181
|
.cn-dashboard-page__page-filters {
|
|
3957
4182
|
display: flex;
|
|
3958
4183
|
flex-wrap: wrap;
|
|
@@ -91,9 +91,9 @@
|
|
|
91
91
|
</label>
|
|
92
92
|
</div>
|
|
93
93
|
|
|
94
|
-
<!-- string:
|
|
94
|
+
<!-- string: contains (OpenRegister `[like]`, case-insensitive) -->
|
|
95
95
|
<label v-else class="cn-column-filter__field">
|
|
96
|
-
<span class="cn-column-filter__label">{{
|
|
96
|
+
<span class="cn-column-filter__label">{{ containsLabel }}</span>
|
|
97
97
|
<input
|
|
98
98
|
v-model="draft.value"
|
|
99
99
|
class="cn-column-filter__input"
|
|
@@ -131,7 +131,7 @@ let popoverSeq = 0
|
|
|
131
131
|
*
|
|
132
132
|
* Renders the control that fits the column: a checkbox list for an enum, yes,
|
|
133
133
|
* no or any for a boolean, from and to for a number or a date, a searchable
|
|
134
|
-
* list of referenced objects for a reference, and
|
|
134
|
+
* list of referenced objects for a reference, and a contains box for text.
|
|
135
135
|
* Real inputs throughout, so every control works from the keyboard. Escape
|
|
136
136
|
* closes it and the host returns focus to the filter button.
|
|
137
137
|
*
|
|
@@ -265,8 +265,14 @@ export default {
|
|
|
265
265
|
return t('nextcloud-vue', 'To')
|
|
266
266
|
},
|
|
267
267
|
|
|
268
|
-
|
|
269
|
-
|
|
268
|
+
/**
|
|
269
|
+
* Label of the text box: the text filter matches on contains.
|
|
270
|
+
*
|
|
271
|
+
* @return {string}
|
|
272
|
+
* @spec openspec/changes/header-filter-contains/specs/cn-data-table/spec.md#requirement-a-text-header-filter-matches-on-contains
|
|
273
|
+
*/
|
|
274
|
+
containsLabel() {
|
|
275
|
+
return t('nextcloud-vue', 'Contains')
|
|
270
276
|
},
|
|
271
277
|
|
|
272
278
|
clearLabel() {
|
|
@@ -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.
|