@conduction/nextcloud-vue 2.127.0 → 2.128.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/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +3 -113
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +12 -12
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +34 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +16 -8
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +153 -8
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +169 -58
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnTileWidget/CnTileWidget.vue.js +1 -1
- package/dist/esm/components/CnTileWidget/CnTileWidget.vue2.js +11 -0
- package/dist/esm/components/CnTileWidget/CnTileWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTileWidget/CnTileWidget.vue3.js +14 -4
- package/dist/esm/components/CnTileWidget/CnTileWidget.vue3.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/mixins/pageFilters.js +142 -0
- package/dist/esm/mixins/pageFilters.js.map +1 -0
- package/dist/esm/nextcloud-vue.css +269 -74
- package/dist/esm/utils/actionLink.js +9 -3
- package/dist/esm/utils/actionLink.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +1176 -828
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +269 -74
- package/package.json +1 -1
- package/src/components/CnDashboardPage/CnDashboardPage.vue +3 -113
- package/src/components/CnDataTable/CnDataTable.vue +40 -1
- package/src/components/CnDetailPage/CnDetailPage.md +2 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +237 -8
- package/src/components/CnTileWidget/CnTileWidget.vue +30 -2
- package/src/css/detail-page.css +42 -0
- package/src/css/dialog.css +13 -0
- package/src/css/look-board-detail.css +1 -1
- package/src/css/look-board-index.css +8 -0
- package/src/css/look-board.css +97 -0
- package/src/css/table.css +12 -0
- package/src/mixins/pageFilters.js +138 -0
- package/src/schemas/app-manifest-v2.schema.json +41 -3
- package/src/utils/actionLink.js +9 -3
- package/src/utils/validateManifestV2.compiled.js +3 -3
package/src/css/detail-page.css
CHANGED
|
@@ -464,6 +464,48 @@
|
|
|
464
464
|
flex: 1 1 300px;
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
+
/* The band (`config.band`): a wrapping row across the page, above the body
|
|
468
|
+
and the side column. An entry takes `--cn-band-span` twelfths of the row,
|
|
469
|
+
less its share of the gaps, and wraps under 220px. It is a full line in the
|
|
470
|
+
with-side flex row, like every child but the body and the column. */
|
|
471
|
+
.cn-detail-page__band {
|
|
472
|
+
display: flex;
|
|
473
|
+
flex-wrap: wrap;
|
|
474
|
+
gap: calc(5 * var(--default-grid-baseline));
|
|
475
|
+
margin-block-end: calc(5 * var(--default-grid-baseline));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/* The page filters: a right-aligned row above the band's widgets, a label
|
|
479
|
+
before each control. */
|
|
480
|
+
.cn-detail-page__page-filters {
|
|
481
|
+
display: flex;
|
|
482
|
+
flex: 1 1 100%;
|
|
483
|
+
flex-wrap: wrap;
|
|
484
|
+
justify-content: flex-end;
|
|
485
|
+
align-items: center;
|
|
486
|
+
gap: calc(2 * var(--default-grid-baseline)) calc(4 * var(--default-grid-baseline));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.cn-detail-page__page-filter {
|
|
490
|
+
display: inline-flex;
|
|
491
|
+
align-items: center;
|
|
492
|
+
gap: calc(2 * var(--default-grid-baseline));
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
.cn-detail-page__page-filter-label {
|
|
496
|
+
color: var(--color-text-maxcontrast);
|
|
497
|
+
font-size: 14px;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
.cn-detail-page__band-item {
|
|
501
|
+
flex: 1 1 calc(var(--cn-band-span, 12) / 12 * 100% - calc(5 * var(--default-grid-baseline)));
|
|
502
|
+
min-width: min(220px, 100%);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.cn-detail-page__band-item--full {
|
|
506
|
+
flex-basis: 100%;
|
|
507
|
+
}
|
|
508
|
+
|
|
467
509
|
.cn-detail-page__side {
|
|
468
510
|
display: flex;
|
|
469
511
|
flex-direction: column;
|
package/src/css/dialog.css
CHANGED
|
@@ -314,6 +314,19 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
|
|
|
314
314
|
border-radius: 8px;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
/* A field's help line sits right under the control in 13px muted ink, flush
|
|
318
|
+
with the label, as the boards draw it (PtUitnodigen "De uitnodiging met een
|
|
319
|
+
eenmalige code gaat naar dit adres.", PtAccountUitgeven). The ink carries
|
|
320
|
+
`!important` against a theme that colours every span and p (thematiq).
|
|
321
|
+
@spec openspec/changes/board-dialog-field-help/specs/board-dialog-field-help/spec.md#requirement-a-board-dialog-fields-help-line-is-13px-muted-under-the-control */
|
|
322
|
+
.cn-dialog-board .dialog__content .input-field__helper-text-message,
|
|
323
|
+
.cn-dialog-board .dialog__content .textarea__helper-text-message {
|
|
324
|
+
padding: 4px 0 0;
|
|
325
|
+
color: var(--color-text-maxcontrast) !important;
|
|
326
|
+
font-size: 13px;
|
|
327
|
+
line-height: 1.3;
|
|
328
|
+
}
|
|
329
|
+
|
|
317
330
|
.cn-dialog-board .dialog__content .input-field:has(.input-field__label) .input-field__input:not(:focus)::placeholder,
|
|
318
331
|
.cn-dialog-board .dialog__content .textarea__input:not(:focus)::placeholder {
|
|
319
332
|
opacity: 1 !important;
|
|
@@ -575,7 +575,7 @@
|
|
|
575
575
|
}
|
|
576
576
|
|
|
577
577
|
/* The same for a CnDetailCard (DqTaak "Notities", DqZaakEmail "E-mail",
|
|
578
|
-
DqZaakTaken "Afspraken", DqHandhaving "Historie"). */
|
|
578
|
+
DqZaakTaken "Afspraken", DqHandhaving "Historie", PtInzending "Bewijslog"). */
|
|
579
579
|
.cn-look-board.cn-detail-page .cn-detail-card__header .cn-detail-card__icon {
|
|
580
580
|
display: none;
|
|
581
581
|
}
|
|
@@ -1560,6 +1560,14 @@
|
|
|
1560
1560
|
color: var(--color-primary-element-light-text);
|
|
1561
1561
|
}
|
|
1562
1562
|
|
|
1563
|
+
/* A row the manifest marks as asking for attention (`config.rowClass` with
|
|
1564
|
+
`class: "cn-row--attention"`) is tinted warm, as the boards draw the open
|
|
1565
|
+
requests on top of a list (PtToegangsverzoeken, PtUitnodigingen).
|
|
1566
|
+
@spec openspec/changes/board-attention-row/specs/board-attention-row/spec.md#requirement-a-board-table-row-marked-for-attention-is-tinted-warm */
|
|
1567
|
+
.cn-look-board .cn-data-table tr.cn-row--attention > td:not(.cn-look-nextcloud *) {
|
|
1568
|
+
background: color-mix(in srgb, var(--color-warning) 4%, var(--color-main-background));
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1563
1571
|
/* ---- A kanban column's show-more button ---------------------------------- */
|
|
1564
1572
|
/* The board draws "Toon 12 meer" in the soft ink, its dashed edge in the 3:1
|
|
1565
1573
|
control colour, 6px in at the sides (DqWerkbord); the view draws the main
|
package/src/css/look-board.css
CHANGED
|
@@ -1659,6 +1659,103 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .v-popper__popper.action-
|
|
|
1659
1659
|
align-self: flex-start;
|
|
1660
1660
|
}
|
|
1661
1661
|
|
|
1662
|
+
/* ---- Warning and error banners (CnBannerWidget) ------------------------- */
|
|
1663
|
+
/* The boards draw a warning or error banner as a tinted box with a 1px edge
|
|
1664
|
+
in its own tone, radius 10, 12px 14px of padding, the text in the main ink
|
|
1665
|
+
(PtInzending "De ontvangstbevestiging kwam niet aan.", PtAccounts
|
|
1666
|
+
"aanmeldingen wachten op goedkeuring"), not NcNoteCard's 4px edge bar. A
|
|
1667
|
+
settings card's notes take the same box, success included
|
|
1668
|
+
(PtBeheerinstellingen "Er is een eigen ondertekeningssleutel ingesteld").
|
|
1669
|
+
@spec openspec/changes/board-banner-note-tone/specs/board-banner-note-tone/spec.md#requirement-a-board-warning-or-error-banner-is-a-tinted-box-with-an-edge-in-its-tone */
|
|
1670
|
+
.cn-look-board .cn-banner-widget .notecard.notecard--warning:not(.cn-look-nextcloud *),
|
|
1671
|
+
.cn-look-board .settings-section .notecard.notecard--warning:not(.cn-look-nextcloud *) {
|
|
1672
|
+
--cn-banner-ground: color-mix(in srgb, var(--color-warning) 13%, var(--color-main-background));
|
|
1673
|
+
--cn-banner-edge: color-mix(in srgb, var(--color-warning) 50%, var(--color-main-background));
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
.cn-look-board .cn-banner-widget .notecard.notecard--error:not(.cn-look-nextcloud *),
|
|
1677
|
+
.cn-look-board .settings-section .notecard.notecard--error:not(.cn-look-nextcloud *) {
|
|
1678
|
+
--cn-banner-ground: color-mix(in srgb, var(--color-error) 7%, var(--color-main-background));
|
|
1679
|
+
--cn-banner-edge: color-mix(in srgb, var(--color-error) 30%, var(--color-main-background));
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
.cn-look-board .settings-section .notecard.notecard--success:not(.cn-look-nextcloud *) {
|
|
1683
|
+
--cn-banner-ground: color-mix(in srgb, var(--color-success) 9%, var(--color-main-background));
|
|
1684
|
+
--cn-banner-edge: color-mix(in srgb, var(--color-success) 30%, var(--color-main-background));
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
.cn-look-board .cn-banner-widget .notecard.notecard--warning:not(.cn-look-nextcloud *),
|
|
1688
|
+
.cn-look-board .cn-banner-widget .notecard.notecard--error:not(.cn-look-nextcloud *),
|
|
1689
|
+
.cn-look-board .settings-section .notecard.notecard--warning:not(.cn-look-nextcloud *),
|
|
1690
|
+
.cn-look-board .settings-section .notecard.notecard--error:not(.cn-look-nextcloud *),
|
|
1691
|
+
.cn-look-board .settings-section .notecard.notecard--success:not(.cn-look-nextcloud *) {
|
|
1692
|
+
--note-card-icon-size: 18px;
|
|
1693
|
+
gap: 10px;
|
|
1694
|
+
margin: 0;
|
|
1695
|
+
padding: 12px 14px;
|
|
1696
|
+
border: 1px solid var(--cn-banner-edge);
|
|
1697
|
+
border-radius: 10px;
|
|
1698
|
+
background-color: var(--cn-banner-ground) !important;
|
|
1699
|
+
color: var(--color-main-text) !important;
|
|
1700
|
+
font-size: 14px;
|
|
1701
|
+
line-height: 1.5;
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
/* A stat-row cell that holds only a banner is the banner: no card around it. */
|
|
1705
|
+
.cn-look-board .cn-index-page-widgets__cell .cn-widget-wrapper:has(.cn-banner-widget):not(:has(.cn-widget-wrapper__header)):not(.cn-look-nextcloud *) {
|
|
1706
|
+
border: none;
|
|
1707
|
+
background: transparent;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
.cn-look-board .cn-index-page-widgets__cell .cn-widget-wrapper:has(.cn-banner-widget):not(:has(.cn-widget-wrapper__header)):not(.cn-look-nextcloud *) > .cn-widget-wrapper__content {
|
|
1711
|
+
padding: 0;
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
/* ---- Link tiles (CnTileWidget) ------------------------------------------ */
|
|
1715
|
+
/* The board draws a link tile as a rounded card, its icon on the left beside
|
|
1716
|
+
a 15px bold title and a 13px line under it, all left-aligned (PtThemas
|
|
1717
|
+
"Themacatalogus", "Portalen en hun thema's"), not a square with a centred
|
|
1718
|
+
48px icon.
|
|
1719
|
+
@spec openspec/changes/board-link-tile/specs/board-link-tile/spec.md#requirement-a-board-link-tile-is-a-rounded-card-with-its-icon-beside-the-title */
|
|
1720
|
+
.cn-look-board .cn-tile-widget:not(.cn-look-nextcloud *) {
|
|
1721
|
+
border-radius: var(--cn-board-card-radius, 12px);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
.cn-look-board .cn-tile-widget .cn-tile-widget__link:not(.cn-look-nextcloud *) {
|
|
1725
|
+
flex-direction: row;
|
|
1726
|
+
justify-content: flex-start;
|
|
1727
|
+
gap: 14px;
|
|
1728
|
+
padding: 20px 22px;
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
.cn-look-board .cn-tile-widget .cn-tile-widget__link:not(.cn-look-nextcloud *):hover {
|
|
1732
|
+
transform: none;
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
.cn-look-board .cn-tile-widget .cn-tile-widget__icon:not(.cn-look-nextcloud *),
|
|
1736
|
+
.cn-look-board .cn-tile-widget .cn-tile-widget__icon--svg:not(.cn-look-nextcloud *) {
|
|
1737
|
+
width: 24px;
|
|
1738
|
+
height: 24px;
|
|
1739
|
+
font-size: 24px;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
.cn-look-board .cn-tile-widget .cn-tile-widget__text:not(.cn-look-nextcloud *) {
|
|
1743
|
+
display: flex;
|
|
1744
|
+
flex-direction: column;
|
|
1745
|
+
gap: 2px;
|
|
1746
|
+
min-width: 0;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
.cn-look-board .cn-tile-widget .cn-tile-widget__title:not(.cn-look-nextcloud *),
|
|
1750
|
+
.cn-look-board .cn-tile-widget .cn-tile-widget__subtitle:not(.cn-look-nextcloud *) {
|
|
1751
|
+
text-align: start;
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
.cn-look-board .cn-tile-widget .cn-tile-widget__title:not(.cn-look-nextcloud *) {
|
|
1755
|
+
font-size: 15px;
|
|
1756
|
+
line-height: 1.2;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1662
1759
|
/* A record list's notice is the board's note box too: 10px 14px, radius 10
|
|
1663
1760
|
(DqZaaktypeOverzicht).
|
|
1664
1761
|
@spec openspec/changes/board-record-list-notice-box/specs/board-record-list-notice-box/spec.md#requirement-a-board-record-list-notice-is-the-boards-note-box */
|
package/src/css/table.css
CHANGED
|
@@ -186,6 +186,18 @@
|
|
|
186
186
|
text-overflow: ellipsis;
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/* The overline of a cell (`columns[].overline` / `overlineText`): muted and
|
|
190
|
+
smaller, on its own line above the value. Same theme hooks as the
|
|
191
|
+
secondary line. */
|
|
192
|
+
.cn-data-table .cn-table-cell__overline {
|
|
193
|
+
display: block;
|
|
194
|
+
color: var(--cn-table-secondary-color, var(--color-text-maxcontrast));
|
|
195
|
+
font-size: var(--cn-table-secondary-size, 0.9em);
|
|
196
|
+
line-height: 1.35;
|
|
197
|
+
overflow: hidden;
|
|
198
|
+
text-overflow: ellipsis;
|
|
199
|
+
}
|
|
200
|
+
|
|
189
201
|
/* Back to wrapping, for a column holding prose rather than a label:
|
|
190
202
|
`columns: [{ key: 'description', cellClass: 'cn-cell--wrap' }]`. */
|
|
191
203
|
.cn-data-table th.cn-cell--wrap,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { isOptionsFrom, loadOptionsFrom } from '../utils/optionsFrom.js'
|
|
7
|
+
import { generatedPageFilterOptions } from '../utils/periodOptions.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Page-level filters (manifest `config.pageFilters`), shared by CnDashboardPage
|
|
11
|
+
* and CnDetailPage: seeding each filter's value, reading its options and
|
|
12
|
+
* writing a choice into the page's workspace context, where a widget reads it
|
|
13
|
+
* through `@page.<key>` / `@workspace.<key>`.
|
|
14
|
+
*
|
|
15
|
+
* The host provides `pageFilters` (the declared filters), `workspaceContext`
|
|
16
|
+
* (the reactive bag it provides as `cnWorkspaceContext`) and
|
|
17
|
+
* `effectiveTranslate` (the host app's translate).
|
|
18
|
+
*
|
|
19
|
+
* @spec openspec/changes/detail-page-filters/specs/detail-page/spec.md#requirement-a-detail-page-can-draw-page-filters-that-its-widgets-read
|
|
20
|
+
*/
|
|
21
|
+
export const pageFilters = {
|
|
22
|
+
data() {
|
|
23
|
+
return {
|
|
24
|
+
/** Options read through a page filter's `optionsFrom`, by filter key. */
|
|
25
|
+
pageFilterFetched: {},
|
|
26
|
+
/** Whether a page filter's `optionsFrom` read is in flight, by filter key. */
|
|
27
|
+
pageFilterLoading: {},
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
methods: {
|
|
32
|
+
/**
|
|
33
|
+
* Seed the reactive workspace context with each page filter's default
|
|
34
|
+
* (its `default`, else the first option's value) so endpoint-bound
|
|
35
|
+
* widgets have a value to interpolate on first render. Only writes keys
|
|
36
|
+
* that aren't already present, so a value another widget set survives.
|
|
37
|
+
*
|
|
38
|
+
* @return {void}
|
|
39
|
+
*/
|
|
40
|
+
initPageFilters() {
|
|
41
|
+
for (const pf of this.pageFilters || []) {
|
|
42
|
+
if (!pf || !pf.key) {
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
if (this.workspaceContext[pf.key] !== undefined) {
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
const listed = Array.isArray(pf.options) ? pf.options : generatedPageFilterOptions(pf)
|
|
49
|
+
const fallback = (listed && listed.length && pf.clearable !== true) ? listed[0].value : undefined
|
|
50
|
+
const value = pf.default !== undefined ? pf.default : fallback
|
|
51
|
+
if (value !== undefined) {
|
|
52
|
+
this.workspaceContext[pf.key] = value
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The currently-selected option object for a page filter's NcSelect
|
|
59
|
+
* (matched by `value` against the workspace context), or null.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} pf The page-filter descriptor.
|
|
62
|
+
* @return {object|null}
|
|
63
|
+
*/
|
|
64
|
+
selectedPageFilterOption(pf) {
|
|
65
|
+
if (!pf || !pf.key) {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
const current = this.workspaceContext[pf.key]
|
|
69
|
+
return this.pageFilterSegments(pf).find((o) => o.value === current) || null
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* A page filter's options, labels through the host translate: the
|
|
74
|
+
* segments of a segmented filter and the options of a select.
|
|
75
|
+
*
|
|
76
|
+
* @spec openspec/changes/dashboard-board-termijnen/specs/dashboard-page/spec.md#requirement-a-page-filter-as-a-segmented-group-on-the-description-line
|
|
77
|
+
* @param {object} pf The page-filter descriptor.
|
|
78
|
+
* @return {Array<{value: (string|number), label: string}>}
|
|
79
|
+
*/
|
|
80
|
+
pageFilterSegments(pf) {
|
|
81
|
+
const options = Array.isArray(pf.options) ? pf.options : (generatedPageFilterOptions(pf) || this.pageFilterFetched[pf.key] || [])
|
|
82
|
+
return options.map((o) => ({ value: o.value, label: o.label ? this.effectiveTranslate(o.label) : String(o.value) }))
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Read the options of every page filter with `optionsFrom`, then seed
|
|
87
|
+
* a filter that is not clearable with its first option when it has no
|
|
88
|
+
* value yet.
|
|
89
|
+
*
|
|
90
|
+
* @spec openspec/changes/page-filter-options-from/specs/dashboard-page/spec.md#requirement-a-page-filter-can-read-its-options
|
|
91
|
+
* @return {Promise<void>}
|
|
92
|
+
*/
|
|
93
|
+
async loadPageFilterOptions() {
|
|
94
|
+
const ctx = { workspace: this.workspaceContext || {} }
|
|
95
|
+
await Promise.all((this.pageFilters || [])
|
|
96
|
+
.filter((pf) => pf && pf.key && !Array.isArray(pf.options) && pf.optionsFrom && typeof pf.optionsFrom === 'object')
|
|
97
|
+
.map(async (pf) => {
|
|
98
|
+
const from = pf.optionsFrom
|
|
99
|
+
if (!isOptionsFrom(from)) {
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
this.pageFilterLoading = { ...this.pageFilterLoading, [pf.key]: true }
|
|
103
|
+
let options
|
|
104
|
+
try {
|
|
105
|
+
options = (await loadOptionsFrom(from, ctx)).map((o) => ({ value: o.id, label: o.label }))
|
|
106
|
+
} catch {
|
|
107
|
+
options = []
|
|
108
|
+
}
|
|
109
|
+
this.pageFilterFetched = { ...this.pageFilterFetched, [pf.key]: options }
|
|
110
|
+
this.pageFilterLoading = { ...this.pageFilterLoading, [pf.key]: false }
|
|
111
|
+
if (pf.clearable !== true && pf.default === undefined && this.workspaceContext[pf.key] === undefined && options.length > 0) {
|
|
112
|
+
this.workspaceContext[pf.key] = options[0].value
|
|
113
|
+
}
|
|
114
|
+
}))
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Write a page filter's new selection into the reactive workspace
|
|
119
|
+
* context so every `@page.<key>` / `@workspace.<key>` token re-resolves.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} pf The page-filter descriptor.
|
|
122
|
+
* @param {object|null} option The chosen NcSelect option (`{ value, label }`).
|
|
123
|
+
* @return {void}
|
|
124
|
+
*/
|
|
125
|
+
onPageFilterChange(pf, option) {
|
|
126
|
+
if (!pf || !pf.key) {
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
const value = option && typeof option === 'object' ? option.value : (option ?? null)
|
|
130
|
+
this.workspaceContext[pf.key] = value
|
|
131
|
+
/**
|
|
132
|
+
* @event page-filter-change Emitted when a page-level filter selection changes.
|
|
133
|
+
* @type {{ key: string, value: (string|number|null) }}
|
|
134
|
+
*/
|
|
135
|
+
this.$emit('page-filter-change', { key: pf.key, value })
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
|
|
4
4
|
"title": "Conduction App Manifest v2",
|
|
5
5
|
"description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.145.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -1581,9 +1581,14 @@
|
|
|
1581
1581
|
"type": "string",
|
|
1582
1582
|
"description": "Named vue-router route to navigate to on click."
|
|
1583
1583
|
},
|
|
1584
|
+
"query": {
|
|
1585
|
+
"type": "object",
|
|
1586
|
+
"additionalProperties": { "type": "string" },
|
|
1587
|
+
"description": "Query for `route`, e.g. { \"action\": \"create\" } to open an index page's create flow (detail-band, 2.140.0)."
|
|
1588
|
+
},
|
|
1584
1589
|
"href": {
|
|
1585
1590
|
"type": "string",
|
|
1586
|
-
"description": "External URL opened in a new tab on click."
|
|
1591
|
+
"description": "External URL opened in a new tab on click. On a detail page it may name the record's fields as @object.<field> tokens."
|
|
1587
1592
|
},
|
|
1588
1593
|
"payload": {
|
|
1589
1594
|
"description": "Free-form payload echoed back to the host inside the @primary-action-click event."
|
|
@@ -4600,6 +4605,39 @@
|
|
|
4600
4605
|
"$ref": "#/$defs/headerPill",
|
|
4601
4606
|
"description": "type='detail': a pill above the title saying where the record stands."
|
|
4602
4607
|
},
|
|
4608
|
+
"band": {
|
|
4609
|
+
"type": "array",
|
|
4610
|
+
"description": "type='detail': a row of widgets across the whole page, above the body and the side column (detail-band). An item is the id of a widget declared on the page, a widget definition, or { widgetId, span, chrome } where span is the share of the row in twelfths (default 12) and chrome is card or bare. The row wraps; each item follows its visibleWhen.",
|
|
4611
|
+
"items": {
|
|
4612
|
+
"oneOf": [
|
|
4613
|
+
{ "type": "string", "minLength": 1 },
|
|
4614
|
+
{
|
|
4615
|
+
"type": "object",
|
|
4616
|
+
"additionalProperties": false,
|
|
4617
|
+
"required": ["widgetId"],
|
|
4618
|
+
"properties": {
|
|
4619
|
+
"widgetId": { "type": "string", "minLength": 1 },
|
|
4620
|
+
"span": { "type": "integer", "minimum": 1, "maximum": 12 },
|
|
4621
|
+
"chrome": { "type": "string", "enum": ["card", "bare"], "description": "card (default) or bare, for a widget that draws its own card; a banner defaults to bare." }
|
|
4622
|
+
}
|
|
4623
|
+
},
|
|
4624
|
+
{
|
|
4625
|
+
"type": "object",
|
|
4626
|
+
"additionalProperties": true,
|
|
4627
|
+
"required": ["type"],
|
|
4628
|
+
"not": { "required": ["widgetId"] },
|
|
4629
|
+
"properties": {
|
|
4630
|
+
"id": { "type": "string" },
|
|
4631
|
+
"type": { "type": "string", "minLength": 1 },
|
|
4632
|
+
"title": { "type": "string" },
|
|
4633
|
+
"span": { "type": "integer", "minimum": 1, "maximum": 12 },
|
|
4634
|
+
"chrome": { "type": "string", "enum": ["card", "bare"], "description": "card (default) or bare, for a widget that draws its own card; a banner defaults to bare." },
|
|
4635
|
+
"content": { "type": "object", "additionalProperties": true }
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
]
|
|
4639
|
+
}
|
|
4640
|
+
},
|
|
4603
4641
|
"sideColumn": {
|
|
4604
4642
|
"type": "array",
|
|
4605
4643
|
"description": "type='detail': a column of cards beside the body, for the record's facts. An item is a widget definition, or the id of a widget declared on the page. The column stacks under the body on a narrow page.",
|
|
@@ -5290,7 +5328,7 @@
|
|
|
5290
5328
|
},
|
|
5291
5329
|
"pageFilters": {
|
|
5292
5330
|
"type": "array",
|
|
5293
|
-
"description": "type='dashboard': page-level filter controls in
|
|
5331
|
+
"description": "type='dashboard' and type='detail': page-level filter controls, in a dashboard's header and in a detail page's band (detail-page-filters, 2.141.0). Each selection is written into the page's workspace context, so a widget reads it through a `@page.<key>` / `@workspace.<key>` token (an endpoint widget also in its `responsePath`). Maps to the CnDashboardPage `pageFilters` prop.",
|
|
5294
5332
|
"items": {
|
|
5295
5333
|
"type": "object",
|
|
5296
5334
|
"required": ["key"],
|
package/src/utils/actionLink.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { generateUrl, getRootUrl } from '@nextcloud/router'
|
|
19
|
+
import { interpolateUrlTokens } from './interpolateUrlTokens.js'
|
|
19
20
|
import { followLinkClick } from './linkNavigation.js'
|
|
20
21
|
|
|
21
22
|
/** An absolute URL a manifest link may name, matching the schema's lowercase pattern. */
|
|
@@ -80,16 +81,21 @@ export function resolveManifestHref(href) {
|
|
|
80
81
|
* function `handler` is dropped like any other, with a one-time warning.
|
|
81
82
|
*
|
|
82
83
|
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
84
|
+
* With `tokenCtx` the `href` first resolves its `@object.<field>`,
|
|
85
|
+
* `@objectId`, `@page.<key>` and `@config.<key>` tokens, so a detail page can
|
|
86
|
+
* link to a URL built from its record (PtPortal "Website bekijken").
|
|
87
|
+
*
|
|
83
88
|
* @param {object} entry The manifest header action.
|
|
89
|
+
* @param {object|null} [tokenCtx] The token context (`{ objectId?, object?, workspace?, config? }`).
|
|
84
90
|
* @return {object} The entry, or a copy in link form.
|
|
85
91
|
*/
|
|
86
|
-
export function headerActionAsLink(entry) {
|
|
92
|
+
export function headerActionAsLink(entry, tokenCtx = null) {
|
|
87
93
|
if (!entry || typeof entry !== 'object') {
|
|
88
94
|
return entry
|
|
89
95
|
}
|
|
90
96
|
let resolved = entry
|
|
91
97
|
if (Array.isArray(entry.children)) {
|
|
92
|
-
const children = entry.children.map(headerActionAsLink)
|
|
98
|
+
const children = entry.children.map((child) => headerActionAsLink(child, tokenCtx))
|
|
93
99
|
if (children.some((child, index) => child !== entry.children[index])) {
|
|
94
100
|
resolved = { ...entry, children }
|
|
95
101
|
}
|
|
@@ -99,7 +105,7 @@ export function headerActionAsLink(entry) {
|
|
|
99
105
|
}
|
|
100
106
|
const { href: declared, ...withoutHref } = resolved
|
|
101
107
|
const { handler: _handler, ...rest } = withoutHref
|
|
102
|
-
const href = resolveManifestHref(declared)
|
|
108
|
+
const href = resolveManifestHref(tokenCtx ? interpolateUrlTokens(declared, tokenCtx) : declared)
|
|
103
109
|
if (!href) {
|
|
104
110
|
if (!warnedHrefs.has(declared)) {
|
|
105
111
|
warnedHrefs.add(declared)
|