@conduction/nextcloud-vue 2.111.0 → 2.113.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/CnActionButtons/CnActionButtons.vue.js +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +4 -6
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js +3 -2
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +4 -2
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue.js +1 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +3 -2
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js +2 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +54 -5
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +16 -2
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue.js +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +10 -0
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +2 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue.js +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +22 -2
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +17 -3
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
- 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/CnChoiceCards/CnChoiceCards.vue.js +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +18 -3
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +52 -13
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +31 -3
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +48 -11
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +160 -97
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +2 -2
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +222 -15
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +24 -7
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue.js +1 -1
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js +21 -1
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue2.js.map +1 -1
- package/dist/esm/components/CnNcDashboardList/CnNcDashboardList.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +36 -2
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +44 -5
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -0
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js.map +1 -1
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +22 -2
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js +36 -16
- package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue.js +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js +29 -12
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js +3 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +1 -1
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -1
- package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -1
- package/dist/esm/composables/useRefLabels.js +26 -6
- package/dist/esm/composables/useRefLabels.js.map +1 -1
- package/dist/esm/integrations/registry.js +4 -0
- package/dist/esm/integrations/registry.js.map +1 -1
- package/dist/esm/l10n/en.json.js +3 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +3 -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 +680 -449
- package/dist/esm/store/plugins/liveUpdates.js +3 -1
- package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
- package/dist/esm/store/plugins/logs.js +2 -1
- package/dist/esm/store/plugins/logs.js.map +1 -1
- package/dist/esm/utils/actionLink.js +104 -1
- package/dist/esm/utils/actionLink.js.map +1 -1
- package/dist/esm/utils/detailActionModel.js +28 -1
- package/dist/esm/utils/detailActionModel.js.map +1 -1
- package/dist/esm/utils/fillTemplate.js +13 -5
- package/dist/esm/utils/fillTemplate.js.map +1 -1
- package/dist/esm/utils/headerFieldChips.js +21 -2
- package/dist/esm/utils/headerFieldChips.js.map +1 -1
- package/dist/esm/utils/mapList.js +26 -11
- package/dist/esm/utils/mapList.js.map +1 -1
- package/dist/esm/utils/savedViewMenu.js +12 -5
- package/dist/esm/utils/savedViewMenu.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +2800 -1933
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +680 -449
- package/l10n/en.json +3 -1
- package/l10n/nl.json +3 -1
- package/package.json +1 -1
- package/src/components/CnActionButtons/CnActionButtons.vue +4 -6
- package/src/components/CnActionDialog/CnActionDialog.vue +32 -9
- package/src/components/CnAssignDialog/CnAssignDialog.vue +4 -2
- package/src/components/CnBoardView/CnBoardView.vue +58 -5
- package/src/components/CnCardGrid/CnCardGrid.md +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +11 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +27 -3
- package/src/components/CnChartWidget/CnChartWidget.vue +6 -0
- package/src/components/CnChoiceCards/CnChoiceCards.vue +0 -3
- package/src/components/CnDashboardPage/CnDashboardPage.vue +20 -5
- package/src/components/CnDetailPage/CnDetailPage.vue +62 -14
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +100 -5
- package/src/components/CnFormDialog/CnFormDialog.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +233 -18
- package/src/components/CnNcDashboardList/CnNcDashboardList.vue +21 -1
- package/src/components/CnObjectCard/CnObjectCard.md +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +51 -5
- package/src/components/CnPartyPicker/CnPartyPicker.vue +62 -16
- package/src/components/CnReportsPage/CnReportsPage.vue +31 -12
- package/src/components/CnSearchSelect/CnSearchSelect.vue +8 -0
- package/src/composables/useRefLabels.js +25 -5
- package/src/css/badge.css +15 -13
- package/src/css/board.css +0 -1
- package/src/css/dialog.css +53 -3
- package/src/css/index-page.css +26 -1
- package/src/css/look-board-detail.css +36 -15
- package/src/css/look-board-index.css +67 -15
- package/src/css/look-board.css +32 -18
- package/src/integrations/registry.js +4 -0
- package/src/schemas/app-manifest-v2.schema.json +61 -13
- package/src/store/plugins/liveUpdates.js +3 -1
- package/src/store/plugins/logs.js +2 -1
- package/src/utils/actionLink.js +103 -0
- package/src/utils/detailActionModel.js +27 -0
- package/src/utils/fillTemplate.js +13 -5
- package/src/utils/headerFieldChips.js +21 -2
- package/src/utils/mapList.js +26 -11
- package/src/utils/savedViewMenu.js +12 -5
- package/src/utils/validateManifestV2.compiled.js +3 -3
|
@@ -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.135.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -1212,6 +1212,7 @@
|
|
|
1212
1212
|
]
|
|
1213
1213
|
},
|
|
1214
1214
|
"pill": { "type": "string", "description": "The field shown as a status pill." },
|
|
1215
|
+
"resolve": { "type": "object", "additionalProperties": false, "description": "pill-color-field: the `pill` field holds the id of a record; the pill takes the tone that record's colour maps onto and shows its icon. Register and schema default to the field's reference.", "properties": { "register": { "type": "string", "minLength": 1 }, "schema": { "type": "string", "minLength": 1 }, "colorField": { "type": "string", "minLength": 1 }, "iconField": { "type": "string", "minLength": 1 } } },
|
|
1215
1216
|
"pillColors": {
|
|
1216
1217
|
"type": "object",
|
|
1217
1218
|
"description": "Pill value to badge variant. A value without an entry takes the default variant.",
|
|
@@ -1378,7 +1379,7 @@
|
|
|
1378
1379
|
"filter": { "type": "object", "additionalProperties": true }
|
|
1379
1380
|
}
|
|
1380
1381
|
},
|
|
1381
|
-
"resolve": { "type": "object", "additionalProperties": false, "required": ["register", "schema"], "description": "detail-pill-resolve: the field holds the id of a record of this register and schema; the pill shows that record's `labelField` (else its title or name) and keys `colorMap` by it. The raw value shows while the lookup runs and when it finds nothing.", "properties": { "register": { "type": "string", "minLength": 1 }, "schema": { "type": "string", "minLength": 1 }, "labelField": { "type": "string", "minLength": 1 } } },
|
|
1382
|
+
"resolve": { "type": "object", "additionalProperties": false, "required": ["register", "schema"], "description": "detail-pill-resolve: the field holds the id of a record of this register and schema; the pill shows that record's `labelField` (else its title or name) and keys `colorMap` by it. The raw value shows while the lookup runs and when it finds nothing.", "properties": { "register": { "type": "string", "minLength": 1 }, "schema": { "type": "string", "minLength": 1 }, "labelField": { "type": "string", "minLength": 1 }, "colorField": { "type": "string", "minLength": 1, "description": "pill-color-field: the referenced record's property holding a colour name (an NL Design System hue such as `red` or `blue-light`, or a badge role); the pill takes the tone it maps onto." }, "iconField": { "type": "string", "minLength": 1, "description": "pill-color-field: the referenced record's property holding a registered icon name, shown before the label." } } },
|
|
1382
1383
|
"countLabel": {
|
|
1383
1384
|
"type": "object",
|
|
1384
1385
|
"additionalProperties": false,
|
|
@@ -3103,10 +3104,17 @@
|
|
|
3103
3104
|
}
|
|
3104
3105
|
},
|
|
3105
3106
|
"action": {
|
|
3107
|
+
"description": "A page action: the fields of $defs/actionFields, nothing else, with children that are actions too. Row, bulk and navigation actions use it; header actions use $defs/headerAction.",
|
|
3108
|
+
"$ref": "#/$defs/actionFields",
|
|
3109
|
+
"properties": {
|
|
3110
|
+
"children": { "items": { "$ref": "#/$defs/action" } }
|
|
3111
|
+
},
|
|
3112
|
+
"unevaluatedProperties": false
|
|
3113
|
+
},
|
|
3114
|
+
"actionFields": {
|
|
3106
3115
|
"type": "object",
|
|
3107
3116
|
"required": ["id", "label"],
|
|
3108
|
-
"
|
|
3109
|
-
"description": "A typed page action with a discriminator field. The type field determines behaviour: handler (registry function), open-modal (modal by id), open-page (named route), navigate (URL), object-op (declarative OpenRegister mutation via useObjectStore), export (opens the shared CnMassExportDialog export launcher), open-form (schema-driven create dialog — CnActionButtons), refresh (bumps the cn:page:refresh signal), api-call (POST/PUT an app endpoint + toast + refresh — CnActionButtons/dispatchAction), agent (run a governed hermiq agent), run-node (invoke one OpenRegister flow node against the page object, config collected via a dialog sourced from the node's own declared form — CnActionButtons/dispatchAction, or-flow-run-node; distinct from the UNRELATED setup-wizard step type of a similar name, 'run-action'), toggle (two-way state button — CnActionButtons only, NOT dispatchable). When type is omitted, it defaults to 'handler' (back-compatibility with v1.3.0 action declarations). The Ajv instance is compiled with useDefaults: true so the default is applied during validation.",
|
|
3117
|
+
"description": "The fields of an action, shared by $defs/action and the non-link $defs/headerAction, which close it and constrain `children`. A typed page action with a discriminator field. The type field determines behaviour: handler (registry function), open-modal (modal by id), open-page (named route), navigate (URL), object-op (declarative OpenRegister mutation via useObjectStore), export (opens the shared CnMassExportDialog export launcher), open-form (schema-driven create dialog — CnActionButtons), refresh (bumps the cn:page:refresh signal), api-call (POST/PUT an app endpoint + toast + refresh — CnActionButtons/dispatchAction), agent (run a governed hermiq agent), run-node (invoke one OpenRegister flow node against the page object, config collected via a dialog sourced from the node's own declared form — CnActionButtons/dispatchAction, or-flow-run-node; distinct from the UNRELATED setup-wizard step type of a similar name, 'run-action'), toggle (two-way state button — CnActionButtons only, NOT dispatchable). When type is omitted, it defaults to 'handler' (back-compatibility with v1.3.0 action declarations). The Ajv instance is compiled with useDefaults: true so the default is applied during validation.",
|
|
3110
3118
|
"properties": {
|
|
3111
3119
|
"id": {
|
|
3112
3120
|
"type": "string",
|
|
@@ -3364,8 +3372,7 @@
|
|
|
3364
3372
|
},
|
|
3365
3373
|
"children": {
|
|
3366
3374
|
"type": "array",
|
|
3367
|
-
"description": "Sub-actions, rendered by CnActionButtons as this action's OWN chevron dropdown beside its button — deliberately not folded into the page's single overflow menu, because NcActions cannot nest and a list that grows per record (app versions, say) would swamp it. Children take the same shape as any action, `visibleWhen` included, and an action carrying them consumes none of the `inline` slots. The parent itself still renders its own button beside the chevron (`type` defaults to 'handler' when omitted), so a parent meant only to group its children needs a `handler` that does something meaningful — an untyped, handler-less parent renders a button that dispatches to nothing."
|
|
3368
|
-
"items": { "$ref": "#/$defs/action" }
|
|
3375
|
+
"description": "Sub-actions, rendered by CnActionButtons as this action's OWN chevron dropdown beside its button — deliberately not folded into the page's single overflow menu, because NcActions cannot nest and a list that grows per record (app versions, say) would swamp it. Children take the same shape as any action, `visibleWhen` included, and an action carrying them consumes none of the `inline` slots. The parent itself still renders its own button beside the chevron (`type` defaults to 'handler' when omitted), so a parent meant only to group its children needs a `handler` that does something meaningful — an untyped, handler-less parent renders a button that dispatches to nothing. Each child is an action; a header action's child is a header action, so it may be a link with `href`."
|
|
3369
3376
|
},
|
|
3370
3377
|
"childrenLabel": {
|
|
3371
3378
|
"type": "string",
|
|
@@ -3437,6 +3444,42 @@
|
|
|
3437
3444
|
}
|
|
3438
3445
|
]
|
|
3439
3446
|
},
|
|
3447
|
+
"headerAction": {
|
|
3448
|
+
"description": "A page header action (`headerActions[]`, a detail page's `quickActions[]` / `primaryActionByStage` entry, and their `children[]`): an action ($ref action), or with `href` a link ($ref headerLinkAction). Only header actions take `href`; row, bulk and navigation actions do not (2.135.0).",
|
|
3449
|
+
"if": { "type": "object", "required": ["href"] },
|
|
3450
|
+
"then": { "$ref": "#/$defs/headerLinkAction" },
|
|
3451
|
+
"else": {
|
|
3452
|
+
"$ref": "#/$defs/actionFields",
|
|
3453
|
+
"properties": {
|
|
3454
|
+
"children": { "items": { "$ref": "#/$defs/headerAction" } }
|
|
3455
|
+
},
|
|
3456
|
+
"unevaluatedProperties": false
|
|
3457
|
+
}
|
|
3458
|
+
},
|
|
3459
|
+
"headerLinkAction": {
|
|
3460
|
+
"type": "object",
|
|
3461
|
+
"required": ["id", "label", "href"],
|
|
3462
|
+
"additionalProperties": false,
|
|
3463
|
+
"description": "A header action that is a real link (`<a href>`), so it can be middle-clicked, opened in a new tab, copied and bookmarked. It only navigates: it runs no handler and emits no `header-action`, so it carries no `handler` and no other `type` (2.135.0).",
|
|
3464
|
+
"properties": {
|
|
3465
|
+
"id": { "type": "string", "description": "Unique action identifier within the page's actions array." },
|
|
3466
|
+
"label": { "type": "string", "description": "i18n key resolved by the consuming app's t() function at render time." },
|
|
3467
|
+
"href": {
|
|
3468
|
+
"type": "string",
|
|
3469
|
+
"pattern": "^(https?://|mailto:|tel:|/)",
|
|
3470
|
+
"description": "The link. An absolute `http(s)`, `mailto:` or `tel:` URL and a protocol-relative `//host/...` URL are used as written. A path starting with a Nextcloud entry point (`/index.php/`, `/remote.php/`, `/public.php/`, `/ocs/`) is prefixed with the instance root unless it already starts with it; any other instance path (leading `/`, e.g. `/apps/integriq/connections`) goes through `generateUrl()`."
|
|
3471
|
+
},
|
|
3472
|
+
"target": { "type": "string", "enum": ["_blank", "_self"], "default": "_self", "description": "`_blank` opens the link in a new tab; `_self` opens it in place." },
|
|
3473
|
+
"type": { "const": "handler", "description": "Only the default; a link has no other behaviour." },
|
|
3474
|
+
"icon": { "type": "string", "description": "Optional icon: an MDI icon name or a CSS icon class, as on any action." },
|
|
3475
|
+
"variant": { "type": "string", "description": "CnActionButtons button variant, as on any action." },
|
|
3476
|
+
"group": { "type": "string", "minLength": 1, "description": "Detail page header actions only: the overflow menu group, as on any action." },
|
|
3477
|
+
"adminOnly": { "type": "boolean", "default": false, "description": "Detail page header actions only: show the link to instance admins alone, as on any action." },
|
|
3478
|
+
"visibleWhen": { "$ref": "#/$defs/visibleWhen", "description": "An optional predicate gating whether the link renders, as on any action." },
|
|
3479
|
+
"children": { "type": "array", "description": "Sub-actions in the link's own chevron dropdown, as on any action.", "items": { "$ref": "#/$defs/headerAction" } },
|
|
3480
|
+
"childrenLabel": { "type": "string", "description": "Name of the sub-action chevron, as on any action." }
|
|
3481
|
+
}
|
|
3482
|
+
},
|
|
3440
3483
|
"fieldValidation": {
|
|
3441
3484
|
"type": "object",
|
|
3442
3485
|
"additionalProperties": false,
|
|
@@ -4353,7 +4396,7 @@
|
|
|
4353
4396
|
"additionalProperties": {
|
|
4354
4397
|
"oneOf": [
|
|
4355
4398
|
{ "type": "string", "minLength": 1 },
|
|
4356
|
-
{ "$ref": "#/$defs/
|
|
4399
|
+
{ "$ref": "#/$defs/headerAction" }
|
|
4357
4400
|
]
|
|
4358
4401
|
}
|
|
4359
4402
|
},
|
|
@@ -4364,7 +4407,7 @@
|
|
|
4364
4407
|
"items": {
|
|
4365
4408
|
"oneOf": [
|
|
4366
4409
|
{ "type": "string", "minLength": 1 },
|
|
4367
|
-
{ "$ref": "#/$defs/
|
|
4410
|
+
{ "$ref": "#/$defs/headerAction" }
|
|
4368
4411
|
]
|
|
4369
4412
|
}
|
|
4370
4413
|
},
|
|
@@ -4488,8 +4531,8 @@
|
|
|
4488
4531
|
},
|
|
4489
4532
|
"headerActions": {
|
|
4490
4533
|
"type": "array",
|
|
4491
|
-
"description": "Page-level header actions rendered inside CnActionsBar's overflow dropdown (between the built-in Refresh action and the `#action-items` slot). On a `type:'detail'` page the same entries render in the header's Actions menu, above the Request-a-feature / Report-a-bug / Documentation trio. Same shape as the row-level `actions[]` ($ref action), but handlers receive NO row context — they are page-level. Reserved ids (`refresh`, `import`, `export`, `copy`, `delete`) are dropped at render time to avoid shadowing built-ins. See manifest-icons-and-page-actions for the dispatch semantics.",
|
|
4492
|
-
"items": { "$ref": "#/$defs/
|
|
4534
|
+
"description": "Page-level header actions rendered inside CnActionsBar's overflow dropdown (between the built-in Refresh action and the `#action-items` slot). On a `type:'detail'` page the same entries render in the header's Actions menu, above the Request-a-feature / Report-a-bug / Documentation trio. Same shape as the row-level `actions[]` ($ref action), but handlers receive NO row context — they are page-level. Reserved ids (`refresh`, `import`, `export`, `copy`, `delete`) are dropped at render time to avoid shadowing built-ins. An entry with `href` (instead of `handler`) renders as a real link to that URL and is never dispatched ($ref headerAction). See manifest-icons-and-page-actions for the dispatch semantics.",
|
|
4535
|
+
"items": { "$ref": "#/$defs/headerAction" }
|
|
4493
4536
|
},
|
|
4494
4537
|
"bulkActions": {
|
|
4495
4538
|
"type": "array",
|
|
@@ -4694,6 +4737,11 @@
|
|
|
4694
4737
|
"type": "string",
|
|
4695
4738
|
"description": "One label for the control's label filter."
|
|
4696
4739
|
},
|
|
4740
|
+
"icon": {
|
|
4741
|
+
"type": "string",
|
|
4742
|
+
"minLength": 1,
|
|
4743
|
+
"description": "pill-color-field: a registered icon name the view's My views menu entry shows instead of the filter icon (a case type's own icon, say)."
|
|
4744
|
+
},
|
|
4697
4745
|
"overrides": {
|
|
4698
4746
|
"type": "object",
|
|
4699
4747
|
"additionalProperties": false,
|
|
@@ -5339,14 +5387,14 @@
|
|
|
5339
5387
|
"additionalProperties": false,
|
|
5340
5388
|
"description": "type='index', board look: the record layout of each card in the cards view (board-record-card-layout): a pill and the number on the head row, the title, a meta line, and a footer with a pill and a date. Each part names a field (a dot path) and is left out while it is empty; a pill maps values to a badge variant (`colorMap`, ignoring case) and to shown text (`labels`). Maps to CnIndexPage `cardLayout`.",
|
|
5341
5389
|
"properties": {
|
|
5342
|
-
"headPill": { "type": "object", "additionalProperties": false, "required": ["field"], "properties": { "field": { "type": "string", "minLength": 1 }, "colorMap": { "type": "object", "additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] } }, "labels": { "type": "object", "additionalProperties": { "type": "string" } }, "variant": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] }, "due": { "type": "object", "description": "Read the field as a due date: overdue gives the error variant, within `soonWithin` days the warning one; any other date draws no pill.", "properties": { "soonWithin": { "type": "number", "minimum": 0 } } }, "variantWhen": { "type": "array", "description": "Rules on the days left until the field's date, `{ op, value, variant }`; the first match gives the variant, no match draws no pill.", "items": { "type": "object", "required": ["op", "value", "variant"], "properties": { "op": { "type": "string" }, "value": { "type": "number" }, "variant": { "type": "string" } } } } } },
|
|
5390
|
+
"headPill": { "type": "object", "additionalProperties": false, "required": ["field"], "properties": { "field": { "type": "string", "minLength": 1 }, "resolve": { "type": "object", "additionalProperties": false, "description": "pill-color-field: the field holds the id of a record; the pill takes the tone that record's colour maps onto and shows its icon. Register and schema default to the field's reference.", "properties": { "register": { "type": "string", "minLength": 1 }, "schema": { "type": "string", "minLength": 1 }, "colorField": { "type": "string", "minLength": 1 }, "iconField": { "type": "string", "minLength": 1 } } }, "colorMap": { "type": "object", "additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] } }, "labels": { "type": "object", "additionalProperties": { "type": "string" } }, "variant": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] }, "due": { "type": "object", "description": "Read the field as a due date: overdue gives the error variant, within `soonWithin` days the warning one; any other date draws no pill.", "properties": { "soonWithin": { "type": "number", "minimum": 0 } } }, "variantWhen": { "type": "array", "description": "Rules on the days left until the field's date, `{ op, value, variant }`; the first match gives the variant, no match draws no pill.", "items": { "type": "object", "required": ["op", "value", "variant"], "properties": { "op": { "type": "string" }, "value": { "type": "number" }, "variant": { "type": "string" } } } } } },
|
|
5343
5391
|
"number": { "type": "string", "minLength": 1 },
|
|
5344
5392
|
"title": { "type": "string", "minLength": 1 },
|
|
5345
5393
|
"meta": { "type": "array", "description": "The meta line's parts: a field name reads through the page column it names; `{ field, formatter?, formatterOptions? }` reads as plain text through its own formatter (card-layout-part-formatter).", "items": { "oneOf": [ { "type": "string", "minLength": 1 }, { "type": "object", "additionalProperties": false, "required": ["field"], "properties": { "field": { "type": "string", "minLength": 1 }, "formatter": { "type": "string", "minLength": 1 }, "formatterOptions": { "type": "object" } } } ] } },
|
|
5346
|
-
"footerPill": { "type": "object", "additionalProperties": false, "required": ["field"], "properties": { "field": { "type": "string", "minLength": 1 }, "colorMap": { "type": "object", "additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] } }, "labels": { "type": "object", "additionalProperties": { "type": "string" } }, "variant": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] }, "due": { "type": "object", "description": "Read the field as a due date: overdue gives the error variant, within `soonWithin` days the warning one; any other date draws no pill.", "properties": { "soonWithin": { "type": "number", "minimum": 0 } } }, "variantWhen": { "type": "array", "description": "Rules on the days left until the field's date, `{ op, value, variant }`; the first match gives the variant, no match draws no pill.", "items": { "type": "object", "required": ["op", "value", "variant"], "properties": { "op": { "type": "string" }, "value": { "type": "number" }, "variant": { "type": "string" } } } } } },
|
|
5394
|
+
"footerPill": { "type": "object", "additionalProperties": false, "required": ["field"], "properties": { "field": { "type": "string", "minLength": 1 }, "resolve": { "type": "object", "additionalProperties": false, "description": "pill-color-field: the field holds the id of a record; the pill takes the tone that record's colour maps onto and shows its icon. Register and schema default to the field's reference.", "properties": { "register": { "type": "string", "minLength": 1 }, "schema": { "type": "string", "minLength": 1 }, "colorField": { "type": "string", "minLength": 1 }, "iconField": { "type": "string", "minLength": 1 } } }, "colorMap": { "type": "object", "additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] } }, "labels": { "type": "object", "additionalProperties": { "type": "string" } }, "variant": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] }, "due": { "type": "object", "description": "Read the field as a due date: overdue gives the error variant, within `soonWithin` days the warning one; any other date draws no pill.", "properties": { "soonWithin": { "type": "number", "minimum": 0 } } }, "variantWhen": { "type": "array", "description": "Rules on the days left until the field's date, `{ op, value, variant }`; the first match gives the variant, no match draws no pill.", "items": { "type": "object", "required": ["op", "value", "variant"], "properties": { "op": { "type": "string" }, "value": { "type": "number" }, "variant": { "type": "string" } } } } } },
|
|
5347
5395
|
"footerDate": { "type": "object", "additionalProperties": false, "required": ["field"], "properties": { "field": { "type": "string", "minLength": 1 }, "label": { "type": "string", "minLength": 1 }, "overdueVariant": { "type": "string", "enum": ["error", "warning"], "description": "The date's tone once it has passed (2.132.0)." } } },
|
|
5348
5396
|
"description": { "description": "A line under the meta line: a field, or `{ field, maxLength? }` cut at maxLength characters (140 by default) with an ellipsis (2.132.0).", "anyOf": [{ "type": "string", "minLength": 1 }, { "type": "object", "additionalProperties": false, "required": ["field"], "properties": { "field": { "type": "string", "minLength": 1 }, "maxLength": { "type": "integer", "minimum": 1 } } }] },
|
|
5349
|
-
"chips": { "type": "array", "description": "Pills under the description, one per entry whose field holds a value; the text reads through the entry's formatter, `colorMap` and `labels` work as on a pill (2.132.0).", "items": { "anyOf": [{ "type": "string", "minLength": 1 }, { "type": "object", "additionalProperties": false, "required": ["field"], "properties": { "field": { "type": "string", "minLength": 1 }, "formatter": { "type": "string", "minLength": 1 }, "formatterOptions": { "type": "object" }, "colorMap": { "type": "object", "additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] } }, "labels": { "type": "object", "additionalProperties": { "type": "string" } }, "variant": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] } } }] } }
|
|
5397
|
+
"chips": { "type": "array", "description": "Pills under the description, one per entry whose field holds a value; the text reads through the entry's formatter, `colorMap` and `labels` work as on a pill (2.132.0).", "items": { "anyOf": [{ "type": "string", "minLength": 1 }, { "type": "object", "additionalProperties": false, "required": ["field"], "properties": { "field": { "type": "string", "minLength": 1 }, "resolve": { "type": "object", "additionalProperties": false, "description": "pill-color-field: the field holds the id of a record; the pill takes the tone that record's colour maps onto and shows its icon. Register and schema default to the field's reference.", "properties": { "register": { "type": "string", "minLength": 1 }, "schema": { "type": "string", "minLength": 1 }, "colorField": { "type": "string", "minLength": 1 }, "iconField": { "type": "string", "minLength": 1 } } }, "formatter": { "type": "string", "minLength": 1 }, "formatterOptions": { "type": "object" }, "colorMap": { "type": "object", "additionalProperties": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] } }, "labels": { "type": "object", "additionalProperties": { "type": "string" } }, "variant": { "type": "string", "enum": ["default", "primary", "success", "warning", "error", "info", "purple", "teal"] } } }] } }
|
|
5350
5398
|
}
|
|
5351
5399
|
},
|
|
5352
5400
|
"afterList": { "type": "array", "description": "type='index': cards after the list, stacked full width in the listed order, the counterpart of statRow above it (index-scope-presentation). Each entry is a widget definition resolved like statRow's. A query preset or a folder scope can set its own. Maps to CnIndexPage `afterList`.", "items": { "type": "object", "required": ["id", "type"], "properties": { "id": { "type": "string", "minLength": 1 }, "type": { "type": "string", "minLength": 1 }, "title": { "type": "string" }, "subtitle": { "type": "string" }, "content": { "type": "object" }, "headerLink": { "type": "object" } } } },
|
|
@@ -399,6 +399,8 @@ export function liveUpdatesPlugin(opts = {}) {
|
|
|
399
399
|
// Plain (non-reactive) Map: type → last params object
|
|
400
400
|
store.__lastCollectionParams = new Map()
|
|
401
401
|
|
|
402
|
+
// Detached: the store outlives the component whose setup first
|
|
403
|
+
// used it, and an attached observer is removed with that scope.
|
|
402
404
|
store.$onAction(({ name, args, after }) => {
|
|
403
405
|
if (name === 'fetchCollection') {
|
|
404
406
|
const [type, params = {}] = args
|
|
@@ -406,7 +408,7 @@ export function liveUpdatesPlugin(opts = {}) {
|
|
|
406
408
|
store.__lastCollectionParams.set(type, params)
|
|
407
409
|
})
|
|
408
410
|
}
|
|
409
|
-
})
|
|
411
|
+
}, true)
|
|
410
412
|
|
|
411
413
|
// -- In-flight dedup for fetchObject --
|
|
412
414
|
// Plain Map: dedupKey → Promise
|
|
@@ -156,6 +156,7 @@ export function logsPlugin(options = {}) {
|
|
|
156
156
|
|
|
157
157
|
if (autoRefreshOnItemChange) {
|
|
158
158
|
plugin.setup = function setup(store) {
|
|
159
|
+
// Detached: the store outlives the component whose setup first used it.
|
|
159
160
|
store.$onAction(({ name, after }) => {
|
|
160
161
|
if (name !== 'setItem') {
|
|
161
162
|
return
|
|
@@ -170,7 +171,7 @@ export function logsPlugin(options = {}) {
|
|
|
170
171
|
store.clearLogs()
|
|
171
172
|
}
|
|
172
173
|
})
|
|
173
|
-
})
|
|
174
|
+
}, true)
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
177
|
|
package/src/utils/actionLink.js
CHANGED
|
@@ -9,11 +9,114 @@
|
|
|
9
9
|
* The rendering component turns it into an `<a href>`; a plain click on an
|
|
10
10
|
* in-app `to` is routed through the router, anything else is the browser's.
|
|
11
11
|
*
|
|
12
|
+
* A manifest `headerActions[]` entry may carry `href` too; the page turns it
|
|
13
|
+
* into the final URL with {@link headerActionAsLink} before it renders.
|
|
14
|
+
*
|
|
12
15
|
* @module utils/actionLink
|
|
13
16
|
*/
|
|
14
17
|
|
|
18
|
+
import { generateUrl, getRootUrl } from '@nextcloud/router'
|
|
15
19
|
import { followLinkClick } from './linkNavigation.js'
|
|
16
20
|
|
|
21
|
+
/** An absolute URL a manifest link may name, matching the schema's lowercase pattern. */
|
|
22
|
+
const ABSOLUTE_HREF = /^(?:https?:\/\/|mailto:|tel:)/
|
|
23
|
+
|
|
24
|
+
/** An instance path that already names a Nextcloud entry point, so generateUrl() must not add `/index.php`. */
|
|
25
|
+
const ROUTED_PATH = /^\/(?:index\.php|remote\.php|public\.php|ocs)(?:[/?#]|$)/
|
|
26
|
+
|
|
27
|
+
/** Bad hrefs already warned about, so a recomputing page warns once per value. */
|
|
28
|
+
const warnedHrefs = new Set()
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Entries whose function handler was dropped for their href, warned about
|
|
32
|
+
* once. Keyed by `id`, else by the declared href and label (behind a NUL, so
|
|
33
|
+
* no real id collides), so entries without an id each get their own warning
|
|
34
|
+
* and a recomputing page does not repeat it.
|
|
35
|
+
*/
|
|
36
|
+
const warnedHandlers = new Set()
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The URL a manifest `href` points at: an absolute `http(s)`, `mailto:` or
|
|
40
|
+
* `tel:` URL as written, `//host/...` as written, an instance path that
|
|
41
|
+
* names an entry point (`/index.php/`, `/remote.php/`, `/public.php/`,
|
|
42
|
+
* `/ocs/`) under `getRootUrl()` (as written when it already starts with the
|
|
43
|
+
* root), any other instance path (leading `/`) through `generateUrl()`.
|
|
44
|
+
*
|
|
45
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-href-resolves-to-a-url-on-this-instance-or-an-allowed-scheme
|
|
46
|
+
* @param {unknown} href The manifest `href`.
|
|
47
|
+
* @return {string} The URL, or '' for anything else (a relative path, another scheme).
|
|
48
|
+
*/
|
|
49
|
+
export function resolveManifestHref(href) {
|
|
50
|
+
if (typeof href !== 'string') {
|
|
51
|
+
return ''
|
|
52
|
+
}
|
|
53
|
+
const value = href.trim()
|
|
54
|
+
if (ABSOLUTE_HREF.test(value) || value.startsWith('//')) {
|
|
55
|
+
return value
|
|
56
|
+
}
|
|
57
|
+
const root = getRootUrl()
|
|
58
|
+
if (root && value.startsWith(root) && ROUTED_PATH.test(value.slice(root.length))) {
|
|
59
|
+
return value
|
|
60
|
+
}
|
|
61
|
+
if (ROUTED_PATH.test(value)) {
|
|
62
|
+
return root + value
|
|
63
|
+
}
|
|
64
|
+
if (value.startsWith('/')) {
|
|
65
|
+
return generateUrl(value)
|
|
66
|
+
}
|
|
67
|
+
return ''
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A manifest header action in its link form. An entry with `href` gets the
|
|
72
|
+
* resolved URL and an anchor `target` (`_blank` when the manifest says so,
|
|
73
|
+
* else `_self`), and loses its `handler`: a link only navigates. Its
|
|
74
|
+
* `children` are resolved the same way. An `href` that resolves to nothing is
|
|
75
|
+
* dropped with a warning, along with a handler name, so the page renders its
|
|
76
|
+
* usual button for the entry (an index or reports page still emits
|
|
77
|
+
* `header-action` for it); a function `handler` (JS composition) is kept
|
|
78
|
+
* then and still runs. An entry with nothing to resolve, a function-valued
|
|
79
|
+
* `href` included, is returned as is. When the `href` does resolve, a
|
|
80
|
+
* function `handler` is dropped like any other, with a one-time warning.
|
|
81
|
+
*
|
|
82
|
+
* @spec openspec/changes/header-action-href/specs/header-action-href/spec.md#requirement-a-header-action-with-href-is-a-link-that-runs-and-emits-nothing
|
|
83
|
+
* @param {object} entry The manifest header action.
|
|
84
|
+
* @return {object} The entry, or a copy in link form.
|
|
85
|
+
*/
|
|
86
|
+
export function headerActionAsLink(entry) {
|
|
87
|
+
if (!entry || typeof entry !== 'object') {
|
|
88
|
+
return entry
|
|
89
|
+
}
|
|
90
|
+
let resolved = entry
|
|
91
|
+
if (Array.isArray(entry.children)) {
|
|
92
|
+
const children = entry.children.map(headerActionAsLink)
|
|
93
|
+
if (children.some((child, index) => child !== entry.children[index])) {
|
|
94
|
+
resolved = { ...entry, children }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (typeof entry.href !== 'string' || entry.href === '') {
|
|
98
|
+
return resolved
|
|
99
|
+
}
|
|
100
|
+
const { href: declared, ...withoutHref } = resolved
|
|
101
|
+
const { handler: _handler, ...rest } = withoutHref
|
|
102
|
+
const href = resolveManifestHref(declared)
|
|
103
|
+
if (!href) {
|
|
104
|
+
if (!warnedHrefs.has(declared)) {
|
|
105
|
+
warnedHrefs.add(declared)
|
|
106
|
+
// eslint-disable-next-line no-console
|
|
107
|
+
console.warn(`headerActions[].id "${entry.id}": href "${declared}" is neither an http(s), mailto or tel URL nor an instance path starting with "/"; the entry renders without a link`)
|
|
108
|
+
}
|
|
109
|
+
return typeof entry.handler === 'function' ? withoutHref : rest
|
|
110
|
+
}
|
|
111
|
+
const handlerKey = entry.id ?? `\u0000${declared}|${entry.label ?? ''}`
|
|
112
|
+
if (typeof entry.handler === 'function' && !warnedHandlers.has(handlerKey)) {
|
|
113
|
+
warnedHandlers.add(handlerKey)
|
|
114
|
+
// eslint-disable-next-line no-console
|
|
115
|
+
console.warn(`headerActions[].id "${entry.id}": an entry with href never runs a handler, so its function handler is dropped`)
|
|
116
|
+
}
|
|
117
|
+
return { ...rest, href, target: entry.target === '_blank' ? '_blank' : '_self' }
|
|
118
|
+
}
|
|
119
|
+
|
|
17
120
|
/**
|
|
18
121
|
* The href of an in-app router location. A string is a route PATH and goes
|
|
19
122
|
* through the router as well, so a hash-history app gets its `#`.
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* @module utils/detailActionModel
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
import { variantForColor } from './headerFieldChips.js'
|
|
19
20
|
import { readPath } from './readPath.js'
|
|
20
21
|
import { evaluateVisibleWhenLocal } from './visibleWhen.js'
|
|
21
22
|
|
|
@@ -239,6 +240,32 @@ export function resolvePill(config, object) {
|
|
|
239
240
|
}
|
|
240
241
|
}
|
|
241
242
|
|
|
243
|
+
/**
|
|
244
|
+
* What a pill takes from the record its `resolve` points at
|
|
245
|
+
* (`resolve.colorField`, `resolve.iconField`): the tone its colour name maps
|
|
246
|
+
* onto (an NL Design System hue or a badge role, see `variantForColor`), which
|
|
247
|
+
* replaces the pill's own `variant` and `colorMap`, and an icon name. An empty
|
|
248
|
+
* colour or icon leaves the pill as declared.
|
|
249
|
+
*
|
|
250
|
+
* @param {{color?: string|null, icon?: string|null}|null|undefined} look The names read off the record.
|
|
251
|
+
* @return {{variant?: string, colorMap?: null, icon?: string}} The keys to lay over the pill.
|
|
252
|
+
* @spec openspec/changes/pill-color-field/specs/pill-color-field/spec.md#requirement-a-pill-takes-its-tone-and-icon-from-the-referenced-record
|
|
253
|
+
*/
|
|
254
|
+
export function pillLook(look) {
|
|
255
|
+
const out = {}
|
|
256
|
+
if (!look || typeof look !== 'object') {
|
|
257
|
+
return out
|
|
258
|
+
}
|
|
259
|
+
if (typeof look.color === 'string' && look.color !== '') {
|
|
260
|
+
out.variant = variantForColor(look.color)
|
|
261
|
+
out.colorMap = null
|
|
262
|
+
}
|
|
263
|
+
if (typeof look.icon === 'string' && look.icon !== '') {
|
|
264
|
+
out.icon = look.icon
|
|
265
|
+
}
|
|
266
|
+
return out
|
|
267
|
+
}
|
|
268
|
+
|
|
242
269
|
/**
|
|
243
270
|
* Resolve a tab's count.
|
|
244
271
|
*
|
|
@@ -7,7 +7,7 @@ import { formatMoney } from './formatMetric.js'
|
|
|
7
7
|
import { readPath } from './readPath.js'
|
|
8
8
|
import { userDisplayName } from './userDisplayName.js'
|
|
9
9
|
|
|
10
|
-
const TOKEN = /\{([^{}\s|]+)(?:\|([
|
|
10
|
+
const TOKEN = /\{([^{}\s|]+)(?:\|([A-Za-z]+)(?::([^{}\s]+))?)?\}/g
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* A moment in the board's short form with the time: "4 okt, 10:52", or
|
|
@@ -68,12 +68,18 @@ export function formatLongDate(value, now = new Date(), withYear = false) {
|
|
|
68
68
|
* @param {string} [arg] The format's argument (`currency:EUR`, or a row path holding the code).
|
|
69
69
|
* @param {object|null} [object] The row, for a currency read from a field.
|
|
70
70
|
* @param {object|null} [config] The app config map, for the reporting currency.
|
|
71
|
+
* @param {object|null} [formatters] The app's named formatters (`cnFormatters`), for any other format name.
|
|
71
72
|
* @return {string} The text, '' when nothing can be shown.
|
|
73
|
+
* @spec openspec/changes/map-list-formatters/specs/map-list-formatters/spec.md#requirement-a-map-list-reads-display-names-through-the-apps-formatters
|
|
72
74
|
*/
|
|
73
|
-
function formatValue(value, format, now, arg = '', object = null, config = null) {
|
|
75
|
+
function formatValue(value, format, now, arg = '', object = null, config = null, formatters = null) {
|
|
74
76
|
if (value === null || value === undefined || value === '' || typeof value === 'object') {
|
|
75
77
|
return ''
|
|
76
78
|
}
|
|
79
|
+
if (format !== '' && formatters && typeof formatters[format] === 'function') {
|
|
80
|
+
const shown = formatters[format](value, object)
|
|
81
|
+
return shown === null || shown === undefined ? '' : String(shown)
|
|
82
|
+
}
|
|
77
83
|
if (format === 'date') {
|
|
78
84
|
return formatBoardDate(value, now)
|
|
79
85
|
}
|
|
@@ -130,14 +136,16 @@ function formatValue(value, format, now, arg = '', object = null, config = null)
|
|
|
130
136
|
* the row field is empty, the app's reporting currency), `{path|relative}` and
|
|
131
137
|
* `{path|relativetime}` say "today", "yesterday" and then the short date
|
|
132
138
|
* ("vandaag, 09.42"), and `{path|days}` is the
|
|
133
|
-
* whole days since that date ("has waited {createdAt|days} days").
|
|
139
|
+
* whole days since that date ("has waited {createdAt|days} days"). Any other
|
|
140
|
+
* name is one of the app's formatters, passed as `context.formatters`
|
|
141
|
+
* (`{id|caseAddress}`). A token without a
|
|
134
142
|
* value empties the WHOLE template, because "Termijn " is worse than nothing.
|
|
135
143
|
* A template without tokens is returned as written (a constant label).
|
|
136
144
|
*
|
|
137
145
|
* @param {string} template The template, already translated.
|
|
138
146
|
* @param {object|null} object The object to read.
|
|
139
147
|
* @param {Date} [now] The reference moment for dates.
|
|
140
|
-
* @param {{config?: object}|null} [context] The app config map, for the reporting currency of a money token.
|
|
148
|
+
* @param {{config?: object, formatters?: object}|null} [context] The app config map, for the reporting currency of a money token, and the app's named formatters.
|
|
141
149
|
* @return {string} The filled text, or ''.
|
|
142
150
|
* @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-a-record-list-shows-the-objects-that-point-at-this-record
|
|
143
151
|
*/
|
|
@@ -148,7 +156,7 @@ export function fillTemplate(template, object, now = new Date(), context = null)
|
|
|
148
156
|
let missing = false
|
|
149
157
|
const text = template.replace(TOKEN, (_match, path, format, arg) => {
|
|
150
158
|
const value = object && typeof object === 'object' ? readPath(object, path) : undefined
|
|
151
|
-
const shown = formatValue(value, format || '', now, arg || '', object, context && context.config ? context.config : null)
|
|
159
|
+
const shown = formatValue(value, format || '', now, arg || '', object, context && context.config ? context.config : null, context && context.formatters ? context.formatters : null)
|
|
152
160
|
if (shown === '') {
|
|
153
161
|
missing = true
|
|
154
162
|
}
|
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
/** The renderers a header chip can use. */
|
|
12
12
|
export const HEADER_CHIP_FORMATS = Object.freeze(['text', 'mono', 'badge', 'user', 'date'])
|
|
13
13
|
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* The variants a colour name maps onto; anything else is neutral. A name is a
|
|
16
|
+
* badge role (`success`, `warning`, ...) or an NL Design System hue as a
|
|
17
|
+
* register stores it (`red`, `blue-light`, ...): a hue and its light tint
|
|
18
|
+
* take the same tone.
|
|
19
|
+
*/
|
|
15
20
|
const COLOR_VARIANTS = Object.freeze({
|
|
16
21
|
success: 'success',
|
|
17
22
|
warning: 'warning',
|
|
@@ -19,6 +24,20 @@ const COLOR_VARIANTS = Object.freeze({
|
|
|
19
24
|
info: 'info',
|
|
20
25
|
primary: 'primary',
|
|
21
26
|
neutral: 'default',
|
|
27
|
+
blue: 'info',
|
|
28
|
+
'blue-light': 'info',
|
|
29
|
+
green: 'success',
|
|
30
|
+
'green-light': 'success',
|
|
31
|
+
orange: 'warning',
|
|
32
|
+
'orange-light': 'warning',
|
|
33
|
+
red: 'error',
|
|
34
|
+
'red-light': 'error',
|
|
35
|
+
purple: 'purple',
|
|
36
|
+
'purple-light': 'purple',
|
|
37
|
+
teal: 'teal',
|
|
38
|
+
'teal-light': 'teal',
|
|
39
|
+
grey: 'default',
|
|
40
|
+
'grey-light': 'default',
|
|
22
41
|
})
|
|
23
42
|
|
|
24
43
|
/**
|
|
@@ -69,7 +88,7 @@ export function isEmptyChipValue(value) {
|
|
|
69
88
|
/**
|
|
70
89
|
* Map a colour name read off a referenced object onto a badge variant.
|
|
71
90
|
*
|
|
72
|
-
* @param {*} name The colour name (`success`, `warning`, `error`, `info`, `neutral`).
|
|
91
|
+
* @param {*} name The colour name: a badge role (`success`, `warning`, `error`, `info`, `neutral`) or an NL Design System hue (`red`, `blue-light`, ...).
|
|
73
92
|
* @return {string} A `CnStatusBadge` variant; `default` (neutral) when unknown.
|
|
74
93
|
*/
|
|
75
94
|
export function variantForColor(name) {
|
package/src/utils/mapList.js
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* SPDX-License-Identifier: EUPL-1.2
|
|
4
4
|
*/
|
|
5
5
|
import { isBadgeVariant } from './badgeVariants.js'
|
|
6
|
-
import {
|
|
6
|
+
import { pillLook } from './detailActionModel.js'
|
|
7
|
+
import { fillTemplate, firstFilled } from './fillTemplate.js'
|
|
7
8
|
import { readPath } from './readPath.js'
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -12,10 +13,14 @@ import { readPath } from './readPath.js'
|
|
|
12
13
|
* pin and the number in the list are the same, and the rows without one.
|
|
13
14
|
*
|
|
14
15
|
* `spec` is `mapConfig.list`: `{ title, sub?, pills?: [{ path?, text?, map?,
|
|
15
|
-
* colorMap? }], due? }`. `title`, `sub` and `due` are
|
|
16
|
-
* (`"{title}"`, `"{address} · {identifier}"`,
|
|
17
|
-
* translated by the caller;
|
|
18
|
-
*
|
|
16
|
+
* formatter?, colorMap?, variant?, resolve? }], due? }`. `title`, `sub` and `due` are
|
|
17
|
+
* field templates (`"{title}"`, `"{address} · {identifier}"`,
|
|
18
|
+
* `"{deadline|date}"`), already translated by the caller; `sub` may list
|
|
19
|
+
* several, the first that fills wins. A pill reads `path` (or shows `text`),
|
|
20
|
+
* maps it through `map` or the app formatter `formatter` (a reference's
|
|
21
|
+
* display name), and takes its tone from `colorMap` by the raw value, or,
|
|
22
|
+
* with `resolve`, the tone and icon of the record the value points at
|
|
23
|
+
* (`options.looks`, read by CnIndexPage as for its other pills).
|
|
19
24
|
*
|
|
20
25
|
* @param {Array<object>} rows The rows, in display order.
|
|
21
26
|
* @param {object} spec The list declaration.
|
|
@@ -23,9 +28,14 @@ import { readPath } from './readPath.js'
|
|
|
23
28
|
* @param {string} options.rowKey The row's key field.
|
|
24
29
|
* @param {Function} options.located Whether a row has a place on the map.
|
|
25
30
|
* @param {Function} [options.translate] The host's translate, for pill labels.
|
|
31
|
+
* @param {object} [options.formatters] The app's named formatters, for pills and template tokens.
|
|
32
|
+
* @param {object} [options.looks] The referenced records' looks by field and id (`{ [path]: { [id]: { color, icon } } }`).
|
|
26
33
|
* @return {{items: Array<{key: string, n: number, row: object, title: string, sub: string, due: string, pills: Array<{label: string, variant: string}>}>, missing: Array<object>}}
|
|
34
|
+
* @spec openspec/changes/map-list-formatters/specs/map-list-formatters/spec.md#requirement-a-map-list-reads-display-names-through-the-apps-formatters
|
|
35
|
+
* @spec openspec/changes/map-list-formatters/specs/map-list-formatters/spec.md#requirement-a-map-list-pill-takes-its-tone-and-icon-from-the-referenced-record
|
|
27
36
|
*/
|
|
28
|
-
export function mapListItems(rows, spec, { rowKey, located, translate = (text) => text }) {
|
|
37
|
+
export function mapListItems(rows, spec, { rowKey, located, translate = (text) => text, formatters = null, looks = null }) {
|
|
38
|
+
const context = formatters ? { formatters } : null
|
|
29
39
|
const items = []
|
|
30
40
|
const missing = []
|
|
31
41
|
const list = spec && typeof spec === 'object' ? spec : {}
|
|
@@ -40,7 +50,9 @@ export function mapListItems(rows, spec, { rowKey, located, translate = (text) =
|
|
|
40
50
|
return null
|
|
41
51
|
}
|
|
42
52
|
const raw = typeof pill.path === 'string' && pill.path !== '' ? readPath(row, pill.path) : undefined
|
|
43
|
-
const
|
|
53
|
+
const formatter = typeof pill.formatter === 'string' && formatters ? formatters[pill.formatter] : null
|
|
54
|
+
const formatted = typeof formatter === 'function' && (typeof raw === 'string' || typeof raw === 'number') && raw !== '' ? formatter(raw, row) : undefined
|
|
55
|
+
const mapped = pill.map && (typeof raw === 'string' || typeof raw === 'number') && Object.hasOwn(pill.map, raw) ? pill.map[raw] : formatted
|
|
44
56
|
const text = mapped ?? pill.text ?? raw
|
|
45
57
|
if (typeof text !== 'string' && typeof text !== 'number') {
|
|
46
58
|
return null
|
|
@@ -50,16 +62,19 @@ export function mapListItems(rows, spec, { rowKey, located, translate = (text) =
|
|
|
50
62
|
return null
|
|
51
63
|
}
|
|
52
64
|
const tone = pill.colorMap && (typeof raw === 'string' || typeof raw === 'number') ? pill.colorMap[raw] : pill.variant
|
|
53
|
-
|
|
65
|
+
const fieldLooks = pill.resolve && looks && typeof pill.path === 'string' ? looks[pill.path] : null
|
|
66
|
+
const look = pillLook(fieldLooks && (typeof raw === 'string' || typeof raw === 'number') ? fieldLooks[String(raw)] : null)
|
|
67
|
+
const variant = look.variant || tone
|
|
68
|
+
return { label, variant: isBadgeVariant(variant) ? variant : 'default', ...(look.icon ? { icon: look.icon } : {}) }
|
|
54
69
|
})
|
|
55
70
|
.filter(Boolean)
|
|
56
71
|
items.push({
|
|
57
72
|
key: String(row[rowKey]),
|
|
58
73
|
n: items.length + 1,
|
|
59
74
|
row,
|
|
60
|
-
title: fillTemplate(list.title || '', row),
|
|
61
|
-
sub:
|
|
62
|
-
due: fillTemplate(list.due || '', row),
|
|
75
|
+
title: fillTemplate(list.title || '', row, undefined, context),
|
|
76
|
+
sub: list.sub ? firstFilled(list.sub, row, undefined, undefined, context) : '',
|
|
77
|
+
due: fillTemplate(list.due || '', row, undefined, context),
|
|
63
78
|
pills,
|
|
64
79
|
})
|
|
65
80
|
}
|
|
@@ -64,6 +64,9 @@ export function normalizeSavedViewMenu(entries) {
|
|
|
64
64
|
}
|
|
65
65
|
seen.add(`${page}|${id}`)
|
|
66
66
|
const entry = { id, page, name: e && typeof e.name === 'string' && e.name !== '' ? e.name : id }
|
|
67
|
+
if (typeof e.icon === 'string' && e.icon !== '') {
|
|
68
|
+
entry.icon = e.icon
|
|
69
|
+
}
|
|
67
70
|
const badge = normalizeBadge(e.badge)
|
|
68
71
|
if (badge) {
|
|
69
72
|
entry.badge = badge
|
|
@@ -87,12 +90,12 @@ export function toggleSavedViewMenuEntry(entries, entry) {
|
|
|
87
90
|
if (present) {
|
|
88
91
|
return list.filter((e) => !(e.id === id && e.page === entry.page))
|
|
89
92
|
}
|
|
90
|
-
return normalizeSavedViewMenu([...list, { id, page: entry.page, name: entry.name, badge: entry.badge }])
|
|
93
|
+
return normalizeSavedViewMenu([...list, { id, page: entry.page, name: entry.name, icon: entry.icon, badge: entry.badge }])
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
/**
|
|
94
97
|
* Bring one page's entries in line with the views that page can show:
|
|
95
|
-
* current names and badges, and no entry for a view that was deleted or is
|
|
98
|
+
* current names, icons and badges, and no entry for a view that was deleted or is
|
|
96
99
|
* no longer shared with the user. Other pages' entries are left alone. A
|
|
97
100
|
* view's badge is read from its `menuBadge` (see `withSavedViewMenu`).
|
|
98
101
|
*
|
|
@@ -110,9 +113,10 @@ export function syncSavedViewMenu(entries, page, views) {
|
|
|
110
113
|
return e
|
|
111
114
|
}
|
|
112
115
|
const view = byId.get(e.id)
|
|
113
|
-
const { badge, ...rest } = e
|
|
116
|
+
const { badge, icon, ...rest } = e
|
|
114
117
|
const next = normalizeBadge(view.menuBadge)
|
|
115
|
-
|
|
118
|
+
const nextIcon = typeof view.icon === 'string' && view.icon !== '' ? view.icon : icon
|
|
119
|
+
return normalizeSavedViewMenu([{ ...rest, name: view.name || e.name, ...(nextIcon ? { icon: nextIcon } : {}), ...(next ? { badge: next } : {}) }])[0]
|
|
116
120
|
})
|
|
117
121
|
}
|
|
118
122
|
|
|
@@ -127,6 +131,9 @@ export const SAVED_VIEW_MENU_EMPTY = 'saved-views-empty'
|
|
|
127
131
|
* same place (Ruben's design rule, 19 Oct); it replaces app-specific groups
|
|
128
132
|
* such as dossiq's "My case types".
|
|
129
133
|
*
|
|
134
|
+
* An entry shows its view's `icon` (a registered icon name; a case type's
|
|
135
|
+
* own icon, say), else the filter icon.
|
|
136
|
+
*
|
|
130
137
|
* An entry with a badge carries it as the menu entry's `count` (and
|
|
131
138
|
* `counterVariant`), which CnAppRoot fetches like any filtered menu count.
|
|
132
139
|
*
|
|
@@ -154,7 +161,7 @@ export function withSavedViewMenu(menu, entries, options = {}) {
|
|
|
154
161
|
label: e.name,
|
|
155
162
|
route: e.page,
|
|
156
163
|
query: { savedView: e.id },
|
|
157
|
-
icon: 'FilterOutline',
|
|
164
|
+
icon: e.icon || 'FilterOutline',
|
|
158
165
|
...(e.badge || {}),
|
|
159
166
|
}))
|
|
160
167
|
: [{ id: SAVED_VIEW_MENU_EMPTY, type: 'hint', label: t('nextcloud-vue', 'Add a view to the menu from Filters') }]),
|