@conduction/nextcloud-vue 2.68.0 → 2.70.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/CnActionsBar/CnActionsBar.vue2.js +25 -0
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +155 -152
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +77 -0
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +8 -8
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue.js +1 -0
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js +28 -0
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +4 -2
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
- package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js +25 -1
- package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue3.js +32 -17
- package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCapabilityTable/CnCapabilityTable.vue2.js +14 -1
- package/dist/esm/components/CnCapabilityTable/CnCapabilityTable.vue2.js.map +1 -1
- package/dist/esm/components/CnCapabilityTable/CnCapabilityTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +43 -2
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +3 -3
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +45 -35
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +19 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +35 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +94 -6
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +64 -19
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +28 -0
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +3 -2
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +12 -0
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +9 -0
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +54 -7
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +26 -8
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +143 -0
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +68 -25
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js.map +1 -1
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +4 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +17 -3
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +25 -24
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +29 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +7 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js +24 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js +1 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +81 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +25 -3
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +15 -2
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js +31 -32
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +3 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +3 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/mixins/pageViews.js +278 -11
- package/dist/esm/mixins/pageViews.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +145 -0
- package/dist/esm/store/plugins/dashboardLayouts.js +35 -1
- package/dist/esm/store/plugins/dashboardLayouts.js.map +1 -1
- package/dist/esm/utils/capabilityComparison.js +29 -1
- package/dist/esm/utils/capabilityComparison.js.map +1 -1
- package/dist/esm/utils/formatMetric.js +137 -16
- package/dist/esm/utils/formatMetric.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 +2840 -1477
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +145 -0
- package/l10n/en.json +3 -0
- package/l10n/nl.json +3 -0
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.md +2 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +28 -2
- package/src/components/CnAppNav/CnAppNav.vue +85 -8
- package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +45 -0
- package/src/components/CnCalendarWidget/CnCalendarWidget.vue +32 -4
- package/src/components/CnCapabilityTable/CnCapabilityTable.vue +14 -0
- package/src/components/CnDashboardGrid/CnDashboardGrid.md +2 -0
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +54 -2
- package/src/components/CnDashboardPage/CnDashboardPage.md +2 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +59 -35
- package/src/components/CnDataTable/CnDataTable.vue +35 -1
- package/src/components/CnDetailPage/CnDetailPage.md +8 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +124 -8
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +30 -1
- package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +12 -0
- package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +9 -0
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +95 -8
- package/src/components/CnIndexPage/CnIndexPage.vue +171 -6
- package/src/components/CnNextStepCard/CnNextStepCard.vue +11 -0
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +4 -1
- package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +34 -7
- package/src/components/CnStagesWidget/CnStagesWidget.vue +19 -0
- package/src/components/CnStatWidget/CnStatWidget.vue +38 -2
- package/src/components/CnStatsBlock/CnStatsBlock.md +2 -0
- package/src/components/CnStatsBlock/CnStatsBlock.vue +25 -2
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +95 -3
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +24 -3
- package/src/css/detail-page.css +10 -0
- package/src/css/index-page.css +12 -0
- package/src/css/kpi-card.css +15 -0
- package/src/mixins/pageViews.js +277 -10
- package/src/schemas/app-manifest-v2.schema.json +73 -6
- package/src/store/plugins/dashboardLayouts.js +34 -0
- package/src/utils/capabilityComparison.js +28 -0
- package/src/utils/formatMetric.js +143 -15
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -1330,6 +1330,21 @@ a.cn-kpi-card--stacked:focus-visible {
|
|
|
1330
1330
|
text-decoration: none !important;
|
|
1331
1331
|
}
|
|
1332
1332
|
|
|
1333
|
+
/* A stacked stats block (CnStatsBlock `layout: "stacked"`): its unit
|
|
1334
|
+
("decisions") goes on a muted line of its own under the number, where the
|
|
1335
|
+
stat tile puts its caption (zuiddrecht-pixel-gaps-3). */
|
|
1336
|
+
.cn-kpi-card--stacked .cn-stats-block__count {
|
|
1337
|
+
flex-direction: column;
|
|
1338
|
+
align-items: flex-start;
|
|
1339
|
+
gap: var(--cn-kpi-stacked-gap, 6px);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
.cn-kpi-card--stacked .cn-stats-block__count-label {
|
|
1343
|
+
font-size: var(--cn-kpi-label-size, 13px);
|
|
1344
|
+
font-weight: 500;
|
|
1345
|
+
color: var(--color-text-maxcontrast);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1333
1348
|
/* Filled: the card draws its own grey box, for a KPI that is NOT already
|
|
1334
1349
|
inside a wrapper that draws one. Opt-in via `content.flat: false`. */
|
|
1335
1350
|
.cn-kpi-card--filled {
|
|
@@ -2283,6 +2298,18 @@ a.cn-kpi-card--stacked:focus-visible {
|
|
|
2283
2298
|
padding: calc(5 * var(--default-grid-baseline));
|
|
2284
2299
|
}
|
|
2285
2300
|
|
|
2301
|
+
/* The board's header buttons (`headerButtons`) at the end of the title row,
|
|
2302
|
+
centred on the title and its count line. They wrap under the title on a
|
|
2303
|
+
narrow page. */
|
|
2304
|
+
.cn-index-page__header-buttons {
|
|
2305
|
+
display: flex;
|
|
2306
|
+
flex-wrap: wrap;
|
|
2307
|
+
align-items: center;
|
|
2308
|
+
align-self: center;
|
|
2309
|
+
gap: calc(2 * var(--default-grid-baseline));
|
|
2310
|
+
margin-inline-start: auto;
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2286
2313
|
.cn-index-page__below-header {
|
|
2287
2314
|
margin-bottom: calc(4 * var(--default-grid-baseline));
|
|
2288
2315
|
}
|
|
@@ -2862,6 +2889,16 @@ a.cn-kpi-card--stacked:focus-visible {
|
|
|
2862
2889
|
row-gap: var(--cn-detail-header-row-gap, 22px);
|
|
2863
2890
|
}
|
|
2864
2891
|
|
|
2892
|
+
/* That wrap must not take the actions off the title row. With `flex-basis:
|
|
2893
|
+
auto` a long title claimed its whole text width, so the actions wrapped
|
|
2894
|
+
under it. From a zero basis the title takes what the actions leave and
|
|
2895
|
+
wraps (two lines, then an ellipsis) inside it; only on a page too narrow
|
|
2896
|
+
for 16rem of title beside the actions do the actions drop a row. */
|
|
2897
|
+
.cn-detail-page__header--with-widget > :first-child {
|
|
2898
|
+
flex: 1 1 0;
|
|
2899
|
+
min-width: min(100%, 16rem);
|
|
2900
|
+
}
|
|
2901
|
+
|
|
2865
2902
|
.cn-detail-page__header-widget {
|
|
2866
2903
|
flex: 1 0 100%;
|
|
2867
2904
|
min-width: 0;
|
|
@@ -6173,6 +6210,17 @@ svg.leaflet-image-layer.leaflet-interactive path {
|
|
|
6173
6210
|
margin: 0;
|
|
6174
6211
|
text-transform: uppercase;
|
|
6175
6212
|
}
|
|
6213
|
+
|
|
6214
|
+
/* The title is a small kicker, not a section heading, but it is an <h3> (or
|
|
6215
|
+
whatever `titleTag` says) for the outline, and a theme that sizes every
|
|
6216
|
+
heading with !important (thematiq's nldesign sheet: `h3 { font-size:
|
|
6217
|
+
var(--nldesign-component-heading-3-font-size) !important }`) blew it up to
|
|
6218
|
+
an 18px uppercase heading. Only !important beats !important; the doubled
|
|
6219
|
+
class makes this the more specific rule. Without such a theme the size is
|
|
6220
|
+
the 0.85em above, unchanged. */
|
|
6221
|
+
.cn-next-step-card__title.cn-next-step-card__title[data-v-3a9ce962] {
|
|
6222
|
+
font-size: 0.85em !important;
|
|
6223
|
+
}
|
|
6176
6224
|
.cn-next-step-card__list[data-v-3a9ce962] {
|
|
6177
6225
|
display: grid;
|
|
6178
6226
|
gap: calc(2 * var(--default-grid-baseline));
|
|
@@ -16465,6 +16513,14 @@ a.cn-xwiki-card__title[data-v-54e85fe3]:hover {
|
|
|
16465
16513
|
gap: 16px;
|
|
16466
16514
|
width: 100%;
|
|
16467
16515
|
}
|
|
16516
|
+
|
|
16517
|
+
/* `content.inset: true`: the board's inset inside the card (zuiddrecht-pixel-
|
|
16518
|
+
gaps-3). A dashboard renders its widgets flush, so without it the strip ran
|
|
16519
|
+
from card edge to card edge. Theme hook: --cn-widget-board-inset. */
|
|
16520
|
+
.cn-stacked-bar--inset[data-v-c0bac1a2] {
|
|
16521
|
+
box-sizing: border-box;
|
|
16522
|
+
padding: var(--cn-widget-board-inset, 16px 24px 22px);
|
|
16523
|
+
}
|
|
16468
16524
|
.cn-stacked-bar__state[data-v-c0bac1a2] {
|
|
16469
16525
|
display: flex;
|
|
16470
16526
|
align-items: center;
|
|
@@ -16980,6 +17036,25 @@ button.cn-stages-widget__bar-label[data-v-358fde0f] {
|
|
|
16980
17036
|
line-height: inherit;
|
|
16981
17037
|
cursor: pointer;
|
|
16982
17038
|
}
|
|
17039
|
+
|
|
17040
|
+
/* The same label under a theme that restyles every button with !important
|
|
17041
|
+
(thematiq's nldesign sheet: `button:not(.action-button)` gets 8px 16px
|
|
17042
|
+
padding, `min-width: max-content` and `overflow: visible`), and under core's
|
|
17043
|
+
`margin: 3px 3px 3px 0`, which outranks the scoped base rule. Together they
|
|
17044
|
+
put each clickable label about 10px below the current step's <span> and let
|
|
17045
|
+
a long label run over its neighbours, so eight Woo steps overlapped instead
|
|
17046
|
+
of ending in an ellipsis. Only !important beats !important; this selector is
|
|
17047
|
+
the more specific of the two. Without such a theme nothing changes: these
|
|
17048
|
+
are the values the base rule already sets. */
|
|
17049
|
+
button.cn-stages-widget__bar-label.cn-stages-widget__bar-label[data-v-358fde0f] {
|
|
17050
|
+
min-width: 0 !important;
|
|
17051
|
+
margin: 0 !important;
|
|
17052
|
+
padding: 0 !important;
|
|
17053
|
+
border-width: 0 !important;
|
|
17054
|
+
overflow: hidden !important;
|
|
17055
|
+
text-overflow: ellipsis !important;
|
|
17056
|
+
white-space: nowrap !important;
|
|
17057
|
+
}
|
|
16983
17058
|
button.cn-stages-widget__bar-label[aria-disabled="true"][data-v-358fde0f] {
|
|
16984
17059
|
cursor: not-allowed;
|
|
16985
17060
|
}
|
|
@@ -17459,6 +17534,21 @@ a.cn-kpi-card--stacked:focus-visible {
|
|
|
17459
17534
|
text-decoration: none !important;
|
|
17460
17535
|
}
|
|
17461
17536
|
|
|
17537
|
+
/* A stacked stats block (CnStatsBlock `layout: "stacked"`): its unit
|
|
17538
|
+
("decisions") goes on a muted line of its own under the number, where the
|
|
17539
|
+
stat tile puts its caption (zuiddrecht-pixel-gaps-3). */
|
|
17540
|
+
.cn-kpi-card--stacked .cn-stats-block__count {
|
|
17541
|
+
flex-direction: column;
|
|
17542
|
+
align-items: flex-start;
|
|
17543
|
+
gap: var(--cn-kpi-stacked-gap, 6px);
|
|
17544
|
+
}
|
|
17545
|
+
|
|
17546
|
+
.cn-kpi-card--stacked .cn-stats-block__count-label {
|
|
17547
|
+
font-size: var(--cn-kpi-label-size, 13px);
|
|
17548
|
+
font-weight: 500;
|
|
17549
|
+
color: var(--color-text-maxcontrast);
|
|
17550
|
+
}
|
|
17551
|
+
|
|
17462
17552
|
/* Filled: the card draws its own grey box, for a KPI that is NOT already
|
|
17463
17553
|
inside a wrapper that draws one. Opt-in via `content.flat: false`. */
|
|
17464
17554
|
.cn-kpi-card--filled {
|
|
@@ -18405,6 +18495,30 @@ to { background-position: -200% 0;
|
|
|
18405
18495
|
flex: 1 1 320px;
|
|
18406
18496
|
width: auto;
|
|
18407
18497
|
}
|
|
18498
|
+
|
|
18499
|
+
/* The emblem beside the greeting (`content.emblem`): on one row with the
|
|
18500
|
+
date line and heading, centred on them, as LpStart draws it. */
|
|
18501
|
+
.cn-header-widget--with-emblem[data-v-1b97430f] {
|
|
18502
|
+
display: flex;
|
|
18503
|
+
column-gap: var(--cn-header-emblem-gap, 16px);
|
|
18504
|
+
}
|
|
18505
|
+
.cn-header-widget--with-emblem .cn-header-widget__content[data-v-1b97430f] {
|
|
18506
|
+
flex: 1 1 auto;
|
|
18507
|
+
width: auto;
|
|
18508
|
+
min-width: 0;
|
|
18509
|
+
}
|
|
18510
|
+
.cn-header-widget__emblem[data-v-1b97430f] {
|
|
18511
|
+
position: relative;
|
|
18512
|
+
z-index: 1;
|
|
18513
|
+
flex: none;
|
|
18514
|
+
align-self: center;
|
|
18515
|
+
height: var(--cn-header-emblem-size, 52px);
|
|
18516
|
+
width: auto;
|
|
18517
|
+
}
|
|
18518
|
+
.cn-header-widget__emblem--theme[data-v-1b97430f] {
|
|
18519
|
+
width: var(--cn-header-emblem-size, 52px);
|
|
18520
|
+
background: var(--nldesign-emblem-url) center / contain no-repeat;
|
|
18521
|
+
}
|
|
18408
18522
|
.cn-header-widget__views[data-v-1b97430f] {
|
|
18409
18523
|
position: relative;
|
|
18410
18524
|
z-index: 1;
|
|
@@ -19507,6 +19621,14 @@ to { background-position: -200% 0;
|
|
|
19507
19621
|
.cn-week-strip[data-v-153dd4cb] {
|
|
19508
19622
|
width: 100%;
|
|
19509
19623
|
}
|
|
19624
|
+
|
|
19625
|
+
/* `content.inset: true`: the board's inset inside the card (zuiddrecht-pixel-
|
|
19626
|
+
gaps-3). A dashboard renders its widgets flush, so without it the strip ran
|
|
19627
|
+
from card edge to card edge. Theme hook: --cn-widget-board-inset. */
|
|
19628
|
+
.cn-week-strip--inset[data-v-153dd4cb] {
|
|
19629
|
+
box-sizing: border-box;
|
|
19630
|
+
padding: var(--cn-widget-board-inset, 16px 24px 22px);
|
|
19631
|
+
}
|
|
19510
19632
|
.cn-week-strip__state[data-v-153dd4cb] {
|
|
19511
19633
|
display: flex;
|
|
19512
19634
|
align-items: center;
|
|
@@ -30052,6 +30174,19 @@ to {
|
|
|
30052
30174
|
}
|
|
30053
30175
|
|
|
30054
30176
|
|
|
30177
|
+
/* `separator`: a text separator in place of the chevron. The chevron's own
|
|
30178
|
+
element stays (it carries NcBreadcrumbs' spacing and its "no separator after
|
|
30179
|
+
the last crumb" rule); only its icon gives way to the text. */
|
|
30180
|
+
.cn-breadcrumbs--text-separator[data-v-296fde9b] .vue-crumb__separator svg {
|
|
30181
|
+
display: none;
|
|
30182
|
+
}
|
|
30183
|
+
.cn-breadcrumbs--text-separator[data-v-296fde9b] .vue-crumb__separator::before {
|
|
30184
|
+
content: var(--cn-breadcrumbs-separator);
|
|
30185
|
+
padding-inline: var(--cn-breadcrumbs-separator-gap, 0);
|
|
30186
|
+
color: var(--color-text-maxcontrast);
|
|
30187
|
+
}
|
|
30188
|
+
|
|
30189
|
+
|
|
30055
30190
|
.cn-run-node-dialog__form[data-v-3eb7c55d] {
|
|
30056
30191
|
display: flex;
|
|
30057
30192
|
flex-direction: column;
|
|
@@ -30223,6 +30358,16 @@ to {
|
|
|
30223
30358
|
outline-offset: 2px;
|
|
30224
30359
|
border-radius: var(--border-radius-container-large, 16px);
|
|
30225
30360
|
}
|
|
30361
|
+
|
|
30362
|
+
/* A size-to-content cell (`sizeToContent`) is measured by GridStack from the
|
|
30363
|
+
content's first child, and a widget wrapper there filled the cell
|
|
30364
|
+
(`height: 100%`), so the measure returned the cell's own height and the
|
|
30365
|
+
cell never shrank: "Waiting for me" stayed a tall card around one row. In
|
|
30366
|
+
such a cell the child takes its own height. A cell without the key keeps
|
|
30367
|
+
the filling child. */
|
|
30368
|
+
.cn-dashboard-grid__item--fit[data-v-9f6b1a22] > .grid-stack-item-content > * {
|
|
30369
|
+
height: auto;
|
|
30370
|
+
}
|
|
30226
30371
|
[data-v-9f6b1a22] .grid-stack-item-content {
|
|
30227
30372
|
background: var(--color-main-background);
|
|
30228
30373
|
border: none;
|
|
@@ -154,6 +154,40 @@ function mergeUserLayout(manifestLayout, record) {
|
|
|
154
154
|
return [...arranged, ...appended]
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* The three calls a user layout needs: from a store that carries the
|
|
159
|
+
* `dashboardLayoutsPlugin` actions, or straight over user preferences.
|
|
160
|
+
*
|
|
161
|
+
* Both paths write through `writeUserPreference`, so a record lands on the
|
|
162
|
+
* server and in the browser mirror whichever one a page uses. CnDashboardPage
|
|
163
|
+
* reads its own grid's record through this, and the page views mixin reads
|
|
164
|
+
* each view's record through it, so the two cannot disagree about where a
|
|
165
|
+
* user's arrangement lives.
|
|
166
|
+
*
|
|
167
|
+
* @param {object|null} [store] A store with `loadDashboardLayout`, `saveDashboardLayout` and `resetDashboardLayout`, or null.
|
|
168
|
+
* @return {{ load: (appId: string, pageId: string) => Promise<object|null>, save: (appId: string, pageId: string, layout: Array<object>) => Promise<boolean>, reset: (appId: string, pageId: string) => Promise<boolean> }} The api.
|
|
169
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
|
|
170
|
+
*/
|
|
171
|
+
function resolveUserLayoutApi(store = null) {
|
|
172
|
+
if (store && typeof store.loadDashboardLayout === 'function') {
|
|
173
|
+
return {
|
|
174
|
+
load: (...a) => store.loadDashboardLayout(...a),
|
|
175
|
+
save: (...a) => store.saveDashboardLayout(...a),
|
|
176
|
+
reset: (...a) => store.resetDashboardLayout(...a),
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
load: (appId, pageId) => readUserPreference(appId, dashboardLayoutKey(pageId), null),
|
|
182
|
+
save: (appId, pageId, layout) => writeUserPreference(
|
|
183
|
+
appId,
|
|
184
|
+
dashboardLayoutKey(pageId),
|
|
185
|
+
{ items: (Array.isArray(layout) ? layout : []).map(geometryOf).filter((entry) => entry.widgetId !== '') },
|
|
186
|
+
),
|
|
187
|
+
reset: (appId, pageId) => writeUserPreference(appId, dashboardLayoutKey(pageId), { items: [] }),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
157
191
|
/**
|
|
158
192
|
* Plugin definition.
|
|
159
193
|
*
|
|
@@ -259,5 +293,5 @@ function dashboardLayoutsPlugin(options = {}) {
|
|
|
259
293
|
}
|
|
260
294
|
}
|
|
261
295
|
|
|
262
|
-
export { LAYOUT_GEOMETRY_FIELDS, dashboardLayoutKey, dashboardLayoutsPlugin, mergeUserLayout };
|
|
296
|
+
export { LAYOUT_GEOMETRY_FIELDS, dashboardLayoutKey, dashboardLayoutsPlugin, mergeUserLayout, resolveUserLayoutApi };
|
|
263
297
|
//# sourceMappingURL=dashboardLayouts.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboardLayouts.js","sources":["../../../../src/store/plugins/dashboardLayouts.js"],"sourcesContent":["/**\n * Per-user dashboard layouts for the object store.\n *\n * A manifest dashboard is the same page for everybody. This plugin lets one\n * user keep their own arrangement of it without changing what anybody else\n * sees: the manifest layout stays the base and the reset target, and the\n * user's record is a thin overlay of geometry on top of it.\n *\n * 🔴 THE MANIFEST WINS ON MEMBERSHIP, THE USER WINS ON GEOMETRY. A widget the\n * admin removes disappears for everyone, stored record or not; a widget the\n * admin adds appears for everyone, at the end of the user's grid. What the\n * user owns is where their widgets sit, not which widgets exist. Reading it\n * the other way round would let a stored record resurrect a widget the admin\n * deliberately took off the page, and nothing on screen would say why it was\n * back.\n *\n * 🔴 IT WRITES THROUGH `writeUserPreference`, NOT A SECOND HTTP PATH. That\n * helper already addresses the app's preference route, mirrors to the browser\n * so a layout survives an instance without the endpoint, and refuses an SPA\n * shell answering 200 with HTML. A second client here would be a second answer\n * about where a user's settings live.\n *\n * State added:\n * - `dashboardLayouts` — stored records by page id\n *\n * Actions added:\n * - `loadDashboardLayout(appId, pageId)` — read this user's record\n * - `saveDashboardLayout(appId, pageId, layout)` — store the geometry\n * - `resetDashboardLayout(appId, pageId)` — drop the record\n *\n * @example\n * import { createObjectStore, dashboardLayoutsPlugin } from '@conduction/nextcloud-vue'\n *\n * const useMyStore = createObjectStore('myapp', {\n * plugins: [dashboardLayoutsPlugin()],\n * })\n *\n * const store = useMyStore()\n * await store.loadDashboardLayout('dossiq', 'Dashboard')\n * await store.saveDashboardLayout('dossiq', 'Dashboard', grid)\n *\n * @return {object} Plugin definition\n */\n\nimport {\n\treadUserPreference,\n\twriteUserPreference,\n} from '../../composables/useUserPreferences.js'\n\n/**\n * The preference key one page's layout is stored under.\n *\n * Keyed by page so two dashboards in one app do not overwrite each other. The\n * prefix is spelled once here because a read and a write that disagree about\n * the key read as a layout that never persists.\n *\n * @param {string} pageId The manifest page id.\n * @return {string} The preference key.\n */\nexport function dashboardLayoutKey(pageId) {\n\treturn 'dashboard-layout.' + String(pageId || '')\n}\n\n/**\n * The geometry fields a stored record keeps.\n *\n * Only these five travel. A record that carried the whole layout item would\n * carry the widget's title, its style and its config, and the next manifest\n * change would be silently overridden by a copy the user never edited.\n *\n * @type {Array<string>}\n */\nexport const LAYOUT_GEOMETRY_FIELDS = ['gridX', 'gridY', 'gridWidth', 'gridHeight']\n\n/**\n * One layout item reduced to what a user record stores.\n *\n * @param {object} item The layout item.\n * @return {object} The stored entry.\n */\nfunction geometryOf(item) {\n\tconst entry = { widgetId: String(item?.widgetId ?? '') }\n\tfor (const field of LAYOUT_GEOMETRY_FIELDS) {\n\t\tif (item?.[field] !== undefined) {\n\t\t\tentry[field] = item[field]\n\t\t}\n\t}\n\n\treturn entry\n}\n\n/**\n * Merge a user's stored record over the manifest layout.\n *\n * The rule, in the order it is applied:\n *\n * 1. The manifest layout is the base. Every widget it declares is present and\n * every widget it does not declare is absent, whatever the record says.\n * 2. A widget with a stored entry takes that entry's geometry.\n * 3. A widget with no stored entry keeps its manifest position, so a page that\n * gains a widget shows it rather than hiding it until the user next edits.\n * 4. The user's own order is kept for the widgets they arranged, and widgets\n * the admin added since are appended after them.\n *\n * Pure on purpose: this is the rule the whole change turns on, and a rule that\n * can only be exercised by mounting a grid is a rule nobody tests at the edges.\n *\n * @param {Array<object>} manifestLayout The layout the manifest ships.\n * @param {object|Array<object>|null} record The user's stored record, or its items.\n * @return {Array<object>} The merged layout.\n */\nexport function mergeUserLayout(manifestLayout, record) {\n\tconst base = Array.isArray(manifestLayout) ? manifestLayout : []\n\tconst items = Array.isArray(record) ? record : (record?.items || [])\n\tif (!Array.isArray(items) || items.length === 0) {\n\t\treturn [...base]\n\t}\n\n\tconst stored = new Map()\n\tfor (const entry of items) {\n\t\tconst widgetId = String(entry?.widgetId ?? '')\n\t\tif (widgetId !== '') {\n\t\t\tstored.set(widgetId, entry)\n\t\t}\n\t}\n\n\tconst arranged = []\n\tconst appended = []\n\n\tfor (const item of base) {\n\t\tconst widgetId = String(item?.widgetId ?? '')\n\t\tconst entry = stored.get(widgetId)\n\t\tif (!entry) {\n\t\t\t// A widget the admin added since the user last arranged the page.\n\t\t\t// It goes after what they arranged rather than into the middle of\n\t\t\t// it, where it would push their own grid around.\n\t\t\tappended.push({ ...item })\n\t\t\tcontinue\n\t\t}\n\n\t\tconst merged = { ...item }\n\t\tfor (const field of LAYOUT_GEOMETRY_FIELDS) {\n\t\t\tif (entry[field] !== undefined) {\n\t\t\t\tmerged[field] = entry[field]\n\t\t\t}\n\t\t}\n\t\tarranged.push(merged)\n\t}\n\n\t// The user's own order, for the widgets they arranged.\n\tarranged.sort((a, b) => {\n\t\tconst order = [...stored.keys()]\n\t\treturn order.indexOf(String(a.widgetId)) - order.indexOf(String(b.widgetId))\n\t})\n\n\treturn [...arranged, ...appended]\n}\n\n/**\n * Plugin definition.\n *\n * @param {object} [options] Options.\n * @param {object} [options.http] An axios-shaped client, for tests.\n * @param {object} [options.storage] A localStorage-shaped store, for tests.\n * @return {object} Plugin definition\n */\nexport function dashboardLayoutsPlugin(options = {}) {\n\treturn {\n\t\tname: 'dashboardLayouts',\n\n\t\tstate: () => ({\n\t\t\t/**\n\t\t\t * Stored layout records by page id.\n\t\t\t *\n\t\t\t * @type {Record<string, object|null>}\n\t\t\t */\n\t\t\tdashboardLayouts: {},\n\t\t}),\n\n\t\tactions: {\n\t\t\t/**\n\t\t\t * Read this user's stored layout for one page.\n\t\t\t *\n\t\t\t * Answers null when there is none, which is what a page with no\n\t\t\t * user record must show: the manifest layout, untouched. An empty\n\t\t\t * array would read the same to a caller that only checks length,\n\t\t\t * so null is returned and the difference is stated.\n\t\t\t *\n\t\t\t * @param {string} appId The Nextcloud app id.\n\t\t\t * @param {string} pageId The manifest page id.\n\t\t\t * @return {Promise<object|null>} The record, or null.\n\t\t\t */\n\t\t\tasync loadDashboardLayout(appId, pageId) {\n\t\t\t\tconst record = await readUserPreference(\n\t\t\t\t\tappId,\n\t\t\t\t\tdashboardLayoutKey(pageId),\n\t\t\t\t\tnull,\n\t\t\t\t\toptions,\n\t\t\t\t)\n\n\t\t\t\tconst items = Array.isArray(record) ? record : record?.items\n\t\t\t\tconst stored = Array.isArray(items) && items.length > 0\n\t\t\t\t\t? { items }\n\t\t\t\t\t: null\n\n\t\t\t\tthis.dashboardLayouts = { ...this.dashboardLayouts, [pageId]: stored }\n\n\t\t\t\treturn stored\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Store this user's geometry for one page.\n\t\t\t *\n\t\t\t * Only the geometry is stored, never the whole layout item: a\n\t\t\t * record carrying a widget's title and config would override the\n\t\t\t * next manifest change with a copy the user never edited.\n\t\t\t *\n\t\t\t * @param {string} appId The Nextcloud app id.\n\t\t\t * @param {string} pageId The manifest page id.\n\t\t\t * @param {Array<object>} layout The layout as it now stands.\n\t\t\t * @return {Promise<boolean>} Whether it reached the server.\n\t\t\t */\n\t\t\tasync saveDashboardLayout(appId, pageId, layout) {\n\t\t\t\tconst items = (Array.isArray(layout) ? layout : [])\n\t\t\t\t\t.map(geometryOf)\n\t\t\t\t\t.filter((entry) => entry.widgetId !== '')\n\n\t\t\t\tthis.dashboardLayouts = { ...this.dashboardLayouts, [pageId]: { items } }\n\n\t\t\t\treturn await writeUserPreference(\n\t\t\t\t\tappId,\n\t\t\t\t\tdashboardLayoutKey(pageId),\n\t\t\t\t\t{ items },\n\t\t\t\t\toptions,\n\t\t\t\t)\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Drop this user's record, returning the page to the manifest.\n\t\t\t *\n\t\t\t * An empty record is written rather than a delete, because the\n\t\t\t * preference route answers one verb for a value and a written\n\t\t\t * empty reads back as \"no arrangement\" on every instance,\n\t\t\t * including one whose endpoint has no delete at all.\n\t\t\t *\n\t\t\t * @param {string} appId The Nextcloud app id.\n\t\t\t * @param {string} pageId The manifest page id.\n\t\t\t * @return {Promise<boolean>} Whether it reached the server.\n\t\t\t */\n\t\t\tasync resetDashboardLayout(appId, pageId) {\n\t\t\t\tthis.dashboardLayouts = { ...this.dashboardLayouts, [pageId]: null }\n\n\t\t\t\treturn await writeUserPreference(\n\t\t\t\t\tappId,\n\t\t\t\t\tdashboardLayoutKey(pageId),\n\t\t\t\t\t{ items: [] },\n\t\t\t\t\toptions,\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t}\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dashboardLayouts.js","sources":["../../../../src/store/plugins/dashboardLayouts.js"],"sourcesContent":["/**\n * Per-user dashboard layouts for the object store.\n *\n * A manifest dashboard is the same page for everybody. This plugin lets one\n * user keep their own arrangement of it without changing what anybody else\n * sees: the manifest layout stays the base and the reset target, and the\n * user's record is a thin overlay of geometry on top of it.\n *\n * 🔴 THE MANIFEST WINS ON MEMBERSHIP, THE USER WINS ON GEOMETRY. A widget the\n * admin removes disappears for everyone, stored record or not; a widget the\n * admin adds appears for everyone, at the end of the user's grid. What the\n * user owns is where their widgets sit, not which widgets exist. Reading it\n * the other way round would let a stored record resurrect a widget the admin\n * deliberately took off the page, and nothing on screen would say why it was\n * back.\n *\n * 🔴 IT WRITES THROUGH `writeUserPreference`, NOT A SECOND HTTP PATH. That\n * helper already addresses the app's preference route, mirrors to the browser\n * so a layout survives an instance without the endpoint, and refuses an SPA\n * shell answering 200 with HTML. A second client here would be a second answer\n * about where a user's settings live.\n *\n * State added:\n * - `dashboardLayouts` — stored records by page id\n *\n * Actions added:\n * - `loadDashboardLayout(appId, pageId)` — read this user's record\n * - `saveDashboardLayout(appId, pageId, layout)` — store the geometry\n * - `resetDashboardLayout(appId, pageId)` — drop the record\n *\n * @example\n * import { createObjectStore, dashboardLayoutsPlugin } from '@conduction/nextcloud-vue'\n *\n * const useMyStore = createObjectStore('myapp', {\n * plugins: [dashboardLayoutsPlugin()],\n * })\n *\n * const store = useMyStore()\n * await store.loadDashboardLayout('dossiq', 'Dashboard')\n * await store.saveDashboardLayout('dossiq', 'Dashboard', grid)\n *\n * @return {object} Plugin definition\n */\n\nimport {\n\treadUserPreference,\n\twriteUserPreference,\n} from '../../composables/useUserPreferences.js'\n\n/**\n * The preference key one page's layout is stored under.\n *\n * Keyed by page so two dashboards in one app do not overwrite each other. The\n * prefix is spelled once here because a read and a write that disagree about\n * the key read as a layout that never persists.\n *\n * @param {string} pageId The manifest page id.\n * @return {string} The preference key.\n */\nexport function dashboardLayoutKey(pageId) {\n\treturn 'dashboard-layout.' + String(pageId || '')\n}\n\n/**\n * The geometry fields a stored record keeps.\n *\n * Only these five travel. A record that carried the whole layout item would\n * carry the widget's title, its style and its config, and the next manifest\n * change would be silently overridden by a copy the user never edited.\n *\n * @type {Array<string>}\n */\nexport const LAYOUT_GEOMETRY_FIELDS = ['gridX', 'gridY', 'gridWidth', 'gridHeight']\n\n/**\n * One layout item reduced to what a user record stores.\n *\n * @param {object} item The layout item.\n * @return {object} The stored entry.\n */\nfunction geometryOf(item) {\n\tconst entry = { widgetId: String(item?.widgetId ?? '') }\n\tfor (const field of LAYOUT_GEOMETRY_FIELDS) {\n\t\tif (item?.[field] !== undefined) {\n\t\t\tentry[field] = item[field]\n\t\t}\n\t}\n\n\treturn entry\n}\n\n/**\n * Merge a user's stored record over the manifest layout.\n *\n * The rule, in the order it is applied:\n *\n * 1. The manifest layout is the base. Every widget it declares is present and\n * every widget it does not declare is absent, whatever the record says.\n * 2. A widget with a stored entry takes that entry's geometry.\n * 3. A widget with no stored entry keeps its manifest position, so a page that\n * gains a widget shows it rather than hiding it until the user next edits.\n * 4. The user's own order is kept for the widgets they arranged, and widgets\n * the admin added since are appended after them.\n *\n * Pure on purpose: this is the rule the whole change turns on, and a rule that\n * can only be exercised by mounting a grid is a rule nobody tests at the edges.\n *\n * @param {Array<object>} manifestLayout The layout the manifest ships.\n * @param {object|Array<object>|null} record The user's stored record, or its items.\n * @return {Array<object>} The merged layout.\n */\nexport function mergeUserLayout(manifestLayout, record) {\n\tconst base = Array.isArray(manifestLayout) ? manifestLayout : []\n\tconst items = Array.isArray(record) ? record : (record?.items || [])\n\tif (!Array.isArray(items) || items.length === 0) {\n\t\treturn [...base]\n\t}\n\n\tconst stored = new Map()\n\tfor (const entry of items) {\n\t\tconst widgetId = String(entry?.widgetId ?? '')\n\t\tif (widgetId !== '') {\n\t\t\tstored.set(widgetId, entry)\n\t\t}\n\t}\n\n\tconst arranged = []\n\tconst appended = []\n\n\tfor (const item of base) {\n\t\tconst widgetId = String(item?.widgetId ?? '')\n\t\tconst entry = stored.get(widgetId)\n\t\tif (!entry) {\n\t\t\t// A widget the admin added since the user last arranged the page.\n\t\t\t// It goes after what they arranged rather than into the middle of\n\t\t\t// it, where it would push their own grid around.\n\t\t\tappended.push({ ...item })\n\t\t\tcontinue\n\t\t}\n\n\t\tconst merged = { ...item }\n\t\tfor (const field of LAYOUT_GEOMETRY_FIELDS) {\n\t\t\tif (entry[field] !== undefined) {\n\t\t\t\tmerged[field] = entry[field]\n\t\t\t}\n\t\t}\n\t\tarranged.push(merged)\n\t}\n\n\t// The user's own order, for the widgets they arranged.\n\tarranged.sort((a, b) => {\n\t\tconst order = [...stored.keys()]\n\t\treturn order.indexOf(String(a.widgetId)) - order.indexOf(String(b.widgetId))\n\t})\n\n\treturn [...arranged, ...appended]\n}\n\n/**\n * The three calls a user layout needs: from a store that carries the\n * `dashboardLayoutsPlugin` actions, or straight over user preferences.\n *\n * Both paths write through `writeUserPreference`, so a record lands on the\n * server and in the browser mirror whichever one a page uses. CnDashboardPage\n * reads its own grid's record through this, and the page views mixin reads\n * each view's record through it, so the two cannot disagree about where a\n * user's arrangement lives.\n *\n * @param {object|null} [store] A store with `loadDashboardLayout`, `saveDashboardLayout` and `resetDashboardLayout`, or null.\n * @return {{ load: (appId: string, pageId: string) => Promise<object|null>, save: (appId: string, pageId: string, layout: Array<object>) => Promise<boolean>, reset: (appId: string, pageId: string) => Promise<boolean> }} The api.\n * @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves\n */\nexport function resolveUserLayoutApi(store = null) {\n\tif (store && typeof store.loadDashboardLayout === 'function') {\n\t\treturn {\n\t\t\tload: (...a) => store.loadDashboardLayout(...a),\n\t\t\tsave: (...a) => store.saveDashboardLayout(...a),\n\t\t\treset: (...a) => store.resetDashboardLayout(...a),\n\t\t}\n\t}\n\n\treturn {\n\t\tload: (appId, pageId) => readUserPreference(appId, dashboardLayoutKey(pageId), null),\n\t\tsave: (appId, pageId, layout) => writeUserPreference(\n\t\t\tappId,\n\t\t\tdashboardLayoutKey(pageId),\n\t\t\t{ items: (Array.isArray(layout) ? layout : []).map(geometryOf).filter((entry) => entry.widgetId !== '') },\n\t\t),\n\t\treset: (appId, pageId) => writeUserPreference(appId, dashboardLayoutKey(pageId), { items: [] }),\n\t}\n}\n\n/**\n * Plugin definition.\n *\n * @param {object} [options] Options.\n * @param {object} [options.http] An axios-shaped client, for tests.\n * @param {object} [options.storage] A localStorage-shaped store, for tests.\n * @return {object} Plugin definition\n */\nexport function dashboardLayoutsPlugin(options = {}) {\n\treturn {\n\t\tname: 'dashboardLayouts',\n\n\t\tstate: () => ({\n\t\t\t/**\n\t\t\t * Stored layout records by page id.\n\t\t\t *\n\t\t\t * @type {Record<string, object|null>}\n\t\t\t */\n\t\t\tdashboardLayouts: {},\n\t\t}),\n\n\t\tactions: {\n\t\t\t/**\n\t\t\t * Read this user's stored layout for one page.\n\t\t\t *\n\t\t\t * Answers null when there is none, which is what a page with no\n\t\t\t * user record must show: the manifest layout, untouched. An empty\n\t\t\t * array would read the same to a caller that only checks length,\n\t\t\t * so null is returned and the difference is stated.\n\t\t\t *\n\t\t\t * @param {string} appId The Nextcloud app id.\n\t\t\t * @param {string} pageId The manifest page id.\n\t\t\t * @return {Promise<object|null>} The record, or null.\n\t\t\t */\n\t\t\tasync loadDashboardLayout(appId, pageId) {\n\t\t\t\tconst record = await readUserPreference(\n\t\t\t\t\tappId,\n\t\t\t\t\tdashboardLayoutKey(pageId),\n\t\t\t\t\tnull,\n\t\t\t\t\toptions,\n\t\t\t\t)\n\n\t\t\t\tconst items = Array.isArray(record) ? record : record?.items\n\t\t\t\tconst stored = Array.isArray(items) && items.length > 0\n\t\t\t\t\t? { items }\n\t\t\t\t\t: null\n\n\t\t\t\tthis.dashboardLayouts = { ...this.dashboardLayouts, [pageId]: stored }\n\n\t\t\t\treturn stored\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Store this user's geometry for one page.\n\t\t\t *\n\t\t\t * Only the geometry is stored, never the whole layout item: a\n\t\t\t * record carrying a widget's title and config would override the\n\t\t\t * next manifest change with a copy the user never edited.\n\t\t\t *\n\t\t\t * @param {string} appId The Nextcloud app id.\n\t\t\t * @param {string} pageId The manifest page id.\n\t\t\t * @param {Array<object>} layout The layout as it now stands.\n\t\t\t * @return {Promise<boolean>} Whether it reached the server.\n\t\t\t */\n\t\t\tasync saveDashboardLayout(appId, pageId, layout) {\n\t\t\t\tconst items = (Array.isArray(layout) ? layout : [])\n\t\t\t\t\t.map(geometryOf)\n\t\t\t\t\t.filter((entry) => entry.widgetId !== '')\n\n\t\t\t\tthis.dashboardLayouts = { ...this.dashboardLayouts, [pageId]: { items } }\n\n\t\t\t\treturn await writeUserPreference(\n\t\t\t\t\tappId,\n\t\t\t\t\tdashboardLayoutKey(pageId),\n\t\t\t\t\t{ items },\n\t\t\t\t\toptions,\n\t\t\t\t)\n\t\t\t},\n\n\t\t\t/**\n\t\t\t * Drop this user's record, returning the page to the manifest.\n\t\t\t *\n\t\t\t * An empty record is written rather than a delete, because the\n\t\t\t * preference route answers one verb for a value and a written\n\t\t\t * empty reads back as \"no arrangement\" on every instance,\n\t\t\t * including one whose endpoint has no delete at all.\n\t\t\t *\n\t\t\t * @param {string} appId The Nextcloud app id.\n\t\t\t * @param {string} pageId The manifest page id.\n\t\t\t * @return {Promise<boolean>} Whether it reached the server.\n\t\t\t */\n\t\t\tasync resetDashboardLayout(appId, pageId) {\n\t\t\t\tthis.dashboardLayouts = { ...this.dashboardLayouts, [pageId]: null }\n\n\t\t\t\treturn await writeUserPreference(\n\t\t\t\t\tappId,\n\t\t\t\t\tdashboardLayoutKey(pageId),\n\t\t\t\t\t{ items: [] },\n\t\t\t\t\toptions,\n\t\t\t\t)\n\t\t\t},\n\t\t},\n\t}\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -22,6 +22,34 @@
|
|
|
22
22
|
* @module utils/capabilityComparison
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
/*
|
|
26
|
+
* `process.env.NODE_ENV` is substituted by every consuming app's bundler at
|
|
27
|
+
* build time (webpack's DefinePlugin, rollup's replace); see NcSelectTags.js.
|
|
28
|
+
*/
|
|
29
|
+
/* global process */
|
|
30
|
+
|
|
31
|
+
let deprecationWarned = false;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Warn once per page load, in a development build only, that the in-app
|
|
35
|
+
* capability comparison is deprecated. Called by CnFeaturesAndRoadmapPage,
|
|
36
|
+
* CnFeaturesAndRoadmapView and CnCapabilityTable when a comparison is
|
|
37
|
+
* actually given, so an app that passes none hears nothing.
|
|
38
|
+
*
|
|
39
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-the-in-app-capability-comparison-is-deprecated
|
|
40
|
+
* @param {string} source What carried the comparison, for the message.
|
|
41
|
+
* @return {boolean} True when this call printed the warning.
|
|
42
|
+
*/
|
|
43
|
+
function warnCapabilityComparisonDeprecated(source) {
|
|
44
|
+
if (deprecationWarned || process.env.NODE_ENV === 'production') {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
deprecationWarned = true;
|
|
48
|
+
// eslint-disable-next-line no-console -- a deliberate deprecation notice to the developer integrating the component
|
|
49
|
+
console.warn(`[nextcloud-vue] ${source} is deprecated: the in-app capability comparison (capabilityComparison, the features_roadmap_capabilities initial state, CnCapabilityTable) will be removed in a future major version. Publish the comparison on the app's docs site and link to it instead.`);
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
25
53
|
/**
|
|
26
54
|
* Ratings in the order they are counted and rendered.
|
|
27
55
|
*
|
|
@@ -371,5 +399,5 @@ function groupCapabilities(comparison, options = {}) {
|
|
|
371
399
|
return groups
|
|
372
400
|
}
|
|
373
401
|
|
|
374
|
-
export { PROVIDER_KINDS, RATINGS, UNMAPPED_GROUP_KEY, capabilitySearchIndex, defaultGroupMode, filterCapabilities, groupCapabilities, hasProviders, labelFor, matchesQuery, normaliseText, overallTallies, resolveProvider, tally };
|
|
402
|
+
export { PROVIDER_KINDS, RATINGS, UNMAPPED_GROUP_KEY, capabilitySearchIndex, defaultGroupMode, filterCapabilities, groupCapabilities, hasProviders, labelFor, matchesQuery, normaliseText, overallTallies, resolveProvider, tally, warnCapabilityComparisonDeprecated };
|
|
375
403
|
//# sourceMappingURL=capabilityComparison.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capabilityComparison.js","sources":["../../../src/utils/capabilityComparison.js"],"sourcesContent":["/**\n * SPDX-License-Identifier: EUPL-1.2\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n *\n * Presentation logic for the capability comparison rendered by\n * `CnCapabilityTable`. Pure functions over the comparison document the host\n * app supplies, kept out of the SFC so the search predicate, the grouping\n * switch and the provider fallback are unit-testable without a DOM.\n *\n * THE ONE RULE THIS MODULE ENFORCES. A row never disappears. Not for an\n * unknown provider key, not for an area the document forgot to declare, not\n * for a feature nobody has mapped yet, and not for a rating outside the three\n * the audit emits. Every one of those lands in a named bucket with the raw\n * value visible, because a silently dropped row reads exactly like a row that\n * was never written.\n *\n * Tallies are DERIVED here rather than read from the document. A stored total\n * is a second copy of the truth that goes stale the moment a row is corrected,\n * and nothing would fail: the page would show a number that no longer counts\n * its own rows.\n *\n * @module utils/capabilityComparison\n */\n\n/**\n * Ratings in the order they are counted and rendered.\n *\n * @type {ReadonlyArray<string>}\n */\nexport const RATINGS = Object.freeze(['yes', 'partial', 'no'])\n\n/**\n * The columns the totals table and the legend show: the ratings plus\n * `unknown`. `unknown` is a real answer, not a data error. A later audit round\n * can add a capability row without re-reading the products an earlier round\n * rated, and the honest cell for those columns is empty.\n *\n * @type {ReadonlyArray<string>}\n */\nexport const RATING_COLUMNS = Object.freeze([...RATINGS, 'unknown'])\n\n/**\n * The provider kinds the document may declare. Anything else is carried\n * through as `unknown`, which renders as a plain chip rather than a styled\n * one.\n *\n * @type {ReadonlyArray<string>}\n */\nexport const PROVIDER_KINDS = Object.freeze(['self', 'app', 'platform'])\n\n/**\n * Group key for rows that carry no `feature`. It is a bucket, never a filter:\n * the rows render under a heading that says they are not mapped yet.\n *\n * @type {string}\n */\nexport const UNMAPPED_GROUP_KEY = '__cn-unmapped__'\n\n/**\n * Lower-case and strip diacritics so a reader typing `zaken` finds `Zaken` and\n * a reader typing `een` finds `Eén`.\n *\n * `normalize('NFD')` splits a letter from its accent and the range strips the\n * combining marks. Both have been in every browser and in Node since long\n * before this library's floor, so the accent-insensitive half costs one regex.\n *\n * @param {unknown} value Any value; non-strings are coerced.\n * @return {string} The comparable form.\n */\nexport function normaliseText(value) {\n\tif (value === null || value === undefined) {\n\t\treturn ''\n\t}\n\treturn String(value)\n\t\t.normalize('NFD')\n\t\t.replace(/[\\u0300-\\u036f]/g, '')\n\t\t.toLowerCase()\n\t\t.trim()\n}\n\n/**\n * Pick the reader's language variant of a labelled entry.\n *\n * The document carries `name` (English) and `name_nl` (Dutch) side by side.\n * Dutch wins only for a Dutch locale AND only when the Dutch string is\n * actually there, so a half-translated document degrades to English rather\n * than to a blank cell.\n *\n * @param {{name?: string, name_nl?: string}} entry Labelled entry.\n * @param {string} [locale] BCP 47 locale, e.g. `nl`, `nl_NL`, `en-GB`.\n * @return {string} The label to render.\n */\nexport function labelFor(entry, locale = 'en') {\n\tif (!entry) {\n\t\treturn ''\n\t}\n\tconst dutch = String(locale || '').toLowerCase().startsWith('nl')\n\tif (dutch && typeof entry.name_nl === 'string' && entry.name_nl.trim() !== '') {\n\t\treturn entry.name_nl\n\t}\n\treturn entry.name ?? ''\n}\n\n/**\n * Index the document's `providers` list by key.\n *\n * @param {object} comparison Parsed comparison document.\n * @return {Map<string, object>} Provider entries by key.\n */\nfunction providerIndex(comparison) {\n\tconst map = new Map()\n\tfor (const provider of comparison?.providers ?? []) {\n\t\tif (provider && typeof provider.key === 'string') {\n\t\t\tmap.set(provider.key, provider)\n\t\t}\n\t}\n\treturn map\n}\n\n/**\n * Resolve the provider of one capability row.\n *\n * An unrecognised key is NOT an error and never costs the row its place: the\n * raw key becomes the label and the kind becomes `unknown`, so a reader sees\n * that the document names a provider the dictionary forgot rather than seeing\n * nothing at all.\n *\n * @param {object} row One capability row.\n * @param {object} comparison Parsed comparison document.\n * @param {string} [locale] BCP 47 locale used to pick the provider name.\n * @return {{key: string, name: string, kind: string, declared: boolean}|null}\n * The provider, or null when the row names none.\n */\nexport function resolveProvider(row, comparison, locale = 'en') {\n\tconst key = row?.provider\n\tif (typeof key !== 'string' || key.trim() === '') {\n\t\treturn null\n\t}\n\tconst declared = providerIndex(comparison).get(key)\n\tif (!declared) {\n\t\treturn { key, name: key, kind: 'unknown', declared: false }\n\t}\n\tconst kind = PROVIDER_KINDS.includes(declared.kind) ? declared.kind : 'unknown'\n\treturn {\n\t\tkey,\n\t\tname: labelFor(declared, locale) || key,\n\t\tkind,\n\t\tdeclared: true,\n\t}\n}\n\n/**\n * Every string a search should match on for one row, joined.\n *\n * Both languages are indexed whatever the reader's locale, because a Dutch\n * reader searching for the English term they read in a tender document should\n * still find the row.\n *\n * @param {object} row One capability row.\n * @param {object} comparison Parsed comparison document.\n * @return {string} The haystack, already normalised.\n */\nexport function capabilitySearchIndex(row, comparison) {\n\tconst areas = comparison?.areas ?? []\n\tconst features = comparison?.features ?? []\n\tconst area = areas.find((entry) => entry?.key === row?.area)\n\tconst feature = features.find((entry) => entry?.key === row?.feature)\n\tconst provider = resolveProvider(row, comparison)\n\t// The declared entry and not `provider.name`: that one is already resolved\n\t// to a single language, so indexing it would honour the rule above for the\n\t// row, the area and the feature and quietly break it for the provider.\n\tconst declared = providerIndex(comparison).get(provider?.key)\n\tconst parts = [\n\t\trow?.id,\n\t\trow?.name,\n\t\trow?.name_nl,\n\t\trow?.area,\n\t\tarea?.name,\n\t\tarea?.name_nl,\n\t\trow?.feature,\n\t\tfeature?.name,\n\t\tfeature?.name_nl,\n\t\tprovider?.key,\n\t\tprovider?.name,\n\t\tdeclared?.name,\n\t\tdeclared?.name_nl,\n\t]\n\treturn parts.filter((part) => part !== null && part !== undefined && part !== '')\n\t\t.map((part) => normaliseText(part))\n\t\t.join(' ')\n}\n\n/**\n * Does one row match the reader's query?\n *\n * Every whitespace-separated term must appear somewhere in the row's index, so\n * `intake dossiq` narrows rather than widens. An empty query matches\n * everything, which is what keeps the unsearched page identical to the\n * searched-for-nothing page.\n *\n * @param {object} row One capability row.\n * @param {string} query The reader's raw input.\n * @param {object} comparison Parsed comparison document.\n * @return {boolean} True when the row should stay on the page.\n */\nexport function matchesQuery(row, query, comparison) {\n\tconst terms = normaliseText(query).split(/\\s+/).filter(Boolean)\n\tif (terms.length === 0) {\n\t\treturn true\n\t}\n\tconst haystack = capabilitySearchIndex(row, comparison)\n\treturn terms.every((term) => haystack.includes(term))\n}\n\n/**\n * The rows that survive the reader's query, in document order.\n *\n * @param {object} comparison Parsed comparison document.\n * @param {string} [query] The reader's raw input.\n * @return {Array<object>} Matching rows.\n */\nexport function filterCapabilities(comparison, query = '') {\n\treturn (comparison?.capabilities ?? []).filter((row) => matchesQuery(row, query, comparison))\n}\n\n/**\n * Count one system's ratings across a set of capability rows.\n *\n * A rating outside `RATINGS` is counted under `unknown` rather than dropped,\n * so a bad row shows up as a number that does not add up instead of\n * disappearing.\n *\n * @param {Array<object>} capabilities Capability rows.\n * @param {string} systemKey Key of the system column, e.g. `dossiq`.\n * @return {{yes: number, partial: number, no: number, unknown: number, total: number}} The tally.\n */\nexport function tally(capabilities, systemKey) {\n\tconst counts = { yes: 0, partial: 0, no: 0, unknown: 0, total: 0 }\n\tfor (const row of capabilities ?? []) {\n\t\tconst rating = row?.[systemKey]\n\t\tcounts[RATINGS.includes(rating) ? rating : 'unknown'] += 1\n\t\tcounts.total += 1\n\t}\n\treturn counts\n}\n\n/**\n * Tally every system over a set of rows.\n *\n * @param {object} comparison Parsed comparison document.\n * @param {Array<object>} [rows] Rows to count; defaults to all of them.\n * @return {Record<string, object>} Tally per system key.\n */\nexport function overallTallies(comparison, rows = null) {\n\tconst counted = rows ?? comparison?.capabilities ?? []\n\tconst out = {}\n\tfor (const system of comparison?.systems ?? []) {\n\t\tout[system.key] = tally(counted, system.key)\n\t}\n\treturn out\n}\n\n/**\n * Which grouping a document should open on.\n *\n * Feature grouping is the default the moment ANY row carries a `feature`,\n * because a document that bothered to map features wants them shown. A\n * document without them groups by area, which is what every consumer does\n * today.\n *\n * @param {object} comparison Parsed comparison document.\n * @return {'feature'|'area'} The opening mode.\n */\nexport function defaultGroupMode(comparison) {\n\tconst mapped = (comparison?.capabilities ?? []).some((row) => typeof row?.feature === 'string' && row.feature.trim() !== '')\n\treturn mapped ? 'feature' : 'area'\n}\n\n/**\n * Does any row in the document name a provider?\n *\n * Drives whether the provided-by column renders at all. A consumer whose\n * document predates the field gets exactly the columns it has today.\n *\n * @param {object} comparison Parsed comparison document.\n * @return {boolean} True when at least one row names a provider.\n */\nexport function hasProviders(comparison) {\n\treturn (comparison?.capabilities ?? []).some((row) => typeof row?.provider === 'string' && row.provider.trim() !== '')\n}\n\n/**\n * Group the rows for rendering, in the order a reader should meet them.\n *\n * Declared groups come first, in the order the document declares them: for\n * areas that is the audit's own numbering, and re-sorting would put `13.1`\n * above `1.1` on a table whose first column is that number. Then come keys the\n * rows use that the document never declared, labelled with the raw key. Last\n * comes the bucket for rows that name no group at all.\n *\n * An empty group is dropped, because a search that matches nothing in an area\n * should not leave an empty heading behind. The unmapped bucket follows the\n * same rule.\n *\n * @param {object} comparison Parsed comparison document.\n * @param {object} [options] Options.\n * @param {'feature'|'area'} [options.mode] Grouping mode.\n * @param {string} [options.locale] BCP 47 locale used for labels.\n * @param {string} [options.query] The reader's search input.\n * @param {string} [options.unmappedLabel] Heading for rows that name no group.\n * @return {Array<{key: string, label: string, declared: boolean, capabilities: Array<object>, tallies: object}>}\n * One entry per non-empty group.\n */\nexport function groupCapabilities(comparison, options = {}) {\n\tconst mode = options.mode === 'feature' ? 'feature' : 'area'\n\tconst locale = options.locale ?? 'en'\n\tconst unmappedLabel = options.unmappedLabel ?? ''\n\tconst declared = mode === 'feature' ? (comparison?.features ?? []) : (comparison?.areas ?? [])\n\tconst rows = filterCapabilities(comparison, options.query ?? '')\n\n\tconst buckets = new Map()\n\t/**\n\t * Fetch or create a bucket, keeping insertion order.\n\t *\n\t * @param {string} key Bucket key.\n\t * @param {string} label Bucket heading.\n\t * @param {boolean} isDeclared Whether the document declares this key.\n\t * @return {object} The bucket.\n\t */\n\tconst bucket = (key, label, isDeclared) => {\n\t\tif (!buckets.has(key)) {\n\t\t\tbuckets.set(key, { key, label, declared: isDeclared, capabilities: [] })\n\t\t}\n\t\treturn buckets.get(key)\n\t}\n\n\t// Seed the declared groups first so they keep the document's order even\n\t// when the rows arrive in another one.\n\tfor (const entry of declared) {\n\t\tif (entry && typeof entry.key === 'string') {\n\t\t\tbucket(entry.key, labelFor(entry, locale) || entry.key, true)\n\t\t}\n\t}\n\n\tconst unmapped = []\n\tfor (const row of rows) {\n\t\tconst key = row?.[mode]\n\t\tif (typeof key !== 'string' || key.trim() === '') {\n\t\t\tunmapped.push({ ...row, label: labelFor(row, locale) })\n\t\t\tcontinue\n\t\t}\n\t\t// An undeclared key gets a bucket of its own, labelled with the raw\n\t\t// key. Dropping the row here is the silent failure this module exists\n\t\t// to prevent.\n\t\tbucket(key, key, false).capabilities.push({ ...row, label: labelFor(row, locale) })\n\t}\n\n\tconst groups = [...buckets.values()].filter((entry) => entry.capabilities.length > 0)\n\tif (unmapped.length > 0) {\n\t\tgroups.push({\n\t\t\tkey: UNMAPPED_GROUP_KEY,\n\t\t\tlabel: unmappedLabel,\n\t\t\tdeclared: false,\n\t\t\tcapabilities: unmapped,\n\t\t})\n\t}\n\n\tfor (const group of groups) {\n\t\tgroup.tallies = overallTallies(comparison, group.capabilities)\n\t}\n\treturn groups\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"capabilityComparison.js","sources":["../../../src/utils/capabilityComparison.js"],"sourcesContent":["/**\n * SPDX-License-Identifier: EUPL-1.2\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n *\n * Presentation logic for the capability comparison rendered by\n * `CnCapabilityTable`. Pure functions over the comparison document the host\n * app supplies, kept out of the SFC so the search predicate, the grouping\n * switch and the provider fallback are unit-testable without a DOM.\n *\n * THE ONE RULE THIS MODULE ENFORCES. A row never disappears. Not for an\n * unknown provider key, not for an area the document forgot to declare, not\n * for a feature nobody has mapped yet, and not for a rating outside the three\n * the audit emits. Every one of those lands in a named bucket with the raw\n * value visible, because a silently dropped row reads exactly like a row that\n * was never written.\n *\n * Tallies are DERIVED here rather than read from the document. A stored total\n * is a second copy of the truth that goes stale the moment a row is corrected,\n * and nothing would fail: the page would show a number that no longer counts\n * its own rows.\n *\n * @module utils/capabilityComparison\n */\n\n/*\n * `process.env.NODE_ENV` is substituted by every consuming app's bundler at\n * build time (webpack's DefinePlugin, rollup's replace); see NcSelectTags.js.\n */\n/* global process */\n\nlet deprecationWarned = false\n\n/**\n * Warn once per page load, in a development build only, that the in-app\n * capability comparison is deprecated. Called by CnFeaturesAndRoadmapPage,\n * CnFeaturesAndRoadmapView and CnCapabilityTable when a comparison is\n * actually given, so an app that passes none hears nothing.\n *\n * @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-the-in-app-capability-comparison-is-deprecated\n * @param {string} source What carried the comparison, for the message.\n * @return {boolean} True when this call printed the warning.\n */\nexport function warnCapabilityComparisonDeprecated(source) {\n\tif (deprecationWarned || process.env.NODE_ENV === 'production') {\n\t\treturn false\n\t}\n\tdeprecationWarned = true\n\t// eslint-disable-next-line no-console -- a deliberate deprecation notice to the developer integrating the component\n\tconsole.warn(`[nextcloud-vue] ${source} is deprecated: the in-app capability comparison (capabilityComparison, the features_roadmap_capabilities initial state, CnCapabilityTable) will be removed in a future major version. Publish the comparison on the app's docs site and link to it instead.`)\n\treturn true\n}\n\n/**\n * Ratings in the order they are counted and rendered.\n *\n * @type {ReadonlyArray<string>}\n */\nexport const RATINGS = Object.freeze(['yes', 'partial', 'no'])\n\n/**\n * The columns the totals table and the legend show: the ratings plus\n * `unknown`. `unknown` is a real answer, not a data error. A later audit round\n * can add a capability row without re-reading the products an earlier round\n * rated, and the honest cell for those columns is empty.\n *\n * @type {ReadonlyArray<string>}\n */\nexport const RATING_COLUMNS = Object.freeze([...RATINGS, 'unknown'])\n\n/**\n * The provider kinds the document may declare. Anything else is carried\n * through as `unknown`, which renders as a plain chip rather than a styled\n * one.\n *\n * @type {ReadonlyArray<string>}\n */\nexport const PROVIDER_KINDS = Object.freeze(['self', 'app', 'platform'])\n\n/**\n * Group key for rows that carry no `feature`. It is a bucket, never a filter:\n * the rows render under a heading that says they are not mapped yet.\n *\n * @type {string}\n */\nexport const UNMAPPED_GROUP_KEY = '__cn-unmapped__'\n\n/**\n * Lower-case and strip diacritics so a reader typing `zaken` finds `Zaken` and\n * a reader typing `een` finds `Eén`.\n *\n * `normalize('NFD')` splits a letter from its accent and the range strips the\n * combining marks. Both have been in every browser and in Node since long\n * before this library's floor, so the accent-insensitive half costs one regex.\n *\n * @param {unknown} value Any value; non-strings are coerced.\n * @return {string} The comparable form.\n */\nexport function normaliseText(value) {\n\tif (value === null || value === undefined) {\n\t\treturn ''\n\t}\n\treturn String(value)\n\t\t.normalize('NFD')\n\t\t.replace(/[\\u0300-\\u036f]/g, '')\n\t\t.toLowerCase()\n\t\t.trim()\n}\n\n/**\n * Pick the reader's language variant of a labelled entry.\n *\n * The document carries `name` (English) and `name_nl` (Dutch) side by side.\n * Dutch wins only for a Dutch locale AND only when the Dutch string is\n * actually there, so a half-translated document degrades to English rather\n * than to a blank cell.\n *\n * @param {{name?: string, name_nl?: string}} entry Labelled entry.\n * @param {string} [locale] BCP 47 locale, e.g. `nl`, `nl_NL`, `en-GB`.\n * @return {string} The label to render.\n */\nexport function labelFor(entry, locale = 'en') {\n\tif (!entry) {\n\t\treturn ''\n\t}\n\tconst dutch = String(locale || '').toLowerCase().startsWith('nl')\n\tif (dutch && typeof entry.name_nl === 'string' && entry.name_nl.trim() !== '') {\n\t\treturn entry.name_nl\n\t}\n\treturn entry.name ?? ''\n}\n\n/**\n * Index the document's `providers` list by key.\n *\n * @param {object} comparison Parsed comparison document.\n * @return {Map<string, object>} Provider entries by key.\n */\nfunction providerIndex(comparison) {\n\tconst map = new Map()\n\tfor (const provider of comparison?.providers ?? []) {\n\t\tif (provider && typeof provider.key === 'string') {\n\t\t\tmap.set(provider.key, provider)\n\t\t}\n\t}\n\treturn map\n}\n\n/**\n * Resolve the provider of one capability row.\n *\n * An unrecognised key is NOT an error and never costs the row its place: the\n * raw key becomes the label and the kind becomes `unknown`, so a reader sees\n * that the document names a provider the dictionary forgot rather than seeing\n * nothing at all.\n *\n * @param {object} row One capability row.\n * @param {object} comparison Parsed comparison document.\n * @param {string} [locale] BCP 47 locale used to pick the provider name.\n * @return {{key: string, name: string, kind: string, declared: boolean}|null}\n * The provider, or null when the row names none.\n */\nexport function resolveProvider(row, comparison, locale = 'en') {\n\tconst key = row?.provider\n\tif (typeof key !== 'string' || key.trim() === '') {\n\t\treturn null\n\t}\n\tconst declared = providerIndex(comparison).get(key)\n\tif (!declared) {\n\t\treturn { key, name: key, kind: 'unknown', declared: false }\n\t}\n\tconst kind = PROVIDER_KINDS.includes(declared.kind) ? declared.kind : 'unknown'\n\treturn {\n\t\tkey,\n\t\tname: labelFor(declared, locale) || key,\n\t\tkind,\n\t\tdeclared: true,\n\t}\n}\n\n/**\n * Every string a search should match on for one row, joined.\n *\n * Both languages are indexed whatever the reader's locale, because a Dutch\n * reader searching for the English term they read in a tender document should\n * still find the row.\n *\n * @param {object} row One capability row.\n * @param {object} comparison Parsed comparison document.\n * @return {string} The haystack, already normalised.\n */\nexport function capabilitySearchIndex(row, comparison) {\n\tconst areas = comparison?.areas ?? []\n\tconst features = comparison?.features ?? []\n\tconst area = areas.find((entry) => entry?.key === row?.area)\n\tconst feature = features.find((entry) => entry?.key === row?.feature)\n\tconst provider = resolveProvider(row, comparison)\n\t// The declared entry and not `provider.name`: that one is already resolved\n\t// to a single language, so indexing it would honour the rule above for the\n\t// row, the area and the feature and quietly break it for the provider.\n\tconst declared = providerIndex(comparison).get(provider?.key)\n\tconst parts = [\n\t\trow?.id,\n\t\trow?.name,\n\t\trow?.name_nl,\n\t\trow?.area,\n\t\tarea?.name,\n\t\tarea?.name_nl,\n\t\trow?.feature,\n\t\tfeature?.name,\n\t\tfeature?.name_nl,\n\t\tprovider?.key,\n\t\tprovider?.name,\n\t\tdeclared?.name,\n\t\tdeclared?.name_nl,\n\t]\n\treturn parts.filter((part) => part !== null && part !== undefined && part !== '')\n\t\t.map((part) => normaliseText(part))\n\t\t.join(' ')\n}\n\n/**\n * Does one row match the reader's query?\n *\n * Every whitespace-separated term must appear somewhere in the row's index, so\n * `intake dossiq` narrows rather than widens. An empty query matches\n * everything, which is what keeps the unsearched page identical to the\n * searched-for-nothing page.\n *\n * @param {object} row One capability row.\n * @param {string} query The reader's raw input.\n * @param {object} comparison Parsed comparison document.\n * @return {boolean} True when the row should stay on the page.\n */\nexport function matchesQuery(row, query, comparison) {\n\tconst terms = normaliseText(query).split(/\\s+/).filter(Boolean)\n\tif (terms.length === 0) {\n\t\treturn true\n\t}\n\tconst haystack = capabilitySearchIndex(row, comparison)\n\treturn terms.every((term) => haystack.includes(term))\n}\n\n/**\n * The rows that survive the reader's query, in document order.\n *\n * @param {object} comparison Parsed comparison document.\n * @param {string} [query] The reader's raw input.\n * @return {Array<object>} Matching rows.\n */\nexport function filterCapabilities(comparison, query = '') {\n\treturn (comparison?.capabilities ?? []).filter((row) => matchesQuery(row, query, comparison))\n}\n\n/**\n * Count one system's ratings across a set of capability rows.\n *\n * A rating outside `RATINGS` is counted under `unknown` rather than dropped,\n * so a bad row shows up as a number that does not add up instead of\n * disappearing.\n *\n * @param {Array<object>} capabilities Capability rows.\n * @param {string} systemKey Key of the system column, e.g. `dossiq`.\n * @return {{yes: number, partial: number, no: number, unknown: number, total: number}} The tally.\n */\nexport function tally(capabilities, systemKey) {\n\tconst counts = { yes: 0, partial: 0, no: 0, unknown: 0, total: 0 }\n\tfor (const row of capabilities ?? []) {\n\t\tconst rating = row?.[systemKey]\n\t\tcounts[RATINGS.includes(rating) ? rating : 'unknown'] += 1\n\t\tcounts.total += 1\n\t}\n\treturn counts\n}\n\n/**\n * Tally every system over a set of rows.\n *\n * @param {object} comparison Parsed comparison document.\n * @param {Array<object>} [rows] Rows to count; defaults to all of them.\n * @return {Record<string, object>} Tally per system key.\n */\nexport function overallTallies(comparison, rows = null) {\n\tconst counted = rows ?? comparison?.capabilities ?? []\n\tconst out = {}\n\tfor (const system of comparison?.systems ?? []) {\n\t\tout[system.key] = tally(counted, system.key)\n\t}\n\treturn out\n}\n\n/**\n * Which grouping a document should open on.\n *\n * Feature grouping is the default the moment ANY row carries a `feature`,\n * because a document that bothered to map features wants them shown. A\n * document without them groups by area, which is what every consumer does\n * today.\n *\n * @param {object} comparison Parsed comparison document.\n * @return {'feature'|'area'} The opening mode.\n */\nexport function defaultGroupMode(comparison) {\n\tconst mapped = (comparison?.capabilities ?? []).some((row) => typeof row?.feature === 'string' && row.feature.trim() !== '')\n\treturn mapped ? 'feature' : 'area'\n}\n\n/**\n * Does any row in the document name a provider?\n *\n * Drives whether the provided-by column renders at all. A consumer whose\n * document predates the field gets exactly the columns it has today.\n *\n * @param {object} comparison Parsed comparison document.\n * @return {boolean} True when at least one row names a provider.\n */\nexport function hasProviders(comparison) {\n\treturn (comparison?.capabilities ?? []).some((row) => typeof row?.provider === 'string' && row.provider.trim() !== '')\n}\n\n/**\n * Group the rows for rendering, in the order a reader should meet them.\n *\n * Declared groups come first, in the order the document declares them: for\n * areas that is the audit's own numbering, and re-sorting would put `13.1`\n * above `1.1` on a table whose first column is that number. Then come keys the\n * rows use that the document never declared, labelled with the raw key. Last\n * comes the bucket for rows that name no group at all.\n *\n * An empty group is dropped, because a search that matches nothing in an area\n * should not leave an empty heading behind. The unmapped bucket follows the\n * same rule.\n *\n * @param {object} comparison Parsed comparison document.\n * @param {object} [options] Options.\n * @param {'feature'|'area'} [options.mode] Grouping mode.\n * @param {string} [options.locale] BCP 47 locale used for labels.\n * @param {string} [options.query] The reader's search input.\n * @param {string} [options.unmappedLabel] Heading for rows that name no group.\n * @return {Array<{key: string, label: string, declared: boolean, capabilities: Array<object>, tallies: object}>}\n * One entry per non-empty group.\n */\nexport function groupCapabilities(comparison, options = {}) {\n\tconst mode = options.mode === 'feature' ? 'feature' : 'area'\n\tconst locale = options.locale ?? 'en'\n\tconst unmappedLabel = options.unmappedLabel ?? ''\n\tconst declared = mode === 'feature' ? (comparison?.features ?? []) : (comparison?.areas ?? [])\n\tconst rows = filterCapabilities(comparison, options.query ?? '')\n\n\tconst buckets = new Map()\n\t/**\n\t * Fetch or create a bucket, keeping insertion order.\n\t *\n\t * @param {string} key Bucket key.\n\t * @param {string} label Bucket heading.\n\t * @param {boolean} isDeclared Whether the document declares this key.\n\t * @return {object} The bucket.\n\t */\n\tconst bucket = (key, label, isDeclared) => {\n\t\tif (!buckets.has(key)) {\n\t\t\tbuckets.set(key, { key, label, declared: isDeclared, capabilities: [] })\n\t\t}\n\t\treturn buckets.get(key)\n\t}\n\n\t// Seed the declared groups first so they keep the document's order even\n\t// when the rows arrive in another one.\n\tfor (const entry of declared) {\n\t\tif (entry && typeof entry.key === 'string') {\n\t\t\tbucket(entry.key, labelFor(entry, locale) || entry.key, true)\n\t\t}\n\t}\n\n\tconst unmapped = []\n\tfor (const row of rows) {\n\t\tconst key = row?.[mode]\n\t\tif (typeof key !== 'string' || key.trim() === '') {\n\t\t\tunmapped.push({ ...row, label: labelFor(row, locale) })\n\t\t\tcontinue\n\t\t}\n\t\t// An undeclared key gets a bucket of its own, labelled with the raw\n\t\t// key. Dropping the row here is the silent failure this module exists\n\t\t// to prevent.\n\t\tbucket(key, key, false).capabilities.push({ ...row, label: labelFor(row, locale) })\n\t}\n\n\tconst groups = [...buckets.values()].filter((entry) => entry.capabilities.length > 0)\n\tif (unmapped.length > 0) {\n\t\tgroups.push({\n\t\t\tkey: UNMAPPED_GROUP_KEY,\n\t\t\tlabel: unmappedLabel,\n\t\t\tdeclared: false,\n\t\t\tcapabilities: unmapped,\n\t\t})\n\t}\n\n\tfor (const group of groups) {\n\t\tgroup.tallies = overallTallies(comparison, group.capabilities)\n\t}\n\treturn groups\n}\n"],"names":[],"mappings":"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"}
|