@conduction/nextcloud-vue 2.97.0 → 2.99.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/CnActionsMenu/CnActionsMenu.vue2.js +12 -0
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js +11 -2
- package/dist/esm/components/CnActionsMenu/CnActionsMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +17 -0
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue.js +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +106 -2
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +9 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js.map +1 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js +964 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js +282 -0
- package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnChecklistRunWidget/index.js +28 -0
- package/dist/esm/components/CnChecklistRunWidget/index.js.map +1 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +1 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +26 -8
- 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 +188 -5
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +2 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue.js +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +3 -3
- 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/CnFeaturesBoard/CnFeaturesBoard.vue.js +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js +27 -0
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js +16 -2
- package/dist/esm/components/CnFeaturesBoard/CnFeaturesBoard.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +43 -3
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +9 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js.map +1 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +239 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +182 -0
- package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +9 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js.map +1 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js +265 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +191 -0
- package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js +9 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js.map +1 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js +508 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js.map +1 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js +99 -0
- package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js.map +1 -0
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +18 -70
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js +6 -44
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js +83 -9
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js +80 -11
- package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +2 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue.js +9 -0
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue.js.map +1 -0
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue2.js +472 -0
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue3.js +231 -0
- package/dist/esm/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnWorkingCalendarWidget/index.js +27 -0
- package/dist/esm/components/CnWorkingCalendarWidget/index.js.map +1 -0
- package/dist/esm/composables/indexSources.js +238 -2
- package/dist/esm/composables/indexSources.js.map +1 -1
- package/dist/esm/composables/useFlowStore.js +2 -1
- package/dist/esm/composables/useFlowStore.js.map +1 -1
- package/dist/esm/composables/useOrganisationStore.js +115 -0
- package/dist/esm/composables/useOrganisationStore.js.map +1 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +109 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +138 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +1071 -274
- package/dist/esm/utils/builtInFormatters.js +34 -1
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/libraryWidgetKeys.js +2 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/matrixCells.js +158 -0
- package/dist/esm/utils/matrixCells.js.map +1 -0
- package/dist/esm/utils/nextcloudDirectory.js +121 -0
- package/dist/esm/utils/nextcloudDirectory.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/workingCalendarMonth.js +85 -0
- package/dist/esm/utils/workingCalendarMonth.js.map +1 -0
- package/dist/nextcloud-vue.cjs.js +14888 -10014
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1071 -274
- package/l10n/en.json +109 -1
- package/l10n/nl.json +138 -1
- package/package.json +2 -2
- package/scripts/merge-l10n.js +196 -0
- package/src/components/CnActionsMenu/CnActionsMenu.vue +14 -1
- package/src/components/CnAppRoot/CnAppRoot.md +1 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +17 -0
- package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +1 -0
- package/src/components/CnChartWidget/CnChartWidget.md +2 -1
- package/src/components/CnChartWidget/CnChartWidget.vue +106 -2
- package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +1269 -0
- package/src/components/CnChecklistRunWidget/index.js +27 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +26 -8
- package/src/components/CnDetailPage/CnDetailPage.md +48 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +189 -5
- package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +3 -3
- package/src/components/CnFeaturesBoard/CnFeaturesBoard.vue +38 -2
- package/src/components/CnIndexPage/CnIndexPage.md +28 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +43 -3
- package/src/components/CnOrderedListField/CnOrderedListField.vue +458 -0
- package/src/components/CnOrderedListField/index.js +7 -0
- package/src/components/CnSettingsField/CnSettingsField.vue +449 -0
- package/src/components/CnSettingsField/index.js +4 -0
- package/src/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue +613 -0
- package/src/components/CnSettingsFieldsSection/index.js +4 -0
- package/src/components/CnSettingsPage/CnSettingsPage.vue +6 -86
- package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +139 -10
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +2 -0
- package/src/components/CnWorkingCalendarWidget/CnWorkingCalendarWidget.vue +725 -0
- package/src/components/CnWorkingCalendarWidget/index.js +26 -0
- package/src/components/index.js +5 -0
- package/src/composables/indexSources.js +237 -1
- package/src/composables/useFlowStore.js +2 -1
- package/src/composables/useOrganisationStore.js +111 -0
- package/src/css/badge.css +32 -16
- package/src/css/look-board-detail.css +80 -8
- package/src/css/look-board-index.css +52 -8
- package/src/css/look-board.css +219 -1
- package/src/index.js +5 -0
- package/src/schemas/app-manifest-v2.schema.json +33 -3
- package/src/utils/builtInFormatters.js +33 -0
- package/src/utils/libraryWidgetKeys.js +2 -0
- package/src/utils/matrixCells.js +154 -0
- package/src/utils/nextcloudDirectory.js +117 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/workingCalendarMonth.js +82 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* CnChecklistRunWidget — self-registers the `checklist-run` widget type (a
|
|
6
|
+
* checklist answered a page of questions at a time, with a note and photos)
|
|
7
|
+
* into the shared dashboardWidgetRegistry at module load. Renderer only: the
|
|
8
|
+
* questions and where the answers go are declared in the manifest.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import CnChecklistRunWidget from './CnChecklistRunWidget.vue'
|
|
12
|
+
import { registerDashboardWidget } from '../CnWidgetGrid/dashboardWidgetRegistry.js'
|
|
13
|
+
|
|
14
|
+
registerDashboardWidget('checklist-run', {
|
|
15
|
+
renderer: CnChecklistRunWidget,
|
|
16
|
+
form: null,
|
|
17
|
+
defaultContent: {
|
|
18
|
+
questions: { register: '', schema: '', filter: {} },
|
|
19
|
+
pageSize: 1,
|
|
20
|
+
},
|
|
21
|
+
displayName: 'Checklist run',
|
|
22
|
+
icon: 'ClipboardCheckOutline',
|
|
23
|
+
surfaces: ['detail-page', 'app-dashboard'],
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export { CnChecklistRunWidget }
|
|
27
|
+
export default CnChecklistRunWidget
|
|
@@ -408,6 +408,7 @@ import { fillTemplate, joinTemplateParts } from '../../utils/fillTemplate.js'
|
|
|
408
408
|
import { lensUnavailableText, readLensReports } from '../../utils/lensAvailability.js'
|
|
409
409
|
import { followLinkClick, openRowTarget, resolveHref } from '../../utils/linkNavigation.js'
|
|
410
410
|
import { nextSortState } from '../../utils/multiColumnSort.js'
|
|
411
|
+
import { readPath } from '../../utils/readPath.js'
|
|
411
412
|
import { isNewTabClick, isNewTabHandled, isRowMiddleClick, markNewTabHandled, preventMiddleClickAutoscroll } from '../../utils/rowAuxClick.js'
|
|
412
413
|
import { DEFAULT_ROW_INDICATOR_CAP, resolveRowIndicators } from '../../utils/rowIndicators.js'
|
|
413
414
|
import { columnsFromSchema } from '../../utils/schema.js'
|
|
@@ -1841,8 +1842,11 @@ export default {
|
|
|
1841
1842
|
if (v === undefined) {
|
|
1842
1843
|
return this.aggregateLoading ? '…' : '—'
|
|
1843
1844
|
}
|
|
1845
|
+
if (v === null && !(typeof col.aggregate.zeroLabel === 'string' && col.aggregate.zeroLabel !== '')) {
|
|
1846
|
+
return '—'
|
|
1847
|
+
}
|
|
1844
1848
|
const zeroLabel = col.aggregate.zeroLabel
|
|
1845
|
-
if ((v === 0 || v === '0') && typeof zeroLabel === 'string' && zeroLabel !== '') {
|
|
1849
|
+
if ((v === 0 || v === '0' || v === null) && typeof zeroLabel === 'string' && zeroLabel !== '') {
|
|
1846
1850
|
return typeof this.cnTranslate === 'function' ? this.cnTranslate(zeroLabel) : zeroLabel
|
|
1847
1851
|
}
|
|
1848
1852
|
return v
|
|
@@ -1872,17 +1876,21 @@ export default {
|
|
|
1872
1876
|
},
|
|
1873
1877
|
|
|
1874
1878
|
/**
|
|
1875
|
-
* For every column that declares `aggregate
|
|
1876
|
-
*
|
|
1877
|
-
*
|
|
1879
|
+
* For every column that declares `aggregate`, issue one request per
|
|
1880
|
+
* visible row against the related OpenRegister collection and cache the
|
|
1881
|
+
* answer in `aggregateValues`: `op: "count"` asks `_limit=0` for the
|
|
1882
|
+
* total, `op: "max"` asks for the one object with the highest `field`
|
|
1883
|
+
* (`_limit=1`, sorted descending) and keeps that value, null when there
|
|
1884
|
+
* is none (shown as `zeroLabel`, else a dash).
|
|
1878
1885
|
* Batched with `Promise.all`; a per-request failure degrades that one
|
|
1879
1886
|
* cell to `'—'` (logged), never the page. A monotonic request id
|
|
1880
1887
|
* discards a stale batch when `rows` / columns change mid-flight.
|
|
1881
1888
|
*
|
|
1882
1889
|
* @return {Promise<void>}
|
|
1890
|
+
* @spec openspec/changes/masked-formatter-max-aggregate/specs/index-page/spec.md#requirement-a-column-aggregate-can-take-the-highest-value
|
|
1883
1891
|
*/
|
|
1884
1892
|
async loadAggregates() {
|
|
1885
|
-
const aggCols = this.effectiveColumns.filter((c) => c && c.aggregate && c.aggregate.op === 'count')
|
|
1893
|
+
const aggCols = this.effectiveColumns.filter((c) => c && c.aggregate && (c.aggregate.op === 'count' || (c.aggregate.op === 'max' && c.aggregate.field)))
|
|
1886
1894
|
if (aggCols.length === 0) {
|
|
1887
1895
|
if (Object.keys(this.aggregateValues).length > 0) {
|
|
1888
1896
|
this.aggregateValues = {}
|
|
@@ -1903,11 +1911,21 @@ export default {
|
|
|
1903
1911
|
continue
|
|
1904
1912
|
}
|
|
1905
1913
|
const where = this.resolveAggregateWhere(agg.where, row)
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1914
|
+
// `max`: the highest `field` among the related objects, as the
|
|
1915
|
+
// first of them sorted on it descending ("Laatste contact").
|
|
1916
|
+
const isMax = agg.op === 'max'
|
|
1917
|
+
const params = isMax
|
|
1918
|
+
? { ...where, _limit: 1, [`_order[${agg.field}]`]: 'desc' }
|
|
1919
|
+
: { ...where, _limit: 0 }
|
|
1920
|
+
jobs.push(axios.get(generateUrl(`/apps/openregister/api/objects/${agg.register}/${agg.schema}`), { params })
|
|
1909
1921
|
.then((res) => {
|
|
1910
1922
|
const d = res && res.data
|
|
1923
|
+
if (isMax) {
|
|
1924
|
+
const first = d && Array.isArray(d.results) ? d.results[0] : undefined
|
|
1925
|
+
const top = first === undefined ? undefined : readPath(first, agg.field)
|
|
1926
|
+
next[rowKey][col.key] = top === undefined || top === '' ? null : top
|
|
1927
|
+
return
|
|
1928
|
+
}
|
|
1911
1929
|
next[rowKey][col.key] = (d && (d.total ?? (Array.isArray(d.results) ? d.results.length : undefined))) ?? 0
|
|
1912
1930
|
})
|
|
1913
1931
|
.catch((e) => {
|
|
@@ -434,6 +434,54 @@ Four levels of action and the case surfaces around them. All opt in. See [the re
|
|
|
434
434
|
|
|
435
435
|
`previousTo` and `nextTo` (default `null`) take `useListNavigation`'s `previousRoute` / `nextRoute` and turn the previous and next controls into real links, so they can be opened in a new tab. The link navigates by itself; `previous-record` / `next-record` are still emitted, with `{ event, to }`, and `goPrevious` / `goNext` do not push again when they receive that payload.
|
|
436
436
|
|
|
437
|
+
## Records that are not objects (`config.entitySource`)
|
|
438
|
+
|
|
439
|
+
A detail page reads its record from the object store through `register` +
|
|
440
|
+
`schema`. A record that is not a register/schema object names a registered
|
|
441
|
+
entity source instead, the same registry and the same name `CnIndexPage`
|
|
442
|
+
lists it with. `organisations` reads the OpenRegister organisation whose uuid
|
|
443
|
+
is the route's id (`GET /apps/openregister/api/organisations/{uuid}`):
|
|
444
|
+
|
|
445
|
+
```json
|
|
446
|
+
{
|
|
447
|
+
"id": "organisation-detail",
|
|
448
|
+
"route": "/organisations/:id",
|
|
449
|
+
"type": "detail",
|
|
450
|
+
"config": {
|
|
451
|
+
"entitySource": "organisations",
|
|
452
|
+
"statusPill": { "field": "status" },
|
|
453
|
+
"typePill": { "field": "type" },
|
|
454
|
+
"headerFields": [{ "key": "slug" }],
|
|
455
|
+
"layout": [{ "id": "1", "widgetId": "facts", "gridX": 0, "gridY": 0, "gridWidth": 6 }],
|
|
456
|
+
"widgets": [{
|
|
457
|
+
"id": "facts",
|
|
458
|
+
"type": "facts",
|
|
459
|
+
"title": "Details",
|
|
460
|
+
"content": { "fields": [
|
|
461
|
+
{ "path": "slug", "label": "Slug" },
|
|
462
|
+
{ "path": "created", "label": "Created", "format": "date" },
|
|
463
|
+
{ "path": "contactEmail", "label": "Contact", "format": "email" },
|
|
464
|
+
{ "path": "kvk", "label": "KvK" }
|
|
465
|
+
] }
|
|
466
|
+
}]
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
The record feeds everything a fetched object feeds: the title, the pills, the
|
|
472
|
+
header fields, the widgets, and the `@objectId` / `@object.*` tokens. A tab
|
|
473
|
+
that lists register objects belonging to the organisation filters on
|
|
474
|
+
`@objectId`, e.g. `{ "type": "object-list", "content": { "register": "...",
|
|
475
|
+
"schema": "...", "filter": { "tenantRef": "@objectId" } } }`. The record's
|
|
476
|
+
`id` is its uuid, as an object's is.
|
|
477
|
+
|
|
478
|
+
`entitySource` wins over `register` + `schema`. A name that is unknown, or a
|
|
479
|
+
source that cannot read one record, shows no record and warns in the console.
|
|
480
|
+
A refusal (403) or a missing record (404) shows the not-found state; which
|
|
481
|
+
records the viewer may read is OpenRegister's decision. Refresh and
|
|
482
|
+
`cn:page:refresh` re-read the record. There is no Edit: a record source has
|
|
483
|
+
no schema form.
|
|
484
|
+
|
|
437
485
|
## Not found
|
|
438
486
|
|
|
439
487
|
When the schema-driven fetch answers 404 the page shows a not-found state whose back button is a router link to `notFoundRoute` (default `null`, which means the app root `/`). `notFoundRouteLabel` names that page in the button's "Back to …" label.
|
|
@@ -379,6 +379,7 @@
|
|
|
379
379
|
:showDocumentation="showDocumentation && menuShowsHelpLinks"
|
|
380
380
|
:actionsMenuLabel="boardMenuLabel"
|
|
381
381
|
:variant="isBoard ? 'secondary' : undefined"
|
|
382
|
+
:trailingChevron="isBoard"
|
|
382
383
|
:hasPrimaryItems="headerMenuGroups.length > 0 || showMarkUnread || showOpenSidebar"
|
|
383
384
|
:documentationUrl="documentationUrl"
|
|
384
385
|
:docsAnchor="resolvedPageId"
|
|
@@ -1232,11 +1233,13 @@ import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
|
|
|
1232
1233
|
import CnSummaryAggregates from '../CnSummaryAggregates/CnSummaryAggregates.vue'
|
|
1233
1234
|
import CnTranslatedBadge from '../CnTranslatedBadge/CnTranslatedBadge.vue'
|
|
1234
1235
|
import CnDetailHeaderChips from './CnDetailHeaderChips.vue'
|
|
1236
|
+
import { resolveRecordSource } from '../../composables/indexSources.js'
|
|
1235
1237
|
import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
|
|
1236
1238
|
import { normalizeLook } from '../../composables/useLook.js'
|
|
1237
1239
|
import { useObjectLock } from '../../composables/useObjectLock.js'
|
|
1238
1240
|
import { useObjectPresence } from '../../composables/useObjectPresence.js'
|
|
1239
1241
|
import { useObjectSubscription } from '../../composables/useObjectSubscription.js'
|
|
1242
|
+
import { createRefLabelResolver } from '../../composables/useRefLabels.js'
|
|
1240
1243
|
import { gridLayout } from '../../mixins/gridLayout.js'
|
|
1241
1244
|
import { pageViews } from '../../mixins/pageViews.js'
|
|
1242
1245
|
import { useObjectStore } from '../../store/index.js'
|
|
@@ -1909,6 +1912,23 @@ export default {
|
|
|
1909
1912
|
default: null,
|
|
1910
1913
|
},
|
|
1911
1914
|
|
|
1915
|
+
/**
|
|
1916
|
+
* Name of a registered entity source that reads ONE record (manifest
|
|
1917
|
+
* `config.entitySource`), for a record that is not a register/schema
|
|
1918
|
+
* object. The same registry and the same name as `CnIndexPage`'s
|
|
1919
|
+
* `entitySource`: `organisations` shows the OpenRegister organisation
|
|
1920
|
+
* whose uuid is `objectId`. The record feeds the header, pills, facts,
|
|
1921
|
+
* widgets and the `@objectId` / `@object.*` tokens exactly as a fetched
|
|
1922
|
+
* object does. Wins over `register` + `schema`. Empty (the default)
|
|
1923
|
+
* leaves the page on its object store.
|
|
1924
|
+
*
|
|
1925
|
+
* @type {string}
|
|
1926
|
+
*/
|
|
1927
|
+
entitySource: {
|
|
1928
|
+
type: String,
|
|
1929
|
+
default: '',
|
|
1930
|
+
},
|
|
1931
|
+
|
|
1912
1932
|
/**
|
|
1913
1933
|
* OpenRegister register slug. Pair with `schema` to opt into the
|
|
1914
1934
|
* schema-driven mode: the page fuses the two into an internal
|
|
@@ -2863,6 +2883,10 @@ export default {
|
|
|
2863
2883
|
pillCounts: {},
|
|
2864
2884
|
/** Sequence of the last pill count round, so a late answer does not overwrite a newer one. */
|
|
2865
2885
|
pillCountSeq: 0,
|
|
2886
|
+
/** The labels of the records pills with `resolve` point at, by pill key. */
|
|
2887
|
+
pillRefLabels: {},
|
|
2888
|
+
/** Sequence of the last pill label lookup, so a late answer for another record is dropped. */
|
|
2889
|
+
pillRefSeq: 0,
|
|
2866
2890
|
/** `register/schema/id` the read-state call was last made for, so it is sent once per load. */
|
|
2867
2891
|
readStateSentFor: '',
|
|
2868
2892
|
/** Whether the user is arranging the chosen view (`userLayout`). */
|
|
@@ -3384,7 +3408,16 @@ export default {
|
|
|
3384
3408
|
const pill = pillCountSource(config)
|
|
3385
3409
|
? resolveCountPill(config, this.pillCounts[key], (text) => this.effectiveTranslate(text))
|
|
3386
3410
|
: resolvePill(config, this.resolvedObject)
|
|
3387
|
-
|
|
3411
|
+
if (!pill) {
|
|
3412
|
+
return null
|
|
3413
|
+
}
|
|
3414
|
+
// `resolve`: the referenced record's label replaces the id, and
|
|
3415
|
+
// keys the colour too ("Woo-verzoek", not a uuid).
|
|
3416
|
+
const resolved = this.pillRefLabels[key]
|
|
3417
|
+
if (typeof resolved === 'string' && resolved !== '') {
|
|
3418
|
+
return { ...pill, key, label: this.effectiveTranslate(resolved), colorKey: resolved }
|
|
3419
|
+
}
|
|
3420
|
+
return { ...pill, key, label: this.effectiveTranslate(pill.label) }
|
|
3388
3421
|
})
|
|
3389
3422
|
.filter(Boolean)
|
|
3390
3423
|
},
|
|
@@ -3396,6 +3429,34 @@ export default {
|
|
|
3396
3429
|
* @return {string} The key.
|
|
3397
3430
|
* @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-header-pill-can-be-a-constant-or-a-count
|
|
3398
3431
|
*/
|
|
3432
|
+
/**
|
|
3433
|
+
* What the resolved pills depend on, as one key: a change looks again.
|
|
3434
|
+
*
|
|
3435
|
+
* @return {string}
|
|
3436
|
+
* @spec openspec/changes/detail-pill-resolve/specs/detail-page/spec.md#requirement-a-header-pill-can-show-a-referenced-record-s-label
|
|
3437
|
+
*/
|
|
3438
|
+
pillResolveKey() {
|
|
3439
|
+
return JSON.stringify(this.pillResolveTargets)
|
|
3440
|
+
},
|
|
3441
|
+
|
|
3442
|
+
/**
|
|
3443
|
+
* The pills with `resolve: { register, schema, labelField? }` and the
|
|
3444
|
+
* id each one shows: `[key, register, schema, labelField, id]`. Empty
|
|
3445
|
+
* while the field is empty.
|
|
3446
|
+
*
|
|
3447
|
+
* @return {Array<Array<string>>}
|
|
3448
|
+
* @spec openspec/changes/detail-pill-resolve/specs/detail-page/spec.md#requirement-a-header-pill-can-show-a-referenced-record-s-label
|
|
3449
|
+
*/
|
|
3450
|
+
pillResolveTargets() {
|
|
3451
|
+
return [['type', this.typePill], ['status', this.statusPill]]
|
|
3452
|
+
.filter(([, config]) => config && config.resolve && config.resolve.register && config.resolve.schema && typeof config.field === 'string')
|
|
3453
|
+
.map(([key, config]) => {
|
|
3454
|
+
const pill = resolvePill({ field: config.field }, this.resolvedObject)
|
|
3455
|
+
return pill ? [key, config.resolve.register, config.resolve.schema, config.resolve.labelField || '', pill.colorKey] : null
|
|
3456
|
+
})
|
|
3457
|
+
.filter(Boolean)
|
|
3458
|
+
},
|
|
3459
|
+
|
|
3399
3460
|
pillCountKey() {
|
|
3400
3461
|
return JSON.stringify([this.objectId, pillCountSource(this.typePill), pillCountSource(this.statusPill)])
|
|
3401
3462
|
},
|
|
@@ -3598,6 +3659,9 @@ export default {
|
|
|
3598
3659
|
* @return {boolean}
|
|
3599
3660
|
*/
|
|
3600
3661
|
awaitingObject() {
|
|
3662
|
+
if (this.hasRecordSourceFetch) {
|
|
3663
|
+
return this.objectFetchPending && !this.currentObject
|
|
3664
|
+
}
|
|
3601
3665
|
return this.hasSchemaDrivenFetch
|
|
3602
3666
|
&& this.objectFetchPending
|
|
3603
3667
|
&& typeof this.effectiveObjectStore?.fetchObject === 'function'
|
|
@@ -3671,7 +3735,45 @@ export default {
|
|
|
3671
3735
|
* the fetch path entirely.
|
|
3672
3736
|
*/
|
|
3673
3737
|
hasSchemaDrivenFetch() {
|
|
3674
|
-
return Boolean(this.register && this.schema && this.objectId)
|
|
3738
|
+
return !this.entitySource && Boolean(this.register && this.schema && this.objectId)
|
|
3739
|
+
},
|
|
3740
|
+
|
|
3741
|
+
/**
|
|
3742
|
+
* The record source named by `entitySource`, or null. An unknown name,
|
|
3743
|
+
* or a source that cannot read one record, is null with a warning.
|
|
3744
|
+
*
|
|
3745
|
+
* @return {object|null}
|
|
3746
|
+
*/
|
|
3747
|
+
recordSource() {
|
|
3748
|
+
if (!this.entitySource) {
|
|
3749
|
+
return null
|
|
3750
|
+
}
|
|
3751
|
+
try {
|
|
3752
|
+
return resolveRecordSource(this.entitySource)
|
|
3753
|
+
} catch (err) {
|
|
3754
|
+
// eslint-disable-next-line no-console -- the console is the only channel a host app can read the detail on
|
|
3755
|
+
console.warn('[CnDetailPage] the record source could not start; no record is shown.', err)
|
|
3756
|
+
return null
|
|
3757
|
+
}
|
|
3758
|
+
},
|
|
3759
|
+
|
|
3760
|
+
/**
|
|
3761
|
+
* Whether the page reads its record through a record source.
|
|
3762
|
+
*
|
|
3763
|
+
* @return {boolean}
|
|
3764
|
+
*/
|
|
3765
|
+
hasRecordSourceFetch() {
|
|
3766
|
+
return Boolean(this.recordSource && this.objectId)
|
|
3767
|
+
},
|
|
3768
|
+
|
|
3769
|
+
/**
|
|
3770
|
+
* Whether the page reads its own record, from the object store or a
|
|
3771
|
+
* record source, so Refresh and `cn:page:refresh` have something to do.
|
|
3772
|
+
*
|
|
3773
|
+
* @return {boolean}
|
|
3774
|
+
*/
|
|
3775
|
+
selfFetchesRecord() {
|
|
3776
|
+
return this.hasSchemaDrivenFetch || this.hasRecordSourceFetch
|
|
3675
3777
|
},
|
|
3676
3778
|
|
|
3677
3779
|
/**
|
|
@@ -3886,7 +3988,7 @@ export default {
|
|
|
3886
3988
|
}
|
|
3887
3989
|
// `$.vnode.props`, not `$attrs`: `refresh` is a declared emit, and
|
|
3888
3990
|
// Vue keeps declared emits out of `$attrs`.
|
|
3889
|
-
return Boolean(this.$.vnode.props?.onRefresh) || this.
|
|
3991
|
+
return Boolean(this.$.vnode.props?.onRefresh) || this.selfFetchesRecord
|
|
3890
3992
|
},
|
|
3891
3993
|
|
|
3892
3994
|
/**
|
|
@@ -3930,6 +4032,9 @@ export default {
|
|
|
3930
4032
|
* @return {object|null}
|
|
3931
4033
|
*/
|
|
3932
4034
|
currentObject() {
|
|
4035
|
+
if (this.recordSource) {
|
|
4036
|
+
return this.objectId ? (this.recordSource.record(this.objectId) ?? null) : null
|
|
4037
|
+
}
|
|
3933
4038
|
const store = this.effectiveObjectStore
|
|
3934
4039
|
if (!store) {
|
|
3935
4040
|
return null
|
|
@@ -4605,6 +4710,14 @@ export default {
|
|
|
4605
4710
|
},
|
|
4606
4711
|
},
|
|
4607
4712
|
|
|
4713
|
+
// A pill with `resolve` looks its record up again for another value.
|
|
4714
|
+
pillResolveKey: {
|
|
4715
|
+
immediate: true,
|
|
4716
|
+
handler() {
|
|
4717
|
+
this.refreshPillRefLabels()
|
|
4718
|
+
},
|
|
4719
|
+
},
|
|
4720
|
+
|
|
4608
4721
|
// The header's count pills count again for another record or other lists.
|
|
4609
4722
|
pillCountKey: {
|
|
4610
4723
|
immediate: true,
|
|
@@ -5028,7 +5141,7 @@ export default {
|
|
|
5028
5141
|
// Schema-driven (manifest) detail pages self-fetch — without this
|
|
5029
5142
|
// the Refresh action has no host listener to act on and does
|
|
5030
5143
|
// nothing. Re-fetch the object + schema and spin the action.
|
|
5031
|
-
if (!this.
|
|
5144
|
+
if (!this.selfFetchesRecord) {
|
|
5032
5145
|
return
|
|
5033
5146
|
}
|
|
5034
5147
|
this.internalRefreshing = true
|
|
@@ -5041,6 +5154,42 @@ export default {
|
|
|
5041
5154
|
}
|
|
5042
5155
|
},
|
|
5043
5156
|
|
|
5157
|
+
/**
|
|
5158
|
+
* Look up the records the `resolve` pills point at, through the same
|
|
5159
|
+
* reference-label resolver the index tables use. A failed lookup keeps
|
|
5160
|
+
* the raw value.
|
|
5161
|
+
*
|
|
5162
|
+
* @return {Promise<void>}
|
|
5163
|
+
* @spec openspec/changes/detail-pill-resolve/specs/detail-page/spec.md#requirement-a-header-pill-can-show-a-referenced-record-s-label
|
|
5164
|
+
*/
|
|
5165
|
+
async refreshPillRefLabels() {
|
|
5166
|
+
const targets = this.pillResolveTargets
|
|
5167
|
+
const seq = ++this.pillRefSeq
|
|
5168
|
+
if (targets.length === 0) {
|
|
5169
|
+
this.pillRefLabels = {}
|
|
5170
|
+
return
|
|
5171
|
+
}
|
|
5172
|
+
if (!this._pillRefResolver) {
|
|
5173
|
+
this._pillRefResolver = createRefLabelResolver(() => {
|
|
5174
|
+
try {
|
|
5175
|
+
return this.objectStore || useObjectStore()
|
|
5176
|
+
} catch {
|
|
5177
|
+
return null
|
|
5178
|
+
}
|
|
5179
|
+
})
|
|
5180
|
+
}
|
|
5181
|
+
const labels = {}
|
|
5182
|
+
await Promise.all(targets.map(async ([key, register, schema, labelField, id]) => {
|
|
5183
|
+
const found = await this._pillRefResolver.resolve(register, schema, [id], labelField).catch(() => ({}))
|
|
5184
|
+
if (found && typeof found[id] === 'string') {
|
|
5185
|
+
labels[key] = found[id]
|
|
5186
|
+
}
|
|
5187
|
+
}))
|
|
5188
|
+
if (seq === this.pillRefSeq) {
|
|
5189
|
+
this.pillRefLabels = labels
|
|
5190
|
+
}
|
|
5191
|
+
},
|
|
5192
|
+
|
|
5044
5193
|
/**
|
|
5045
5194
|
* Count the lists behind the header's count pills (`countFrom`): one
|
|
5046
5195
|
* list request each (`_limit=1`, the `total`). A failed count, or a
|
|
@@ -5083,7 +5232,7 @@ export default {
|
|
|
5083
5232
|
*/
|
|
5084
5233
|
onPageRefreshBus(payload) {
|
|
5085
5234
|
this.refreshPillCounts()
|
|
5086
|
-
if (!this.
|
|
5235
|
+
if (!this.selfFetchesRecord) {
|
|
5087
5236
|
return
|
|
5088
5237
|
}
|
|
5089
5238
|
if (this.internalRefreshing) {
|
|
@@ -5308,6 +5457,10 @@ export default {
|
|
|
5308
5457
|
},
|
|
5309
5458
|
|
|
5310
5459
|
async fetchObjectIfNeeded() {
|
|
5460
|
+
if (this.recordSource) {
|
|
5461
|
+
await this.fetchSourceRecord()
|
|
5462
|
+
return
|
|
5463
|
+
}
|
|
5311
5464
|
// Create archetype: no object to fetch, but the create form needs
|
|
5312
5465
|
// the schema — register the type and fetch its schema, then stop.
|
|
5313
5466
|
if (!this.hasSchemaDrivenFetch) {
|
|
@@ -5371,6 +5524,37 @@ export default {
|
|
|
5371
5524
|
}
|
|
5372
5525
|
},
|
|
5373
5526
|
|
|
5527
|
+
/**
|
|
5528
|
+
* Read the record through the record source. A refusal (403) or a
|
|
5529
|
+
* missing record (404) shows the not-found state; which records the
|
|
5530
|
+
* viewer may read is the server's decision.
|
|
5531
|
+
*
|
|
5532
|
+
* @return {Promise<void>}
|
|
5533
|
+
*/
|
|
5534
|
+
async fetchSourceRecord() {
|
|
5535
|
+
const source = this.recordSource
|
|
5536
|
+
const objectId = this.objectId
|
|
5537
|
+
if (!objectId) {
|
|
5538
|
+
this.objectFetchPending = false
|
|
5539
|
+
return
|
|
5540
|
+
}
|
|
5541
|
+
this.objectFetchPending = true
|
|
5542
|
+
this.objectNotFound = false
|
|
5543
|
+
try {
|
|
5544
|
+
await source.loadRecord(String(objectId))
|
|
5545
|
+
} catch (err) {
|
|
5546
|
+
// eslint-disable-next-line no-console -- the failure is already handled; the console is the only channel a host app can read the detail on
|
|
5547
|
+
console.error('[CnDetailPage] record source fetch failed:', err)
|
|
5548
|
+
} finally {
|
|
5549
|
+
// A newer fetch for another id owns the flags now.
|
|
5550
|
+
if (objectId === this.objectId) {
|
|
5551
|
+
const status = typeof source.recordError === 'function' ? source.recordError(String(objectId)) : null
|
|
5552
|
+
this.objectNotFound = !this.currentObject && (status === 403 || status === 404)
|
|
5553
|
+
this.objectFetchPending = false
|
|
5554
|
+
}
|
|
5555
|
+
}
|
|
5556
|
+
},
|
|
5557
|
+
|
|
5374
5558
|
/**
|
|
5375
5559
|
* Whether a grid-layout item resolves to an integration-typed
|
|
5376
5560
|
* widget — `def.type === 'integration'` with a string
|
|
@@ -223,8 +223,8 @@ export default {
|
|
|
223
223
|
/**
|
|
224
224
|
* Board look options, passed to CnFeaturesBoard: `layout` (`cards` or
|
|
225
225
|
* `rows`), `tabs`, `filter` (`chips` or `segmented`), `statusLabels`,
|
|
226
|
-
* `next`, `issueLabels`, `subtitle`, `notice`, `
|
|
227
|
-
* and `suggestButton`. Ignored in the default look.
|
|
226
|
+
* `next`, `issueLabels`, `subtitle`, `notice`, `noticeLink`,
|
|
227
|
+
* `summaries`, `documentationButton` and `suggestButton`. Ignored in the default look.
|
|
228
228
|
*
|
|
229
229
|
* @type {object}
|
|
230
230
|
*/
|
|
@@ -460,7 +460,7 @@ export default {
|
|
|
460
460
|
* @return {object}
|
|
461
461
|
*/
|
|
462
462
|
boardOptions() {
|
|
463
|
-
const keys = ['layout', 'tabs', 'filter', 'statusLabels', 'next', 'issueLabels', 'subtitle', 'notice', 'documentationButton', 'suggestButton']
|
|
463
|
+
const keys = ['layout', 'tabs', 'filter', 'statusLabels', 'next', 'issueLabels', 'subtitle', 'notice', 'noticeLink', 'summaries', 'documentationButton', 'suggestButton']
|
|
464
464
|
const options = {}
|
|
465
465
|
for (const key of keys) {
|
|
466
466
|
if (this.board && this.board[key] !== undefined && this.board[key] !== null) {
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
data-testid="cn-features-board-feature">
|
|
196
196
|
<span class="cn-features-board__row-text">
|
|
197
197
|
<span class="cn-features-board__row-title">{{ feature.title }}</span>
|
|
198
|
-
<span v-if="feature.summary" class="cn-features-board__row-summary">{{ feature.summary }}</span>
|
|
198
|
+
<span v-if="summaries && feature.summary" class="cn-features-board__row-summary">{{ feature.summary }}</span>
|
|
199
199
|
</span>
|
|
200
200
|
<CnStatusBadge
|
|
201
201
|
:label="statusText(feature)"
|
|
@@ -271,7 +271,16 @@
|
|
|
271
271
|
type="info"
|
|
272
272
|
class="cn-features-board__notice"
|
|
273
273
|
data-testid="cn-features-board-notice">
|
|
274
|
-
|
|
274
|
+
<template v-if="noticeParts.link">
|
|
275
|
+
{{ noticeParts.before }}<a
|
|
276
|
+
:href="noticeParts.link.href"
|
|
277
|
+
target="_blank"
|
|
278
|
+
rel="noopener noreferrer"
|
|
279
|
+
data-testid="cn-features-board-notice-link">{{ noticeParts.link.text }}</a>{{ noticeParts.after }}
|
|
280
|
+
</template>
|
|
281
|
+
<template v-else>
|
|
282
|
+
{{ noticeParts.before }}
|
|
283
|
+
</template>
|
|
275
284
|
</NcNoteCard>
|
|
276
285
|
</template>
|
|
277
286
|
</div>
|
|
@@ -397,6 +406,10 @@ export default {
|
|
|
397
406
|
suggestUrl: { type: String, default: '' },
|
|
398
407
|
/** Show the feature-request form as the header's primary button; without it, it is an entry in the Actions menu. */
|
|
399
408
|
suggestButton: { type: Boolean, default: true },
|
|
409
|
+
/** In the `rows` layout, draw each feature's summary under its title; false shows the title and status only. */
|
|
410
|
+
summaries: { type: Boolean, default: true },
|
|
411
|
+
/** A link inside the notice: `{ text, href }`; the first occurrence of `text` in the translated notice opens `href` in a new tab. Only http(s) and site-relative hrefs link. */
|
|
412
|
+
noticeLink: { type: Object, default: null },
|
|
400
413
|
/** Target of the OpenBuilt entry in the Actions menu. */
|
|
401
414
|
openbuiltUrl: { type: String, default: '' },
|
|
402
415
|
/** Target of the AI skills entry in the Actions menu. */
|
|
@@ -421,8 +434,10 @@ export default {
|
|
|
421
434
|
return readerLanguage()
|
|
422
435
|
},
|
|
423
436
|
|
|
437
|
+
/** The groups, their captions through the app's translate function. */
|
|
424
438
|
groups() {
|
|
425
439
|
return groupFeatures(this.features, this.categories, this.language)
|
|
440
|
+
.map((group) => ({ ...group, label: group.label ? this.tr(group.label) : '' }))
|
|
426
441
|
},
|
|
427
442
|
|
|
428
443
|
allFeatures() {
|
|
@@ -466,6 +481,27 @@ export default {
|
|
|
466
481
|
return this.title ? this.tr(this.title) : t('nextcloud-vue', 'Features and roadmap')
|
|
467
482
|
},
|
|
468
483
|
|
|
484
|
+
/**
|
|
485
|
+
* The notice split around its link: the text before, the link and the
|
|
486
|
+
* text after. Without a usable `noticeLink` (no match in the
|
|
487
|
+
* translated notice, or an href that is not http(s) or site-relative)
|
|
488
|
+
* `before` is the whole notice.
|
|
489
|
+
*
|
|
490
|
+
* @return {{before: string, link: ({text: string, href: string}|null), after: string}} The parts.
|
|
491
|
+
*/
|
|
492
|
+
noticeParts() {
|
|
493
|
+
const text = this.notice ? this.tr(this.notice) : ''
|
|
494
|
+
const link = this.noticeLink
|
|
495
|
+
const label = link && typeof link.text === 'string' ? this.tr(link.text) : ''
|
|
496
|
+
const href = link && typeof link.href === 'string' ? link.href.trim() : ''
|
|
497
|
+
const index = label ? text.indexOf(label) : -1
|
|
498
|
+
const allowed = (/^https?:\/\//i.test(href) || href.startsWith('/')) && safeHref(href) === href
|
|
499
|
+
if (index === -1 || !allowed) {
|
|
500
|
+
return { before: text, link: null, after: '' }
|
|
501
|
+
}
|
|
502
|
+
return { before: text.slice(0, index), link: { text: label, href }, after: text.slice(index + label.length) }
|
|
503
|
+
},
|
|
504
|
+
|
|
469
505
|
resolvedSubtitle() {
|
|
470
506
|
if (!this.subtitle) {
|
|
471
507
|
return ''
|
|
@@ -535,6 +535,34 @@ What the source brings, all overridable by the manifest:
|
|
|
535
535
|
- Scope stays the endpoint's session decision. The loader drops any
|
|
536
536
|
user-naming key, so a manifest cannot ask for someone else's inbox.
|
|
537
537
|
|
|
538
|
+
### The `organisations` source: OpenRegister organisations
|
|
539
|
+
|
|
540
|
+
`entitySource: "organisations"` lists the OpenRegister organisations the
|
|
541
|
+
viewer belongs to, from `GET /apps/openregister/api/organisations`. An
|
|
542
|
+
organisation is not a register/schema object, so self-fetch cannot reach it.
|
|
543
|
+
|
|
544
|
+
```json
|
|
545
|
+
{
|
|
546
|
+
"id": "organisations",
|
|
547
|
+
"route": "/organisations",
|
|
548
|
+
"type": "index",
|
|
549
|
+
"config": { "entitySource": "organisations", "rowRoute": "organisation-detail" }
|
|
550
|
+
}
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
- Columns: name, status (a pill with the lifecycle state in words), type and
|
|
554
|
+
created. A manifest `columns` list still wins; every field of the
|
|
555
|
+
organisation can be a column (`slug`, `contactEmail`, `kvk`, ...).
|
|
556
|
+
- A row's `id` is the organisation's uuid, so `rowRoute` pushes
|
|
557
|
+
`{ name: rowRoute, params: { id: <uuid> } }`.
|
|
558
|
+
- No Add, Copy or Delete: creating an organisation is not this page's object
|
|
559
|
+
form, and OpenRegister has no delete for one.
|
|
560
|
+
- Which organisations are listed is OpenRegister's decision from the
|
|
561
|
+
session. The endpoint takes no parameters, so no config key reaches it.
|
|
562
|
+
|
|
563
|
+
The same name works on a `type: "detail"` page, which then shows ONE
|
|
564
|
+
organisation (see CnDetailPage, "Records that are not objects").
|
|
565
|
+
|
|
538
566
|
### Search fields on a named source
|
|
539
567
|
|
|
540
568
|
Available from the release that follows 3.2.0. An app on 3.2.0 or older ignores `sidebar.fields`, so bump the dependency before declaring them.
|