@conduction/nextcloud-vue 2.63.0 → 2.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +253 -10
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +139 -46
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +87 -0
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +26 -24
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +61 -16
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +20 -0
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -2
- package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js +13 -0
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +2 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +33 -2
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +55 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +153 -133
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +37 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +12 -4
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +125 -14
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +87 -40
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +101 -7
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +22 -5
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +30 -0
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +5 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -2
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +63 -2
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js +96 -37
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/stagesModel.js +5 -2
- package/dist/esm/components/CnStagesWidget/stagesModel.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +74 -5
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +15 -8
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +14 -2
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +2 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +73 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +14 -2
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +2 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +2 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +495 -81
- package/dist/esm/store/useObjectStore.js +118 -33
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/actionsDispatcher.js +14 -0
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/schemaRefSlug.js +19 -3
- package/dist/esm/utils/schemaRefSlug.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +7388 -6021
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +495 -81
- package/l10n/en.json +2 -0
- package/l10n/nl.json +2 -0
- package/package.json +1 -1
- package/src/components/CnAppNav/CnAppNav.vue +441 -14
- package/src/components/CnAppRoot/CnAppRoot.md +4 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +94 -1
- package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +61 -16
- package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +22 -1
- package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +14 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +33 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +72 -4
- package/src/components/CnDataTable/CnDataTable.vue +45 -1
- package/src/components/CnDetailPage/CnDetailPage.md +8 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +169 -2
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +147 -9
- package/src/components/CnIndexPage/CnIndexPage.vue +32 -2
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +7 -1
- package/src/components/CnStagesWidget/CnStagesWidget.vue +189 -2
- package/src/components/CnStagesWidget/stagesModel.js +5 -2
- package/src/components/CnStatWidget/CnStatWidget.vue +89 -7
- package/src/components/CnTabsWidget/CnTabsWidget.vue +15 -2
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +4 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +107 -1
- package/src/css/detail-page.css +32 -4
- package/src/css/kpi-card.css +71 -1
- package/src/css/table.css +18 -6
- package/src/schemas/app-manifest-v2.schema.json +134 -3
- package/src/store/useObjectStore.js +118 -33
- package/src/utils/actionsDispatcher.js +14 -0
- package/src/utils/schemaRefSlug.js +19 -3
- package/src/utils/validateManifestV2.compiled.js +2 -2
package/src/css/kpi-card.css
CHANGED
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
padding: var(--cn-kpi-padding-flat, 8px);
|
|
134
134
|
box-sizing: border-box;
|
|
135
135
|
overflow: hidden;
|
|
136
|
-
text-align:
|
|
136
|
+
text-align: start;
|
|
137
137
|
text-decoration: none;
|
|
138
138
|
color: inherit;
|
|
139
139
|
background: transparent;
|
|
@@ -248,6 +248,29 @@
|
|
|
248
248
|
white-space: nowrap;
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/* A caption coloured by a rule (`captionVariant` / `overrides[].captionVariant`):
|
|
252
|
+
"1 due today" in the error colour. Foreground tokens, which stay readable in
|
|
253
|
+
dark mode. */
|
|
254
|
+
.cn-kpi-card__label--primary {
|
|
255
|
+
color: var(--color-primary-element);
|
|
256
|
+
font-weight: 600;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.cn-kpi-card__label--success {
|
|
260
|
+
color: var(--color-text-success, var(--color-success-text));
|
|
261
|
+
font-weight: 600;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.cn-kpi-card__label--warning {
|
|
265
|
+
color: var(--color-element-warning, var(--color-warning-text));
|
|
266
|
+
font-weight: 600;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.cn-kpi-card__label--error {
|
|
270
|
+
color: var(--color-text-error, var(--color-error-text));
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
}
|
|
273
|
+
|
|
251
274
|
/* Semantic variants. Each re-points the ONE accent token, so the icon tint
|
|
252
275
|
and the number move together by construction — there is no second place to
|
|
253
276
|
forget. The inline KPI below shares the same modifiers for the same reason:
|
|
@@ -344,6 +367,53 @@
|
|
|
344
367
|
font-size: var(--cn-kpi-value-size-stacked, 2rem);
|
|
345
368
|
}
|
|
346
369
|
|
|
370
|
+
/* Stacked: the board's tile (`content.layout: 'stacked'`). A plain muted
|
|
371
|
+
label, the number at 34px/700 in the text colour, and the caption on a line
|
|
372
|
+
of its own; CnStatWidget draws no icon circle in this layout. The padding
|
|
373
|
+
adds up with a dashboard card-fit cell's 8px 14px to the board's 18px 20px.
|
|
374
|
+
Every value is a token so a theme can move it. */
|
|
375
|
+
.cn-kpi-card--stacked {
|
|
376
|
+
flex-direction: column;
|
|
377
|
+
align-items: stretch;
|
|
378
|
+
justify-content: center;
|
|
379
|
+
gap: 0;
|
|
380
|
+
padding: var(--cn-kpi-stacked-padding, 10px 6px);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.cn-kpi-card--stacked .cn-kpi-card__body {
|
|
384
|
+
gap: var(--cn-kpi-stacked-gap, 6px);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.cn-kpi-card--stacked .cn-kpi-card__title {
|
|
388
|
+
font-size: var(--cn-kpi-stacked-title-size, 14px);
|
|
389
|
+
font-weight: var(--cn-kpi-stacked-title-weight, 400);
|
|
390
|
+
color: var(--color-text-maxcontrast);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.cn-kpi-card--stacked .cn-kpi-card__value {
|
|
394
|
+
font-size: var(--cn-kpi-stacked-value-size, 34px);
|
|
395
|
+
font-weight: var(--cn-kpi-value-weight, 700);
|
|
396
|
+
line-height: 1;
|
|
397
|
+
letter-spacing: -0.01em;
|
|
398
|
+
color: var(--cn-kpi-stacked-value-color, var(--color-main-text));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
.cn-kpi-card--stacked .cn-kpi-card__caption-line {
|
|
402
|
+
flex: 0 0 auto;
|
|
403
|
+
max-width: 100%;
|
|
404
|
+
font-size: var(--cn-kpi-label-size, 13px);
|
|
405
|
+
font-weight: 500;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/* A linked stacked tile reads as a tile, not as underlined link text. The
|
|
409
|
+
NL Design theme underlines every `a` with `!important`, so the opt-out
|
|
410
|
+
needs it too; it only applies to this opt-in layout. */
|
|
411
|
+
a.cn-kpi-card--stacked,
|
|
412
|
+
a.cn-kpi-card--stacked:hover,
|
|
413
|
+
a.cn-kpi-card--stacked:focus-visible {
|
|
414
|
+
text-decoration: none !important;
|
|
415
|
+
}
|
|
416
|
+
|
|
347
417
|
/* Filled: the card draws its own grey box, for a KPI that is NOT already
|
|
348
418
|
inside a wrapper that draws one. Opt-in via `content.flat: false`. */
|
|
349
419
|
.cn-kpi-card--filled {
|
package/src/css/table.css
CHANGED
|
@@ -155,7 +155,7 @@
|
|
|
155
155
|
.cn-data-table th,
|
|
156
156
|
.cn-data-table td {
|
|
157
157
|
padding: calc(3 * var(--default-grid-baseline));
|
|
158
|
-
text-align:
|
|
158
|
+
text-align: start;
|
|
159
159
|
border-bottom: 1px solid var(--color-border);
|
|
160
160
|
vertical-align: middle;
|
|
161
161
|
max-width: var(--cn-table-cell-max-width, 22rem);
|
|
@@ -174,6 +174,18 @@
|
|
|
174
174
|
white-space: normal;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/* The secondary line of a cell (`columns[].secondary`): muted, smaller, on its
|
|
178
|
+
own line under the value. Theme hooks: --cn-table-secondary-size and
|
|
179
|
+
--cn-table-secondary-color. */
|
|
180
|
+
.cn-data-table .cn-table-cell__secondary {
|
|
181
|
+
display: block;
|
|
182
|
+
color: var(--cn-table-secondary-color, var(--color-text-maxcontrast));
|
|
183
|
+
font-size: var(--cn-table-secondary-size, 0.9em);
|
|
184
|
+
line-height: 1.35;
|
|
185
|
+
overflow: hidden;
|
|
186
|
+
text-overflow: ellipsis;
|
|
187
|
+
}
|
|
188
|
+
|
|
177
189
|
/* Back to wrapping, for a column holding prose rather than a label:
|
|
178
190
|
`columns: [{ key: 'description', cellClass: 'cn-cell--wrap' }]`. */
|
|
179
191
|
.cn-data-table th.cn-cell--wrap,
|
|
@@ -235,7 +247,7 @@
|
|
|
235
247
|
}
|
|
236
248
|
|
|
237
249
|
.cn-data-table td.cn-cell--end {
|
|
238
|
-
text-align:
|
|
250
|
+
text-align: end;
|
|
239
251
|
white-space: nowrap;
|
|
240
252
|
}
|
|
241
253
|
|
|
@@ -273,7 +285,7 @@
|
|
|
273
285
|
}
|
|
274
286
|
|
|
275
287
|
.cn-table-sort-indicator {
|
|
276
|
-
margin-
|
|
288
|
+
margin-inline-start: var(--default-grid-baseline);
|
|
277
289
|
opacity: 0.6;
|
|
278
290
|
}
|
|
279
291
|
|
|
@@ -383,7 +395,7 @@
|
|
|
383
395
|
justify-content: center;
|
|
384
396
|
min-width: 14px;
|
|
385
397
|
height: 14px;
|
|
386
|
-
margin-
|
|
398
|
+
margin-inline-start: 2px;
|
|
387
399
|
padding: 0 3px;
|
|
388
400
|
border-radius: 7px;
|
|
389
401
|
background-color: var(--color-primary-element);
|
|
@@ -448,7 +460,7 @@
|
|
|
448
460
|
|
|
449
461
|
.cn-table-col--icon {
|
|
450
462
|
width: 40px;
|
|
451
|
-
padding-
|
|
463
|
+
padding-inline-end: 0;
|
|
452
464
|
text-align: center;
|
|
453
465
|
color: var(--color-text-maxcontrast);
|
|
454
466
|
}
|
|
@@ -511,7 +523,7 @@
|
|
|
511
523
|
|
|
512
524
|
.viewTable { width: 100%; border-collapse: collapse; background-color: var(--color-main-background); }
|
|
513
525
|
|
|
514
|
-
.viewTable th, .viewTable td { padding: calc(3 * var(--default-grid-baseline)); text-align:
|
|
526
|
+
.viewTable th, .viewTable td { padding: calc(3 * var(--default-grid-baseline)); text-align: start; border-bottom: 1px solid var(--color-border); vertical-align: middle; }
|
|
515
527
|
|
|
516
528
|
.viewTable th { background: var(--color-background-hover); font-weight: 500; color: var(--color-text-maxcontrast); background-color: var(--color-background-dark); }
|
|
517
529
|
|
|
@@ -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.47.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -238,6 +238,45 @@
|
|
|
238
238
|
"type": "string",
|
|
239
239
|
"description": "Override label for the settings foldout's gear button. Defaults to 'Settings'."
|
|
240
240
|
},
|
|
241
|
+
"card": {
|
|
242
|
+
"type": "object",
|
|
243
|
+
"additionalProperties": false,
|
|
244
|
+
"required": ["title"],
|
|
245
|
+
"description": "A card above the footer entries, rendered by CnAppNav (zuiddrecht-pixel-gaps): a title, a line of text and one link. Title, text and link label are i18n keys. Omitted, nothing renders there.",
|
|
246
|
+
"properties": {
|
|
247
|
+
"title": { "type": "string", "minLength": 1 },
|
|
248
|
+
"text": { "type": "string" },
|
|
249
|
+
"link": {
|
|
250
|
+
"type": "object",
|
|
251
|
+
"additionalProperties": false,
|
|
252
|
+
"required": ["label"],
|
|
253
|
+
"description": "The card's link: a `route` (named page, optional `params`) is a router link, an `href` a plain anchor, an `action` id a button that emits `card-action`.",
|
|
254
|
+
"properties": {
|
|
255
|
+
"label": { "type": "string", "minLength": 1 },
|
|
256
|
+
"route": { "type": "string" },
|
|
257
|
+
"params": { "type": "object", "additionalProperties": true },
|
|
258
|
+
"href": { "type": "string" },
|
|
259
|
+
"action": { "type": "string" }
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
"help": {
|
|
265
|
+
"type": "object",
|
|
266
|
+
"additionalProperties": false,
|
|
267
|
+
"required": ["label"],
|
|
268
|
+
"description": "A help entry with a help icon in the navigation footer, above the settings foldout (zuiddrecht-pixel-gaps): `route` (named page) or `href` (opens in a new tab).",
|
|
269
|
+
"properties": {
|
|
270
|
+
"label": { "type": "string", "minLength": 1 },
|
|
271
|
+
"route": { "type": "string" },
|
|
272
|
+
"href": { "type": "string" }
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
"footer": {
|
|
276
|
+
"type": "array",
|
|
277
|
+
"items": { "type": "string", "minLength": 1 },
|
|
278
|
+
"description": "Declare the navigation footer (zuiddrecht-pixel-gaps-2): an ordered list of `section: \"footer\"` menu entry ids, plus the reserved ids `help` (the `nav.help` entry) and `settings` (the settings foldout). Only the named footer entries render there, in that order; footer entries left out move into the settings foldout so their pages stay reachable. `settings` first puts the foldout above the list (the board's `[\"settings\", \"help\"]`). Omitted, the footer renders as before: help, every footer entry, then the foldout."
|
|
279
|
+
},
|
|
241
280
|
"primaryAction": {
|
|
242
281
|
"$ref": "#/$defs/primaryAction",
|
|
243
282
|
"description": "App-wide default primary action rendered as an NcAppNavigationNew button above the main menu list. A page-scoped pages[].primaryAction for the current route wins over this default."
|
|
@@ -251,6 +290,10 @@
|
|
|
251
290
|
"type": "string",
|
|
252
291
|
"description": "URL of the logo image. Decorative when `name` is set."
|
|
253
292
|
},
|
|
293
|
+
"emblem": {
|
|
294
|
+
"type": ["string", "boolean"],
|
|
295
|
+
"description": "An emblem drawn in place of the logo (zuiddrecht-pixel-gaps): the municipality's mark beside the app name, distinct from the wordmark in the top bar. A URL, or `true` to draw the theme's emblem from the `--nldesign-emblem-url` custom property. Decorative when `name` is set."
|
|
296
|
+
},
|
|
254
297
|
"name": {
|
|
255
298
|
"type": "string",
|
|
256
299
|
"description": "i18n key / text for the app or organisation name."
|
|
@@ -1158,6 +1201,23 @@
|
|
|
1158
1201
|
},
|
|
1159
1202
|
"payload": {
|
|
1160
1203
|
"description": "Free-form payload echoed back to the host inside the @primary-action-click event."
|
|
1204
|
+
},
|
|
1205
|
+
"action": {
|
|
1206
|
+
"$ref": "#/$defs/action",
|
|
1207
|
+
"description": "A page action to run on click (zuiddrecht-pixel-gaps): CnAppNav then renders the button through CnActionButtons, so `type: \"open-form\"` (with `register` + `schema`) opens the create dialog without a create page, `navigate` pushes a route, `api-call` calls an endpoint. Drawn as one solid full-width primary button. `route` / `href` on the primary action itself are ignored while `action` is set."
|
|
1208
|
+
},
|
|
1209
|
+
"solid": {
|
|
1210
|
+
"type": "boolean",
|
|
1211
|
+
"default": false,
|
|
1212
|
+
"description": "Draw the primary action as a solid, full-width primary button whatever its form (zuiddrecht-pixel-gaps). Default: a link action is already a primary button; a plain one is NcAppNavigationNew."
|
|
1213
|
+
},
|
|
1214
|
+
"permission": {
|
|
1215
|
+
"type": "string",
|
|
1216
|
+
"description": "Permission the reader must hold for the action to render, checked like `menuItem.permission` (zuiddrecht-pixel-gaps)."
|
|
1217
|
+
},
|
|
1218
|
+
"visibleIf": {
|
|
1219
|
+
"$ref": "#/$defs/visibleIfCondition",
|
|
1220
|
+
"description": "Display condition evaluated at render time, like `menuItem.visibleIf` (zuiddrecht-pixel-gaps). A gated-out page-scoped action falls back to `nav.primaryAction`."
|
|
1161
1221
|
}
|
|
1162
1222
|
}
|
|
1163
1223
|
},
|
|
@@ -1221,9 +1281,20 @@
|
|
|
1221
1281
|
"count": {
|
|
1222
1282
|
"oneOf": [
|
|
1223
1283
|
{ "type": "integer", "minimum": 0 },
|
|
1224
|
-
{ "type": "string", "enum": ["auto"] }
|
|
1284
|
+
{ "type": "string", "enum": ["auto"] },
|
|
1285
|
+
{
|
|
1286
|
+
"type": "object",
|
|
1287
|
+
"additionalProperties": false,
|
|
1288
|
+
"required": ["register", "schema"],
|
|
1289
|
+
"description": "A FILTERED count (zuiddrecht-pixel-gaps): the total of this register and schema with `filter` applied (filter-value tokens such as `@me` and `@today` resolve at fetch time), fetched by CnAppRoot under the entry's own key so an index page's whole-schema total is untouched. Renders \"My work 6\" beside \"All cases 48\".",
|
|
1290
|
+
"properties": {
|
|
1291
|
+
"register": { "type": "string", "minLength": 1 },
|
|
1292
|
+
"schema": { "type": "string", "minLength": 1 },
|
|
1293
|
+
"filter": { "type": "object", "additionalProperties": true }
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1225
1296
|
],
|
|
1226
|
-
"description": "Counter badge in the entry's #counter slot. Positive integer renders as-is; 'auto' resolves from cnMenuCounts (populated by CnAppRoot from useObjectStore totals) for the entry's resolved index-type page (register + schema)."
|
|
1297
|
+
"description": "Counter badge in the entry's #counter slot. Positive integer renders as-is; 'auto' resolves from cnMenuCounts (populated by CnAppRoot from useObjectStore totals) for the entry's resolved index-type page (register + schema); an object `{ register, schema, filter? }` is the total of that filtered list."
|
|
1227
1298
|
},
|
|
1228
1299
|
"pinned": {
|
|
1229
1300
|
"type": "boolean",
|
|
@@ -1485,6 +1556,19 @@
|
|
|
1485
1556
|
"minimum": 1,
|
|
1486
1557
|
"description": "Row span (>= 1)."
|
|
1487
1558
|
},
|
|
1559
|
+
"headerLink": {
|
|
1560
|
+
"type": "object",
|
|
1561
|
+
"additionalProperties": false,
|
|
1562
|
+
"required": ["label"],
|
|
1563
|
+
"description": "One text link in the widget header's actions group, before the overflow menu (zuiddrecht-pixel-gaps): \"All deadlines\", \"To the board\". A `route` (named page, optional `params` / `query`) is a router link, an `href` a plain anchor. The label is an i18n key. Rendered by CnWidgetWrapper; CnDashboardPage reads it off the layout entry or the widget definition.",
|
|
1564
|
+
"properties": {
|
|
1565
|
+
"label": { "type": "string", "minLength": 1 },
|
|
1566
|
+
"route": { "type": "string" },
|
|
1567
|
+
"params": { "type": "object", "additionalProperties": true },
|
|
1568
|
+
"query": { "type": "object", "additionalProperties": true },
|
|
1569
|
+
"href": { "type": "string" }
|
|
1570
|
+
}
|
|
1571
|
+
},
|
|
1488
1572
|
"sizeToContent": {
|
|
1489
1573
|
"type": ["boolean", "integer"],
|
|
1490
1574
|
"minimum": 1,
|
|
@@ -3519,6 +3603,53 @@
|
|
|
3519
3603
|
"type": "string",
|
|
3520
3604
|
"description": "type='index': accessible name and tooltip for the split pane's close button. Defaults to \"Close\". Maps to CnIndexPage `splitCloseLabel`."
|
|
3521
3605
|
},
|
|
3606
|
+
"showHeader": {
|
|
3607
|
+
"type": "boolean",
|
|
3608
|
+
"default": true,
|
|
3609
|
+
"description": "type='dashboard': `false` drops the header row (title, description, header actions, edit toggle) for a page that opens with its own heading, such as a greeting widget; the title stays as a visually hidden heading (zuiddrecht-pixel-gaps). Maps to CnDashboardPage `showHeader`."
|
|
3610
|
+
},
|
|
3611
|
+
"showWidgetActions": {
|
|
3612
|
+
"type": "boolean",
|
|
3613
|
+
"default": true,
|
|
3614
|
+
"description": "type='dashboard': `false` drops the overflow Actions menu from every widget that does not set `showActions` itself, for a dashboard whose widget headers carry a header link and nothing else (zuiddrecht-pixel-gaps-2). Maps to CnDashboardPage `showWidgetActions`."
|
|
3615
|
+
},
|
|
3616
|
+
"showTitle": {
|
|
3617
|
+
"type": "boolean",
|
|
3618
|
+
"default": false,
|
|
3619
|
+
"description": "type='index': show the page header (icon, title, description) inline. Default false: the title renders visually hidden and the sidebar names the page. Maps to CnIndexPage `showTitle`."
|
|
3620
|
+
},
|
|
3621
|
+
"countSubtitle": {
|
|
3622
|
+
"type": "string",
|
|
3623
|
+
"description": "type='index': an i18n key for the description line with `{total}` replaced by the collection's total, e.g. \"{total} open cases\" (zuiddrecht-pixel-gaps). Shown in place of `description` once a total is known. Maps to CnIndexPage `countSubtitle`."
|
|
3624
|
+
},
|
|
3625
|
+
"showTypeEyebrow": {
|
|
3626
|
+
"type": "boolean",
|
|
3627
|
+
"default": true,
|
|
3628
|
+
"description": "type='detail': `false` drops the type eyebrow (the type label above the record name) once the record resolves, for a header that says the type in a pill instead (zuiddrecht-pixel-gaps). Maps to CnDetailPage `showTypeEyebrow`."
|
|
3629
|
+
},
|
|
3630
|
+
"breadcrumb": {
|
|
3631
|
+
"type": "object",
|
|
3632
|
+
"additionalProperties": false,
|
|
3633
|
+
"required": ["label"],
|
|
3634
|
+
"description": "type='detail': a breadcrumb line above the header (zuiddrecht-pixel-gaps): this crumb names the list the record belongs to (`route` + optional `params`, or `href`), the record's display name follows as the current crumb. Maps to CnDetailPage `breadcrumb`.",
|
|
3635
|
+
"properties": {
|
|
3636
|
+
"label": { "type": "string", "minLength": 1 },
|
|
3637
|
+
"route": { "type": "string" },
|
|
3638
|
+
"params": { "type": "object", "additionalProperties": true },
|
|
3639
|
+
"href": { "type": "string" },
|
|
3640
|
+
"icon": { "type": "string", "minLength": 1, "description": "Optional MDI icon name (e.g. `Home`) drawn as the first crumb instead of the label, which then becomes its accessible name (zuiddrecht-pixel-gaps-2). Without it the label shows as text." }
|
|
3641
|
+
}
|
|
3642
|
+
},
|
|
3643
|
+
"headerCard": {
|
|
3644
|
+
"type": "boolean",
|
|
3645
|
+
"default": false,
|
|
3646
|
+
"description": "type='detail': draw the header (pills, title, actions) as a bordered card on the surface colour, as the board's case header (zuiddrecht-pixel-gaps-2). Maps to CnDetailPage `headerCard`."
|
|
3647
|
+
},
|
|
3648
|
+
"headerWidget": {
|
|
3649
|
+
"type": "string",
|
|
3650
|
+
"minLength": 1,
|
|
3651
|
+
"description": "type='detail': the id of a widget in `widgets` to render inside the header on its own row under the title, without a card of its own, e.g. the stages bars (zuiddrecht-pixel-gaps-2). The widget leaves the body grid. Maps to CnDetailPage `headerWidget`."
|
|
3652
|
+
},
|
|
3522
3653
|
"mapConfig": {
|
|
3523
3654
|
"type": "object",
|
|
3524
3655
|
"additionalProperties": true,
|
|
@@ -264,6 +264,27 @@ const baseActions = {
|
|
|
264
264
|
this.facets = { ...this.facets, [slug]: {} }
|
|
265
265
|
},
|
|
266
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Give a registered type a second schema path segment to try once when
|
|
269
|
+
* its own answers 404 (see `_fetchWithSchemaFallback`). Set by
|
|
270
|
+
* `resolveObjectOpType` for a schema title it kebab-cased, with the
|
|
271
|
+
* title as written. A type that is not registered is left alone.
|
|
272
|
+
*
|
|
273
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-schema-title-falls-back-to-its-written-form
|
|
274
|
+
* @param {string} slug The type slug
|
|
275
|
+
* @param {string} schema The fallback schema path segment
|
|
276
|
+
*/
|
|
277
|
+
setSchemaFallback(slug, schema) {
|
|
278
|
+
const config = this.objectTypeRegistry[slug]
|
|
279
|
+
if (!config || typeof schema !== 'string' || schema === '') {
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
this.objectTypeRegistry = {
|
|
283
|
+
...this.objectTypeRegistry,
|
|
284
|
+
[slug]: { ...config, schemaFallback: schema },
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
|
|
267
288
|
/**
|
|
268
289
|
* Unregister an object type and clean up all its state.
|
|
269
290
|
*
|
|
@@ -433,11 +454,12 @@ const baseActions = {
|
|
|
433
454
|
*
|
|
434
455
|
* @param {string} type The type slug
|
|
435
456
|
* @param {string|null} [id] Optional object ID
|
|
457
|
+
* @param {string|null} [schema] Schema path segment to use instead of the type's own
|
|
436
458
|
* @return {string} Full API URL path
|
|
437
459
|
*/
|
|
438
|
-
_buildUrl(type, id = null) {
|
|
460
|
+
_buildUrl(type, id = null, schema = null) {
|
|
439
461
|
const config = this._getTypeConfig(type)
|
|
440
|
-
let url = `${this._options.baseUrl}/${config.register}/${config.schema}`
|
|
462
|
+
let url = `${this._options.baseUrl}/${config.register}/${schema ?? config.schema}`
|
|
441
463
|
if (id) {
|
|
442
464
|
url += `/${id}`
|
|
443
465
|
}
|
|
@@ -458,11 +480,12 @@ const baseActions = {
|
|
|
458
480
|
* @param {string} type The type slug
|
|
459
481
|
* @param {object} [params] Extra query parameters
|
|
460
482
|
* @param {string|null} [id] Optional object ID
|
|
483
|
+
* @param {string|null} [schema] Schema path segment to use instead of the type's own
|
|
461
484
|
* @return {string} Full API URL path including query string
|
|
462
485
|
*/
|
|
463
|
-
_buildUrlWithParams(type, params = {}, id = null) {
|
|
486
|
+
_buildUrlWithParams(type, params = {}, id = null, schema = null) {
|
|
464
487
|
const config = this._getTypeConfig(type)
|
|
465
|
-
let url = `${this._options.baseUrl}/${config.register}/${config.schema}`
|
|
488
|
+
let url = `${this._options.baseUrl}/${config.register}/${schema ?? config.schema}`
|
|
466
489
|
if (id) {
|
|
467
490
|
url += `/${id}`
|
|
468
491
|
}
|
|
@@ -472,6 +495,54 @@ const baseActions = {
|
|
|
472
495
|
return url
|
|
473
496
|
},
|
|
474
497
|
|
|
498
|
+
/**
|
|
499
|
+
* Fetch for a type, retrying ONCE with its `schemaFallback` when the
|
|
500
|
+
* schema path segment answers 404. A type without a fallback (every type
|
|
501
|
+
* not registered by `resolveObjectOpType` for a kebab-cased title) makes
|
|
502
|
+
* exactly the one request it always made. When the retry succeeds the
|
|
503
|
+
* fallback becomes the type's schema, so later calls go straight to it;
|
|
504
|
+
* when it does not, the first answer is returned and nothing changes.
|
|
505
|
+
*
|
|
506
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-schema-title-falls-back-to-its-written-form
|
|
507
|
+
* @param {string} type The type slug
|
|
508
|
+
* @param {function((string|null)): string} makeUrl Builds the URL; called with
|
|
509
|
+
* null for the type's own schema, or with the fallback segment.
|
|
510
|
+
* @param {object} init The fetch options.
|
|
511
|
+
* @return {Promise<Response>} The response to use.
|
|
512
|
+
*/
|
|
513
|
+
async _fetchWithSchemaFallback(type, makeUrl, init) {
|
|
514
|
+
// `makeUrl` wraps `_buildUrl` / `_buildUrlWithParams`, so `url` is built
|
|
515
|
+
// from the prefixed `_options.baseUrl` like every other request here.
|
|
516
|
+
let url = makeUrl(null)
|
|
517
|
+
const response = await fetch(url, init)
|
|
518
|
+
if (!response || response.status !== 404) {
|
|
519
|
+
return response
|
|
520
|
+
}
|
|
521
|
+
const config = this.objectTypeRegistry[type]
|
|
522
|
+
const fallback = config && config.schemaFallback
|
|
523
|
+
if (!fallback || String(fallback) === String(config.schema)) {
|
|
524
|
+
return response
|
|
525
|
+
}
|
|
526
|
+
let retry
|
|
527
|
+
try {
|
|
528
|
+
url = makeUrl(fallback)
|
|
529
|
+
retry = await fetch(url, init)
|
|
530
|
+
} catch {
|
|
531
|
+
return response
|
|
532
|
+
}
|
|
533
|
+
if (!retry || !retry.ok) {
|
|
534
|
+
discardResponseBody(retry)
|
|
535
|
+
return response
|
|
536
|
+
}
|
|
537
|
+
discardResponseBody(response)
|
|
538
|
+
const { schemaFallback: _, ...rest } = config
|
|
539
|
+
this.objectTypeRegistry = {
|
|
540
|
+
...this.objectTypeRegistry,
|
|
541
|
+
[type]: { ...rest, schema: fallback },
|
|
542
|
+
}
|
|
543
|
+
return retry
|
|
544
|
+
},
|
|
545
|
+
|
|
475
546
|
/**
|
|
476
547
|
* Clear the error state for a type.
|
|
477
548
|
*
|
|
@@ -604,12 +675,14 @@ const baseActions = {
|
|
|
604
675
|
}
|
|
605
676
|
}
|
|
606
677
|
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
678
|
+
const response = await this._fetchWithSchemaFallback(
|
|
679
|
+
type,
|
|
680
|
+
(schema) => this._buildUrlWithParams(type, fetchParams, null, schema),
|
|
681
|
+
{
|
|
682
|
+
method: 'GET',
|
|
683
|
+
headers: this._buildHeaders(),
|
|
684
|
+
},
|
|
685
|
+
)
|
|
613
686
|
|
|
614
687
|
if (!response.ok) {
|
|
615
688
|
this.errors = { ...this.errors, [type]: await parseResponseError(response, type) }
|
|
@@ -675,11 +748,14 @@ const baseActions = {
|
|
|
675
748
|
*/
|
|
676
749
|
async fetchCollectionForOptions(type, params = {}) {
|
|
677
750
|
try {
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
751
|
+
const response = await this._fetchWithSchemaFallback(
|
|
752
|
+
type,
|
|
753
|
+
(schema) => this._buildUrlWithParams(type, params, null, schema),
|
|
754
|
+
{
|
|
755
|
+
method: 'GET',
|
|
756
|
+
headers: this._buildHeaders(),
|
|
757
|
+
},
|
|
758
|
+
)
|
|
683
759
|
|
|
684
760
|
if (!response.ok) {
|
|
685
761
|
discardResponseBody(response)
|
|
@@ -734,10 +810,14 @@ const baseActions = {
|
|
|
734
810
|
this.errors = { ...this.errors, [type]: null }
|
|
735
811
|
|
|
736
812
|
try {
|
|
737
|
-
const response = await
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
813
|
+
const response = await this._fetchWithSchemaFallback(
|
|
814
|
+
type,
|
|
815
|
+
(schema) => (schema === null ? url : this._buildUrl(type, id, schema)),
|
|
816
|
+
{
|
|
817
|
+
method: 'GET',
|
|
818
|
+
headers: this._buildHeaders(),
|
|
819
|
+
},
|
|
820
|
+
)
|
|
741
821
|
|
|
742
822
|
if (!response.ok) {
|
|
743
823
|
this.errors = { ...this.errors, [type]: await parseResponseError(response, type) }
|
|
@@ -805,16 +885,19 @@ const baseActions = {
|
|
|
805
885
|
|
|
806
886
|
try {
|
|
807
887
|
const isUpdate = !!objectData.id
|
|
808
|
-
const url = isUpdate
|
|
809
|
-
? this._buildUrl(type, objectData.id)
|
|
810
|
-
: this._buildUrl(type)
|
|
811
888
|
const method = isUpdate ? 'PUT' : 'POST'
|
|
812
889
|
|
|
813
|
-
const response = await
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
890
|
+
const response = await this._fetchWithSchemaFallback(
|
|
891
|
+
type,
|
|
892
|
+
(schema) => (isUpdate
|
|
893
|
+
? this._buildUrl(type, objectData.id, schema)
|
|
894
|
+
: this._buildUrl(type, null, schema)),
|
|
895
|
+
{
|
|
896
|
+
method,
|
|
897
|
+
headers: this._buildHeaders(),
|
|
898
|
+
body: JSON.stringify(objectData),
|
|
899
|
+
},
|
|
900
|
+
)
|
|
818
901
|
|
|
819
902
|
if (!response.ok) {
|
|
820
903
|
this.errors = { ...this.errors, [type]: await parseResponseError(response, type) }
|
|
@@ -866,12 +949,14 @@ const baseActions = {
|
|
|
866
949
|
this.errors = { ...this.errors, [type]: null }
|
|
867
950
|
|
|
868
951
|
try {
|
|
869
|
-
const
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
952
|
+
const response = await this._fetchWithSchemaFallback(
|
|
953
|
+
type,
|
|
954
|
+
(schema) => this._buildUrl(type, id, schema),
|
|
955
|
+
{
|
|
956
|
+
method: 'DELETE',
|
|
957
|
+
headers: this._buildHeaders(),
|
|
958
|
+
},
|
|
959
|
+
)
|
|
875
960
|
|
|
876
961
|
if (!response.ok) {
|
|
877
962
|
this.errors = { ...this.errors, [type]: await parseResponseError(response, type) }
|
|
@@ -100,6 +100,14 @@ export function isExternalActionTarget(target) {
|
|
|
100
100
|
* would otherwise register — and fetch — a 404 (defect 7, learniq round
|
|
101
101
|
* 1). Idempotent for a caller that already passes a correct slug.
|
|
102
102
|
*
|
|
103
|
+
* A title is not always registered under its kebab form: learniq holds
|
|
104
|
+
* `LearniqSettings` and `SubjectTeacherAssignment` as written. So a type
|
|
105
|
+
* registered here for a value the kebab rule CHANGED also carries the value
|
|
106
|
+
* as written as its `schemaFallback`, and the store retries with it once
|
|
107
|
+
* when the kebab form answers 404 (keeping it from then on). The kebab form
|
|
108
|
+
* is still tried first, so a schema registered under it costs nothing.
|
|
109
|
+
*
|
|
110
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-schema-title-falls-back-to-its-written-form
|
|
103
111
|
* @param {object} store The object store instance (useObjectStore shape).
|
|
104
112
|
* @param {{register: (string|number), schema: (string|number)}} source The widget source.
|
|
105
113
|
* @return {string} The type slug to use for store CRUD calls.
|
|
@@ -107,6 +115,9 @@ export function isExternalActionTarget(target) {
|
|
|
107
115
|
export function resolveObjectOpType(store, source) {
|
|
108
116
|
const register = String(source.register)
|
|
109
117
|
const schema = String(schemaRefSlug(source.schema))
|
|
118
|
+
const written = typeof source.schema === 'string'
|
|
119
|
+
? source.schema.substring(source.schema.lastIndexOf('/') + 1)
|
|
120
|
+
: ''
|
|
110
121
|
const registry = store.objectTypeRegistry || {}
|
|
111
122
|
for (const [slug, config] of Object.entries(registry)) {
|
|
112
123
|
if (!config) {
|
|
@@ -120,6 +131,9 @@ export function resolveObjectOpType(store, source) {
|
|
|
120
131
|
const slug = `${register}/${schema}`
|
|
121
132
|
if (!registry[slug]) {
|
|
122
133
|
store.registerObjectType(slug, schema, register)
|
|
134
|
+
if (written !== '' && written !== schema && typeof store.setSchemaFallback === 'function') {
|
|
135
|
+
store.setSchemaFallback(slug, written)
|
|
136
|
+
}
|
|
123
137
|
}
|
|
124
138
|
return slug
|
|
125
139
|
}
|
|
@@ -35,9 +35,14 @@
|
|
|
35
35
|
* registers author the slug by hand for multi-word titles) folds to one
|
|
36
36
|
* `-`, leading/trailing dashes trimmed.
|
|
37
37
|
*
|
|
38
|
-
* Idempotent: a value that is already a slug (`report-period`, `cohort
|
|
39
|
-
* passes through unchanged, so it is always safe to route an
|
|
40
|
-
* value through this function defensively.
|
|
38
|
+
* Idempotent: a value that is already a slug (`report-period`, `cohort`,
|
|
39
|
+
* `statusType`) passes through unchanged, so it is always safe to route an
|
|
40
|
+
* already-correct value through this function defensively. A slug is any
|
|
41
|
+
* value that starts with a lowercase letter or digit and holds only
|
|
42
|
+
* letters, digits and dashes; only a title (capital initial, or spaces and
|
|
43
|
+
* punctuation) is kebab-cased.
|
|
44
|
+
*
|
|
45
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-plain-schema-slug-is-left-as-written
|
|
41
46
|
*
|
|
42
47
|
* A numeric schema id (OpenRegister accepts either the slug or the numeric
|
|
43
48
|
* id in the objects-API path) is returned unchanged — slugifying a number
|
|
@@ -63,6 +68,17 @@ export function schemaRefSlug(ref) {
|
|
|
63
68
|
if (tail === '') {
|
|
64
69
|
return ''
|
|
65
70
|
}
|
|
71
|
+
// A plain slug is left as written. OpenRegister matches a slug
|
|
72
|
+
// case-insensitively (`statusType`, `statustype` and `StatusType` all
|
|
73
|
+
// answer 200) but `status-type` is another slug and 404s, so a
|
|
74
|
+
// camelCase slug authored as a slug must not be kebab-cased. Only a
|
|
75
|
+
// `$ref` TITLE is: it starts with a capital (`ReportPeriod`) or carries
|
|
76
|
+
// spaces or punctuation (`Praktijkovereenkomst (POK)`). 2.62.0 kebab-cased
|
|
77
|
+
// everything and broke every camelCase slug (dossiq: statusType, caseType,
|
|
78
|
+
// caseObject, bezwaarDecision, bacAdviceRequest, wmsLayer, ...).
|
|
79
|
+
if (/^[a-z0-9][A-Za-z0-9-]*$/.test(tail)) {
|
|
80
|
+
return tail
|
|
81
|
+
}
|
|
66
82
|
const kebab = tail
|
|
67
83
|
// PascalCase / camelCase word boundaries: `ReportPeriod` -> `Report-Period`,
|
|
68
84
|
// an acronym run followed by a new capitalised word: `HTTPServer` -> `HTTP-Server`.
|