@conduction/nextcloud-vue 2.79.0 → 2.80.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.vue2.js.map +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +3 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +32 -3
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +10 -8
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +39 -3
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +79 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +133 -96
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +62 -4
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +24 -0
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +43 -4
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +11 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +736 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +238 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnFactsWidget/index.js +30 -0
- package/dist/esm/components/CnFactsWidget/index.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +10 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +551 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +183 -0
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnFieldListWidget/index.js +29 -0
- package/dist/esm/components/CnFieldListWidget/index.js.map +1 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +65 -17
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +4 -4
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +18 -3
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +305 -72
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +31 -13
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js +10 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue.js.map +1 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js +99 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue2.js.map +1 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js +48 -0
- package/dist/esm/components/CnIndexPage/CnIndexPageWidgets.vue3.js.map +1 -0
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js +29 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnPageRenderer/CnPageRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +10 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +387 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +169 -0
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnRecordListWidget/index.js +31 -0
- package/dist/esm/components/CnRecordListWidget/index.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js +10 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js +351 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js +71 -0
- package/dist/esm/components/CnSectionsWidget/CnSectionsWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnSectionsWidget/index.js +35 -0
- package/dist/esm/components/CnSectionsWidget/index.js.map +1 -0
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js +81 -2
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +1 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +122 -7
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js +84 -23
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +106 -2
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +105 -53
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
- package/dist/esm/composables/useFormDraft.js +140 -9
- package/dist/esm/composables/useFormDraft.js.map +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js +28 -20
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue2.js.map +1 -1
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js +9 -0
- package/dist/esm/integrations/builtin/field-inspection/CnFieldInspectionCard.vue3.js.map +1 -1
- package/dist/esm/integrations/builtin/field-inspection.js +21 -0
- package/dist/esm/integrations/builtin/field-inspection.js.map +1 -1
- package/dist/esm/integrations/offline/checklistSubmission.js +141 -0
- package/dist/esm/integrations/offline/checklistSubmission.js.map +1 -0
- package/dist/esm/integrations/offline/fieldCollectionHelpers.js +70 -5
- package/dist/esm/integrations/offline/fieldCollectionHelpers.js.map +1 -1
- package/dist/esm/integrations/offline/index.js +2 -1
- package/dist/esm/integrations/offline/index.js.map +1 -1
- package/dist/esm/integrations/offline/offlineDb.js +6 -1
- package/dist/esm/integrations/offline/offlineDb.js.map +1 -1
- package/dist/esm/integrations/offline/syncReplayService.js +8 -2
- package/dist/esm/integrations/offline/syncReplayService.js.map +1 -1
- package/dist/esm/l10n/en.json.js +12 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +16 -2
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +1000 -59
- package/dist/esm/utils/detailActionModel.js +60 -3
- package/dist/esm/utils/detailActionModel.js.map +1 -1
- package/dist/esm/utils/fillTemplate.js +114 -0
- package/dist/esm/utils/fillTemplate.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +4 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/queryPresets.js +96 -0
- package/dist/esm/utils/queryPresets.js.map +1 -0
- package/dist/esm/utils/resolveFilterTokens.js +11 -0
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/schema.js +21 -0
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +3 -3
- package/dist/esm/utils/sentinelTokens.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/esm/utils/widgetDispatch.js +7 -1
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +12368 -7717
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +1000 -59
- package/l10n/en.json +12 -1
- package/l10n/nl.json +16 -2
- package/package.json +1 -1
- package/src/components/CnActionButtons/CnActionButtons.vue +2 -0
- package/src/components/CnActionsBar/CnActionsBar.md +4 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +42 -10
- package/src/components/CnBannerWidget/CnBannerWidget.vue +39 -3
- package/src/components/CnCellRenderer/CnCellRenderer.vue +188 -1
- package/src/components/CnDataTable/CnDataTable.vue +1 -0
- package/src/components/CnDetailPage/CnDetailPage.md +1 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +63 -3
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +66 -0
- package/src/components/CnFactsWidget/CnFactsWidget.vue +1050 -0
- package/src/components/CnFactsWidget/index.js +29 -0
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +746 -0
- package/src/components/CnFieldListWidget/index.js +28 -0
- package/src/components/CnFormDialog/CnFormDialog.md +9 -3
- package/src/components/CnFormDialog/CnFormDialog.vue +69 -21
- package/src/components/CnFormPage/CnFormPage.vue +18 -3
- package/src/components/CnIndexPage/CnIndexPage.vue +254 -6
- package/src/components/CnIndexPage/CnIndexPageWidgets.vue +169 -0
- package/src/components/CnPageRenderer/CnPageRenderer.vue +29 -1
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +603 -0
- package/src/components/CnRecordListWidget/index.js +30 -0
- package/src/components/CnSectionsWidget/CnSectionsWidget.vue +489 -0
- package/src/components/CnSectionsWidget/index.js +34 -0
- package/src/components/CnStagesWidget/CnStagesWidget.vue +81 -2
- package/src/components/CnStatWidget/CnStatWidget.vue +1 -1
- package/src/components/CnTabsWidget/CnTabsWidget.vue +184 -8
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +4 -0
- package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +188 -4
- package/src/components/index.js +4 -0
- package/src/composables/__tests__/useFormDraft.spec.js +69 -0
- package/src/composables/useFormDraft.js +139 -8
- package/src/css/form-field.css +14 -0
- package/src/css/index-page.css +71 -0
- package/src/css/kpi-card.css +32 -0
- package/src/css/look-board-detail.css +77 -0
- package/src/css/look-board-index.css +51 -0
- package/src/css/look-board.css +37 -2
- package/src/css/table.css +19 -0
- package/src/index.js +4 -0
- package/src/integrations/builtin/field-inspection/CnFieldInspectionCard.vue +35 -20
- package/src/integrations/builtin/field-inspection.js +21 -0
- package/src/integrations/offline/checklistSubmission.js +137 -0
- package/src/integrations/offline/fieldCollectionHelpers.js +69 -4
- package/src/integrations/offline/index.js +3 -0
- package/src/integrations/offline/offlineDb.js +6 -1
- package/src/integrations/offline/syncReplayService.js +8 -2
- package/src/schemas/app-manifest-v2.schema.json +104 -5
- package/src/utils/detailActionModel.js +59 -2
- package/src/utils/fillTemplate.js +110 -0
- package/src/utils/libraryWidgetKeys.js +4 -0
- package/src/utils/queryPresets.js +93 -0
- package/src/utils/resolveFilterTokens.js +11 -0
- package/src/utils/schema.js +21 -0
- package/src/utils/sentinelTokens.js +3 -3
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/widgetDispatch.js +7 -1
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
import { emit } from '@nextcloud/event-bus';
|
|
2
|
+
import { translate } from '@nextcloud/l10n';
|
|
3
|
+
import { NcLoadingIcon } from '@nextcloud/vue';
|
|
4
|
+
import Check from 'vue-material-design-icons/Check.vue';
|
|
5
|
+
import ContentCopy from 'vue-material-design-icons/ContentCopy.vue';
|
|
6
|
+
import EyeOutline from 'vue-material-design-icons/EyeOutline.vue';
|
|
7
|
+
import __sfc_main from '../CnIcon/CnIcon.vue.js';
|
|
8
|
+
import __sfc_main$1 from '../CnStatusBadge/CnStatusBadge.vue.js';
|
|
9
|
+
import { resolveEndpointRequest } from '../../composables/useEndpointSource.js';
|
|
10
|
+
import { dispatchAction } from '../../utils/actionsDispatcher.js';
|
|
11
|
+
import { isBadgeVariant } from '../../utils/badgeVariants.js';
|
|
12
|
+
import { formatBoardDate } from '../../utils/boardDate.js';
|
|
13
|
+
import { parseDateValue } from '../../utils/dateVariant.js';
|
|
14
|
+
import { readPath } from '../../utils/readPath.js';
|
|
15
|
+
import { resolveDeepTokens, hasUnresolvedDeepTokens } from '../../utils/resolveFilterTokens.js';
|
|
16
|
+
|
|
17
|
+
/** Kinds of entry a `fields[]` item can be. A labelled item without `as` is a row. */
|
|
18
|
+
const KINDS = Object.freeze(['row', 'title', 'line', 'pill', 'lead', 'text']);
|
|
19
|
+
|
|
20
|
+
/** Event-bus channel a reveal of a masked value is announced on, so an app can log it. */
|
|
21
|
+
const FACTS_REVEAL_CHANNEL = 'cn:facts:reveal';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* CnFactsWidget — the `facts` widget: a card of facts about ONE object.
|
|
25
|
+
*
|
|
26
|
+
* Renders the configured fields of the current detail-page object, or of one
|
|
27
|
+
* other object named by `content.source`, as the side cards on the screens
|
|
28
|
+
* draw them: label/value rows (a two-column grid, label muted), a bold title
|
|
29
|
+
* line that may link, muted meta lines that join several fields with " · ",
|
|
30
|
+
* a status pill, and one link at the foot.
|
|
31
|
+
*
|
|
32
|
+
* Everything is configuration; nothing in it knows an app:
|
|
33
|
+
*
|
|
34
|
+
* ```json
|
|
35
|
+
* {
|
|
36
|
+
* "type": "facts",
|
|
37
|
+
* "title": "Zaak",
|
|
38
|
+
* "content": {
|
|
39
|
+
* "source": { "register": "dossiq", "schema": "case", "id": "@object.case" },
|
|
40
|
+
* "fields": [
|
|
41
|
+
* { "as": "title", "path": "title", "route": { "name": "CaseDetail", "params": { "id": "@source.id" } } },
|
|
42
|
+
* { "as": "line", "parts": ["identifier", "caseTypeTitle"] },
|
|
43
|
+
* { "as": "pill", "path": "status", "map": { "open": "In behandeling" } },
|
|
44
|
+
* { "path": "deadline", "label": "Termijn", "format": "date" }
|
|
45
|
+
* ],
|
|
46
|
+
* "link": { "label": "Naar de zaak", "route": { "name": "CaseDetail", "params": { "id": "@source.id" } } },
|
|
47
|
+
* "emptyText": "Geen zaak"
|
|
48
|
+
* }
|
|
49
|
+
* }
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* Tokens: `source.id` takes the shared filter tokens; `source.url` takes them
|
|
53
|
+
* inside the url, the endpoint grammar (`/apps/x/api/y/@objectId`)
|
|
54
|
+
* (`@objectId`, `@object.<field>`, `@workspace.<key>`, `@me`, ...). Routes
|
|
55
|
+
* and the link also take `@source.<field>` (a field of the object shown).
|
|
56
|
+
* A source whose id token does not resolve renders the empty text, never a
|
|
57
|
+
* request for the literal token.
|
|
58
|
+
*
|
|
59
|
+
* Labels, map values, the link label and the empty text go through the host
|
|
60
|
+
* app's translate (`cnTranslate`).
|
|
61
|
+
*/
|
|
62
|
+
var script = {
|
|
63
|
+
name: 'CnFactsWidget',
|
|
64
|
+
|
|
65
|
+
components: { Check, CnIcon: __sfc_main, CnStatusBadge: __sfc_main$1, ContentCopy, EyeOutline, NcLoadingIcon },
|
|
66
|
+
|
|
67
|
+
inject: {
|
|
68
|
+
cnTranslate: { default: () => (key) => key },
|
|
69
|
+
cnObjectContext: { default: null },
|
|
70
|
+
cnWorkspaceContext: { default: null },
|
|
71
|
+
/** Pre-bound action dispatcher of CnPageRenderer, for `action` / `headerAction`. */
|
|
72
|
+
cnDispatchAction: { default: null },
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
props: {
|
|
76
|
+
/**
|
|
77
|
+
* The widget configuration.
|
|
78
|
+
*
|
|
79
|
+
* - `fields` — the entries, in order. Each is `{ path, label?, as?, format?, map?, itemPath?, parts?, separator?, route?, variant?, variantMap?, icon?, copy?, reveal?, text?, title?, subtitle?, initialsPath?, tone? }`.
|
|
80
|
+
* `as` is `row` (default when `label` or `icon` is set), `title`, `line`, `text` (a line in the normal ink), `lead` (an icon tile or initials beside `title` and `subtitle`) or `pill`.
|
|
81
|
+
* A row's `format` may also be `email`, `phone` or `url` (a link) or `masked` (the last four characters; `reveal: true` adds Show). `copy: true` adds a copy button; `icon` replaces the label.
|
|
82
|
+
* A part may be `{ text }`, a constant; a `line` or `text` entry without `path` or `parts` shows its `text`.
|
|
83
|
+
* - `action` — an action drawn as a button under the facts; `headerAction` — one drawn in the card header (reported through the `header` event).
|
|
84
|
+
* `format` is `text` (default), `date` (board short date), `date-long` (always with the year) or `list` (an array joined by ", ", each item read at `itemPath`).
|
|
85
|
+
* `map` turns a raw value into a label (translated). `parts` (on a `line`) lists paths, or `{ path, label?, format? }`, joined by `separator` (default " · "); empty parts are dropped.
|
|
86
|
+
* - `source` — `{ register, schema, id }` or `{ url }`; omitted = the current object.
|
|
87
|
+
* - `link` — `{ label, route? | href? }` at the foot.
|
|
88
|
+
* - `hideEmpty` — drop an entry whose value is empty (default true).
|
|
89
|
+
* - `emptyText` — shown when no entry has a value.
|
|
90
|
+
*
|
|
91
|
+
* @type {{fields?: Array<object>, source?: object, link?: object, hideEmpty?: boolean, emptyText?: string}}
|
|
92
|
+
*/
|
|
93
|
+
content: {
|
|
94
|
+
type: Object,
|
|
95
|
+
default: () => ({}),
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The current detail-page object, passed by the detail page host.
|
|
100
|
+
* Read when `content.source` is not set.
|
|
101
|
+
*/
|
|
102
|
+
objectData: {
|
|
103
|
+
type: Object,
|
|
104
|
+
default: null,
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/** The current detail-page object id, for the `@objectId` token. */
|
|
108
|
+
objectId: {
|
|
109
|
+
type: [String, Number],
|
|
110
|
+
default: null,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
emits: ['header', 'reveal'],
|
|
115
|
+
|
|
116
|
+
data() {
|
|
117
|
+
return {
|
|
118
|
+
fetched: null,
|
|
119
|
+
loading: false,
|
|
120
|
+
error: false,
|
|
121
|
+
/** Keys of the masked rows the reader chose to show. */
|
|
122
|
+
revealed: {},
|
|
123
|
+
/** Key of the row whose value was just copied. */
|
|
124
|
+
copied: '',
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
computed: {
|
|
129
|
+
/**
|
|
130
|
+
* The token context: the detail object (prop, else the injected context)
|
|
131
|
+
* and the page workspace.
|
|
132
|
+
*
|
|
133
|
+
* @return {object}
|
|
134
|
+
*/
|
|
135
|
+
tokenCtx() {
|
|
136
|
+
const unwrap = (c) => (c && typeof c === 'object' && 'value' in c ? c.value : c);
|
|
137
|
+
const injected = unwrap(this.cnObjectContext) || {};
|
|
138
|
+
return {
|
|
139
|
+
...injected,
|
|
140
|
+
objectId: this.objectId ?? injected.objectId ?? this.objectData?.id ?? null,
|
|
141
|
+
object: this.objectData || injected.object || null,
|
|
142
|
+
workspace: unwrap(this.cnWorkspaceContext) || {},
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The source declaration with its tokens resolved, or null for the current object.
|
|
148
|
+
*
|
|
149
|
+
* @return {object|null}
|
|
150
|
+
*/
|
|
151
|
+
resolvedSource() {
|
|
152
|
+
const source = this.content?.source;
|
|
153
|
+
if (!source || typeof source !== 'object') {
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
if (typeof source.url === 'string' && source.url !== '') {
|
|
157
|
+
// The endpoint grammar: tokens INSIDE the url, and a request
|
|
158
|
+
// that is blocked while one leaves an empty path segment.
|
|
159
|
+
const request = resolveEndpointRequest({ url: source.url }, this.tokenCtx);
|
|
160
|
+
return { url: request.blocked ? '' : request.url }
|
|
161
|
+
}
|
|
162
|
+
return resolveDeepTokens(source, this.tokenCtx)
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A stable key of the resolved source, so the widget refetches only when it changes.
|
|
167
|
+
*
|
|
168
|
+
* @return {string}
|
|
169
|
+
*/
|
|
170
|
+
sourceKey() {
|
|
171
|
+
return this.resolvedSource ? JSON.stringify(this.resolvedSource) : ''
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The object the facts are read from.
|
|
176
|
+
*
|
|
177
|
+
* @return {object|null}
|
|
178
|
+
*/
|
|
179
|
+
record() {
|
|
180
|
+
return this.resolvedSource ? this.fetched : this.objectData
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The entries to draw, consecutive rows grouped into one list.
|
|
185
|
+
*
|
|
186
|
+
* @return {Array<object>}
|
|
187
|
+
*/
|
|
188
|
+
blocks() {
|
|
189
|
+
const record = this.record;
|
|
190
|
+
if (!record) {
|
|
191
|
+
return []
|
|
192
|
+
}
|
|
193
|
+
const fields = Array.isArray(this.content?.fields) ? this.content.fields : [];
|
|
194
|
+
const hideEmpty = this.content?.hideEmpty !== false;
|
|
195
|
+
const out = [];
|
|
196
|
+
fields.forEach((field, index) => {
|
|
197
|
+
if (!field || typeof field !== 'object') {
|
|
198
|
+
return
|
|
199
|
+
}
|
|
200
|
+
const kind = this.kindOf(field);
|
|
201
|
+
const key = String(field.key || field.path || `${kind}-${index}`).replace(/[^\w-]/g, '-');
|
|
202
|
+
const value = this.entryValue(record, field, kind);
|
|
203
|
+
if (value === '' && (hideEmpty || kind === 'lead')) {
|
|
204
|
+
return
|
|
205
|
+
}
|
|
206
|
+
if (kind === 'row') {
|
|
207
|
+
const last = out[out.length - 1];
|
|
208
|
+
const row = { key, label: this.tr(field.label || ''), value, ...this.rowExtras(record, field, value) };
|
|
209
|
+
if (last && last.kind === 'rows') {
|
|
210
|
+
last.rows.push(row);
|
|
211
|
+
} else {
|
|
212
|
+
out.push({ kind: 'rows', key: `rows-${index}`, rows: [row] });
|
|
213
|
+
}
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
const block = { kind, key, value };
|
|
217
|
+
if (kind === 'lead') {
|
|
218
|
+
block.subtitle = this.partsValue(record, field.subtitle);
|
|
219
|
+
block.icon = typeof field.icon === 'string' ? field.icon : '';
|
|
220
|
+
block.initials = block.icon ? '' : this.initialsOf(readPath(record, field.initialsPath));
|
|
221
|
+
block.tone = ['success', 'info', 'warning', 'error'].includes(field.tone) ? field.tone : 'neutral';
|
|
222
|
+
}
|
|
223
|
+
if (kind === 'title' && field.route) {
|
|
224
|
+
block.to = this.resolveRoute(field.route, record);
|
|
225
|
+
}
|
|
226
|
+
if (kind === 'pill') {
|
|
227
|
+
const raw = readPath(record, field.path);
|
|
228
|
+
const variant = (field.variantMap && field.variantMap[raw]) || field.variant;
|
|
229
|
+
block.variant = isBadgeVariant(variant) ? variant : 'default';
|
|
230
|
+
}
|
|
231
|
+
out.push(block);
|
|
232
|
+
});
|
|
233
|
+
return out
|
|
234
|
+
},
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The foot link, resolved, or null.
|
|
238
|
+
*
|
|
239
|
+
* @return {{label: string, to?: object, href?: string}|null}
|
|
240
|
+
*/
|
|
241
|
+
link() {
|
|
242
|
+
const link = this.content?.link;
|
|
243
|
+
if (!link || typeof link !== 'object' || !link.label) {
|
|
244
|
+
return null
|
|
245
|
+
}
|
|
246
|
+
if (link.route) {
|
|
247
|
+
const to = this.resolveRoute(link.route, this.record);
|
|
248
|
+
return to ? { label: this.tr(link.label), to } : null
|
|
249
|
+
}
|
|
250
|
+
if (typeof link.href === 'string' && link.href !== '') {
|
|
251
|
+
const href = this.resolveTemplate(link.href, this.record);
|
|
252
|
+
return href ? { label: this.tr(link.label), href } : null
|
|
253
|
+
}
|
|
254
|
+
return null
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The action under the facts, as drawn: `{ label, icon }`, or null.
|
|
259
|
+
*
|
|
260
|
+
* @return {{label: string, icon: string}|null}
|
|
261
|
+
* @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead
|
|
262
|
+
*/
|
|
263
|
+
action() {
|
|
264
|
+
return this.actionView(this.content?.action)
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The header action, as drawn: `{ label, icon }`, or null.
|
|
269
|
+
*
|
|
270
|
+
* @return {{label: string, icon: string}|null}
|
|
271
|
+
* @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead
|
|
272
|
+
*/
|
|
273
|
+
headerAction() {
|
|
274
|
+
return this.actionView(this.content?.headerAction)
|
|
275
|
+
},
|
|
276
|
+
|
|
277
|
+
/** @return {string} The reveal button's text. */
|
|
278
|
+
showLabel() {
|
|
279
|
+
return translate('nextcloud-vue', 'Show')
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The empty line.
|
|
284
|
+
*
|
|
285
|
+
* @return {string}
|
|
286
|
+
*/
|
|
287
|
+
emptyLabel() {
|
|
288
|
+
return this.tr(this.content?.emptyText || 'Nothing to show yet')
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
watch: {
|
|
293
|
+
sourceKey: {
|
|
294
|
+
immediate: true,
|
|
295
|
+
handler() {
|
|
296
|
+
this.load();
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
headerAction: {
|
|
301
|
+
immediate: true,
|
|
302
|
+
handler() {
|
|
303
|
+
this.emitHeader();
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
methods: {
|
|
309
|
+
/**
|
|
310
|
+
* Translate through the host app.
|
|
311
|
+
*
|
|
312
|
+
* @param {string} text The text.
|
|
313
|
+
* @return {string}
|
|
314
|
+
*/
|
|
315
|
+
tr(text) {
|
|
316
|
+
if (typeof text !== 'string' || text === '') {
|
|
317
|
+
return ''
|
|
318
|
+
}
|
|
319
|
+
return typeof this.cnTranslate === 'function' ? this.cnTranslate(text) : text
|
|
320
|
+
},
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The kind of a field entry.
|
|
324
|
+
*
|
|
325
|
+
* @param {object} field The entry.
|
|
326
|
+
* @return {string} `row`, `title`, `line` or `pill`.
|
|
327
|
+
*/
|
|
328
|
+
kindOf(field) {
|
|
329
|
+
if (KINDS.includes(field.as)) {
|
|
330
|
+
return field.as
|
|
331
|
+
}
|
|
332
|
+
return field.label || field.icon ? 'row' : 'line'
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* The display text of an entry of any kind: a lead's title, the parts of
|
|
337
|
+
* a line or text, a constant `text`, else the value at `path`.
|
|
338
|
+
*
|
|
339
|
+
* @param {object} record The object.
|
|
340
|
+
* @param {object} field The entry.
|
|
341
|
+
* @param {string} kind The entry's kind.
|
|
342
|
+
* @return {string}
|
|
343
|
+
* @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead
|
|
344
|
+
*/
|
|
345
|
+
entryValue(record, field, kind) {
|
|
346
|
+
if (kind === 'lead') {
|
|
347
|
+
return this.partsValue(record, field.title)
|
|
348
|
+
}
|
|
349
|
+
if ((kind === 'line' || kind === 'text') && Array.isArray(field.parts)) {
|
|
350
|
+
return this.lineValue(record, field)
|
|
351
|
+
}
|
|
352
|
+
if ((kind === 'line' || kind === 'text') && !field.path && typeof field.text === 'string') {
|
|
353
|
+
return this.tr(field.text)
|
|
354
|
+
}
|
|
355
|
+
return this.valueOf(record, field)
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* A lead's title or subtitle: a path, a part spec, or an array of parts
|
|
360
|
+
* joined as a line joins them.
|
|
361
|
+
*
|
|
362
|
+
* @param {object} record The object.
|
|
363
|
+
* @param {string|object|Array} spec The declaration.
|
|
364
|
+
* @return {string}
|
|
365
|
+
*/
|
|
366
|
+
partsValue(record, spec) {
|
|
367
|
+
if (typeof spec === 'string' && spec !== '') {
|
|
368
|
+
return this.valueOf(record, { path: spec })
|
|
369
|
+
}
|
|
370
|
+
if (Array.isArray(spec)) {
|
|
371
|
+
return this.lineValue(record, { parts: spec })
|
|
372
|
+
}
|
|
373
|
+
if (spec && typeof spec === 'object') {
|
|
374
|
+
return this.lineValue(record, { parts: [spec] })
|
|
375
|
+
}
|
|
376
|
+
return ''
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* The initials of a name: "Sanne de Vries" reads "SV".
|
|
381
|
+
*
|
|
382
|
+
* @param {unknown} name The name.
|
|
383
|
+
* @return {string}
|
|
384
|
+
*/
|
|
385
|
+
initialsOf(name) {
|
|
386
|
+
if (typeof name !== 'string' || name.trim() === '') {
|
|
387
|
+
return ''
|
|
388
|
+
}
|
|
389
|
+
const words = name.trim().split(/\s+/).filter((word) => /^\p{Lu}/u.test(word));
|
|
390
|
+
const picked = words.length ? words : name.trim().split(/\s+/);
|
|
391
|
+
const first = picked[0].charAt(0);
|
|
392
|
+
const last = picked.length > 1 ? picked[picked.length - 1].charAt(0) : '';
|
|
393
|
+
return (first + last).toUpperCase()
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* What a row adds to its value: an icon, a link for the email, phone
|
|
398
|
+
* and url formats, the masked text, and the copy and reveal buttons.
|
|
399
|
+
*
|
|
400
|
+
* @param {object} record The object.
|
|
401
|
+
* @param {object} field The entry.
|
|
402
|
+
* @param {string} value The display text.
|
|
403
|
+
* @return {object}
|
|
404
|
+
* @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead
|
|
405
|
+
*/
|
|
406
|
+
rowExtras(record, field, value) {
|
|
407
|
+
const extras = {
|
|
408
|
+
icon: typeof field.icon === 'string' ? field.icon : '',
|
|
409
|
+
path: field.path || '',
|
|
410
|
+
raw: readPath(record, field.path),
|
|
411
|
+
copy: field.copy === true && value !== '',
|
|
412
|
+
reveal: false,
|
|
413
|
+
masked: '',
|
|
414
|
+
href: '',
|
|
415
|
+
external: false,
|
|
416
|
+
};
|
|
417
|
+
if (value === '') {
|
|
418
|
+
return extras
|
|
419
|
+
}
|
|
420
|
+
if (field.format === 'email') {
|
|
421
|
+
extras.href = `mailto:${value}`;
|
|
422
|
+
} else if (field.format === 'phone') {
|
|
423
|
+
extras.href = `tel:${value.replace(/[^\d+]/g, '')}`;
|
|
424
|
+
} else if (field.format === 'url') {
|
|
425
|
+
extras.href = /^https?:\/\//i.test(value) ? value : `https://${value}`;
|
|
426
|
+
extras.external = true;
|
|
427
|
+
} else if (field.format === 'masked') {
|
|
428
|
+
extras.masked = value.length > 4 ? '*'.repeat(Math.min(value.length - 4, 5)) + value.slice(-4) : value;
|
|
429
|
+
extras.reveal = field.reveal === true && value.length > 4;
|
|
430
|
+
}
|
|
431
|
+
return extras
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Show a masked value, and say so: the `reveal` event and the event-bus
|
|
436
|
+
* channel {@link FACTS_REVEAL_CHANNEL}, so an app can log who saw it.
|
|
437
|
+
*
|
|
438
|
+
* @param {object} row The row.
|
|
439
|
+
* @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead
|
|
440
|
+
*/
|
|
441
|
+
reveal(row) {
|
|
442
|
+
this.revealed = { ...this.revealed, [row.key]: true };
|
|
443
|
+
const detail = { path: row.path };
|
|
444
|
+
/**
|
|
445
|
+
* @event reveal Emitted when the reader shows a masked value.
|
|
446
|
+
* @type {{path: string}} The path of the shown field.
|
|
447
|
+
*/
|
|
448
|
+
this.$emit('reveal', detail);
|
|
449
|
+
emit(FACTS_REVEAL_CHANNEL, { ...detail, objectId: this.tokenCtx.objectId, register: this.tokenCtx.register || null, schema: this.tokenCtx.schema || null });
|
|
450
|
+
},
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Copy a row's value to the clipboard.
|
|
454
|
+
*
|
|
455
|
+
* @param {object} row The row.
|
|
456
|
+
* @return {Promise<void>}
|
|
457
|
+
*/
|
|
458
|
+
async copy(row) {
|
|
459
|
+
const text = row.raw === null || row.raw === undefined || typeof row.raw === 'object' ? row.value : String(row.raw);
|
|
460
|
+
try {
|
|
461
|
+
await navigator.clipboard.writeText(text);
|
|
462
|
+
this.copied = row.key;
|
|
463
|
+
setTimeout(() => {
|
|
464
|
+
if (this.copied === row.key) {
|
|
465
|
+
this.copied = '';
|
|
466
|
+
}
|
|
467
|
+
}, 2000);
|
|
468
|
+
} catch (e) {
|
|
469
|
+
// eslint-disable-next-line no-console
|
|
470
|
+
console.warn('[CnFactsWidget] could not copy:', e?.message || e);
|
|
471
|
+
}
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* @param {object} row The row.
|
|
476
|
+
* @return {string} The copy button's accessible name.
|
|
477
|
+
*/
|
|
478
|
+
copyLabel(row) {
|
|
479
|
+
return this.copied === row.key
|
|
480
|
+
? translate('nextcloud-vue', 'Copied')
|
|
481
|
+
: translate('nextcloud-vue', 'Copy {label}', { label: row.label || row.value })
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* @param {object} row The row.
|
|
486
|
+
* @return {string} The reveal button's accessible name.
|
|
487
|
+
*/
|
|
488
|
+
revealLabel(row) {
|
|
489
|
+
return translate('nextcloud-vue', 'Show {label}', { label: row.label || '' }).trim()
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* The drawn part of an action declaration.
|
|
494
|
+
*
|
|
495
|
+
* @param {object} action The declaration.
|
|
496
|
+
* @return {{label: string, icon: string}|null}
|
|
497
|
+
*/
|
|
498
|
+
actionView(action) {
|
|
499
|
+
if (!action || typeof action !== 'object' || typeof action.label !== 'string' || action.label === '') {
|
|
500
|
+
return null
|
|
501
|
+
}
|
|
502
|
+
return { label: this.tr(action.label), icon: typeof action.icon === 'string' ? action.icon : '' }
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Dispatch an action with the record's tokens resolved (`@objectId`,
|
|
507
|
+
* `@object.<field>`), through the page's dispatcher when there is one.
|
|
508
|
+
*
|
|
509
|
+
* @param {object} action The declaration.
|
|
510
|
+
* @return {unknown}
|
|
511
|
+
* @spec openspec/changes/screens-relation-pages/specs/relation-pages/spec.md#requirement-the-facts-card-draws-contact-details-identity-and-a-lead
|
|
512
|
+
*/
|
|
513
|
+
runAction(action) {
|
|
514
|
+
if (!action || typeof action !== 'object') {
|
|
515
|
+
return undefined
|
|
516
|
+
}
|
|
517
|
+
const resolved = resolveDeepTokens(action, this.tokenCtx);
|
|
518
|
+
if (typeof this.cnDispatchAction === 'function') {
|
|
519
|
+
return this.cnDispatchAction(resolved)
|
|
520
|
+
}
|
|
521
|
+
return dispatchAction(resolved, { router: this.$router || null })
|
|
522
|
+
},
|
|
523
|
+
|
|
524
|
+
/** Tell the card around the facts what its header shows (the header action). */
|
|
525
|
+
emitHeader() {
|
|
526
|
+
const view = this.headerAction;
|
|
527
|
+
/**
|
|
528
|
+
* @event header Emitted with the card header's parts: the header action, if any.
|
|
529
|
+
* @type {{link: null, note: string, action: (object|null)}} `action` is `{ label, icon, run }`.
|
|
530
|
+
*/
|
|
531
|
+
this.$emit('header', {
|
|
532
|
+
link: null,
|
|
533
|
+
note: '',
|
|
534
|
+
action: view ? { ...view, run: () => this.runAction(this.content.headerAction) } : null,
|
|
535
|
+
});
|
|
536
|
+
},
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Read the source object.
|
|
540
|
+
*
|
|
541
|
+
* @return {Promise<void>}
|
|
542
|
+
*/
|
|
543
|
+
async load() {
|
|
544
|
+
const source = this.resolvedSource;
|
|
545
|
+
this.error = false;
|
|
546
|
+
if (!source) {
|
|
547
|
+
this.fetched = null;
|
|
548
|
+
return
|
|
549
|
+
}
|
|
550
|
+
const url = this.sourceUrl(source);
|
|
551
|
+
if (!url) {
|
|
552
|
+
this.fetched = null;
|
|
553
|
+
return
|
|
554
|
+
}
|
|
555
|
+
this.loading = true;
|
|
556
|
+
try {
|
|
557
|
+
const [{ default: axios }, { generateUrl }] = await Promise.all([
|
|
558
|
+
import('@nextcloud/axios'),
|
|
559
|
+
import('@nextcloud/router'),
|
|
560
|
+
]);
|
|
561
|
+
const full = url.startsWith('/apps/') || url.startsWith('/ocs/') ? generateUrl(url) : url;
|
|
562
|
+
const response = await axios.get(full);
|
|
563
|
+
const data = response && response.data;
|
|
564
|
+
this.fetched = data && typeof data === 'object' && !Array.isArray(data) ? data : null;
|
|
565
|
+
} catch (e) {
|
|
566
|
+
// eslint-disable-next-line no-console
|
|
567
|
+
console.warn('[CnFactsWidget] the source could not be read:', e?.message || e);
|
|
568
|
+
this.fetched = null;
|
|
569
|
+
this.error = true;
|
|
570
|
+
} finally {
|
|
571
|
+
this.loading = false;
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* The URL of a resolved source, or '' when it cannot be read yet.
|
|
577
|
+
*
|
|
578
|
+
* @param {object} source The resolved source.
|
|
579
|
+
* @return {string}
|
|
580
|
+
*/
|
|
581
|
+
sourceUrl(source) {
|
|
582
|
+
if (typeof source.url === 'string') {
|
|
583
|
+
return source.url
|
|
584
|
+
}
|
|
585
|
+
if (hasUnresolvedDeepTokens(source)) {
|
|
586
|
+
return ''
|
|
587
|
+
}
|
|
588
|
+
const { register, schema, id } = source;
|
|
589
|
+
if (!register || !schema || id === undefined || id === null || id === '' || typeof id === 'object') {
|
|
590
|
+
return ''
|
|
591
|
+
}
|
|
592
|
+
return `/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}/${encodeURIComponent(String(id))}`
|
|
593
|
+
},
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* The display text of one field.
|
|
597
|
+
*
|
|
598
|
+
* @param {object} record The object.
|
|
599
|
+
* @param {object} field The entry.
|
|
600
|
+
* @return {string}
|
|
601
|
+
*/
|
|
602
|
+
valueOf(record, field) {
|
|
603
|
+
const raw = readPath(record, field.path);
|
|
604
|
+
return this.format(raw, field)
|
|
605
|
+
},
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* The text of a `line` entry that joins several parts.
|
|
609
|
+
*
|
|
610
|
+
* @param {object} record The object.
|
|
611
|
+
* @param {object} field The entry.
|
|
612
|
+
* @return {string}
|
|
613
|
+
*/
|
|
614
|
+
lineValue(record, field) {
|
|
615
|
+
const separator = typeof field.separator === 'string' ? field.separator : ' · ';
|
|
616
|
+
return field.parts
|
|
617
|
+
.map((part) => {
|
|
618
|
+
const spec = typeof part === 'string' ? { path: part } : (part || {});
|
|
619
|
+
// A constant part (`{ text }`): "Handelsregister" after the legal form.
|
|
620
|
+
if (!spec.path && typeof spec.text === 'string') {
|
|
621
|
+
return this.tr(spec.text)
|
|
622
|
+
}
|
|
623
|
+
const value = this.format(readPath(record, spec.path), spec);
|
|
624
|
+
if (value === '') {
|
|
625
|
+
return ''
|
|
626
|
+
}
|
|
627
|
+
return spec.label ? `${this.tr(spec.label)} ${value}` : value
|
|
628
|
+
})
|
|
629
|
+
.filter((value) => value !== '')
|
|
630
|
+
.join(separator)
|
|
631
|
+
},
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Format a raw value.
|
|
635
|
+
*
|
|
636
|
+
* @param {unknown} raw The value.
|
|
637
|
+
* @param {object} spec `{ format?, map?, itemPath? }`.
|
|
638
|
+
* @return {string} The text, '' when empty.
|
|
639
|
+
*/
|
|
640
|
+
format(raw, spec) {
|
|
641
|
+
if (raw === null || raw === undefined || raw === '') {
|
|
642
|
+
return ''
|
|
643
|
+
}
|
|
644
|
+
const format = spec.format || 'text';
|
|
645
|
+
if (format === 'list' || Array.isArray(raw)) {
|
|
646
|
+
const items = (Array.isArray(raw) ? raw : [raw])
|
|
647
|
+
.map((item) => (spec.itemPath && item && typeof item === 'object' ? readPath(item, spec.itemPath) : item))
|
|
648
|
+
.map((item) => this.format(item, { ...spec, format: 'text' }))
|
|
649
|
+
.filter((item) => item !== '');
|
|
650
|
+
return items.join(', ')
|
|
651
|
+
}
|
|
652
|
+
if (format === 'date' || format === 'date-long') {
|
|
653
|
+
const date = parseDateValue(raw);
|
|
654
|
+
if (date === null) {
|
|
655
|
+
return ''
|
|
656
|
+
}
|
|
657
|
+
// A year far from now forces the long form, so date-long reuses the board formatter.
|
|
658
|
+
return format === 'date-long'
|
|
659
|
+
? formatBoardDate(date, new Date(date.getFullYear() + 1, 0, 1))
|
|
660
|
+
: formatBoardDate(date)
|
|
661
|
+
}
|
|
662
|
+
if (typeof raw === 'object') {
|
|
663
|
+
const label = raw.title || raw.name || raw.label || raw.displayName;
|
|
664
|
+
return typeof label === 'string' ? label : ''
|
|
665
|
+
}
|
|
666
|
+
if (typeof raw === 'boolean') {
|
|
667
|
+
const mapped = spec.map && spec.map[String(raw)];
|
|
668
|
+
return mapped ? this.tr(mapped) : this.tr(raw ? 'Yes' : 'No')
|
|
669
|
+
}
|
|
670
|
+
if (spec.map && Object.hasOwn(spec.map, String(raw))) {
|
|
671
|
+
return this.tr(spec.map[String(raw)])
|
|
672
|
+
}
|
|
673
|
+
return String(raw)
|
|
674
|
+
},
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Replace `@source.<field>` / `@source.id` and the shared tokens in a string.
|
|
678
|
+
*
|
|
679
|
+
* @param {string} value The template.
|
|
680
|
+
* @param {object|null} record The object shown.
|
|
681
|
+
* @return {string} The value, '' when a token stays unresolved.
|
|
682
|
+
*/
|
|
683
|
+
resolveTemplate(value, record) {
|
|
684
|
+
if (typeof value !== 'string') {
|
|
685
|
+
return value
|
|
686
|
+
}
|
|
687
|
+
let unresolved = false;
|
|
688
|
+
const replaced = value.replace(/@source\.([\w.@]+)/g, (match, path) => {
|
|
689
|
+
const read = record ? (path === 'id' ? (record.id ?? readPath(record, '@self.id')) : readPath(record, path)) : undefined;
|
|
690
|
+
if (read === undefined || read === null || read === '') {
|
|
691
|
+
unresolved = true;
|
|
692
|
+
return match
|
|
693
|
+
}
|
|
694
|
+
return String(read)
|
|
695
|
+
});
|
|
696
|
+
if (unresolved) {
|
|
697
|
+
return ''
|
|
698
|
+
}
|
|
699
|
+
if (replaced.charAt(0) === '@') {
|
|
700
|
+
const resolved = resolveDeepTokens(replaced, this.tokenCtx);
|
|
701
|
+
return typeof resolved === 'string' && resolved.charAt(0) === '@' ? '' : String(resolved)
|
|
702
|
+
}
|
|
703
|
+
return replaced
|
|
704
|
+
},
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Resolve a route declaration (`{ name, params?, query? }`).
|
|
708
|
+
*
|
|
709
|
+
* @param {object} route The declaration.
|
|
710
|
+
* @param {object|null} record The object shown.
|
|
711
|
+
* @return {object|null} The route, or null when a param stays unresolved.
|
|
712
|
+
*/
|
|
713
|
+
resolveRoute(route, record) {
|
|
714
|
+
if (!route || typeof route !== 'object' || !route.name) {
|
|
715
|
+
return null
|
|
716
|
+
}
|
|
717
|
+
const out = { name: route.name };
|
|
718
|
+
for (const key of ['params', 'query']) {
|
|
719
|
+
if (route[key] && typeof route[key] === 'object') {
|
|
720
|
+
out[key] = {};
|
|
721
|
+
for (const [name, value] of Object.entries(route[key])) {
|
|
722
|
+
const resolved = this.resolveTemplate(value, record);
|
|
723
|
+
if (resolved === '') {
|
|
724
|
+
return null
|
|
725
|
+
}
|
|
726
|
+
out[key][name] = resolved;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return out
|
|
731
|
+
},
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
export { FACTS_REVEAL_CHANNEL, script as default };
|
|
736
|
+
//# sourceMappingURL=CnFactsWidget.vue2.js.map
|