@conduction/nextcloud-vue 2.62.0 → 2.63.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/CnAdminActionCard/CnAdminActionCard.vue.js +10 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js.map +1 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js +189 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js.map +1 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js +72 -0
- package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js.map +1 -0
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +271 -32
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +99 -9
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +39 -18
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
- package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
- 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/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +216 -8
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +118 -50
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
- package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +51 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
- package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +36 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
- package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
- package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
- package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
- package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
- package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
- package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
- package/dist/esm/composables/useDependencyCheck.js +65 -0
- package/dist/esm/composables/useDependencyCheck.js.map +1 -0
- package/dist/esm/composables/useWalkthrough.js +218 -6
- package/dist/esm/composables/useWalkthrough.js.map +1 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +43 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +43 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +449 -0
- package/dist/esm/store/useObjectStore.js +8 -0
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/columnFilters.js +311 -0
- package/dist/esm/utils/columnFilters.js.map +1 -0
- package/dist/esm/utils/groupAutocomplete.js +123 -0
- package/dist/esm/utils/groupAutocomplete.js.map +1 -0
- package/dist/esm/utils/libraryWidgetKeys.js +2 -0
- package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
- package/dist/esm/utils/pickerOptions.js +198 -0
- package/dist/esm/utils/pickerOptions.js.map +1 -0
- package/dist/esm/utils/schema.js +83 -7
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/timelineEvents.js +174 -0
- package/dist/esm/utils/timelineEvents.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/walkthroughSignals.js +88 -0
- package/dist/esm/utils/walkthroughSignals.js.map +1 -0
- package/dist/nextcloud-vue.cjs.js +9369 -5193
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +449 -0
- package/l10n/en.json +43 -0
- package/l10n/nl.json +43 -0
- package/package.json +1 -1
- package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
- package/src/components/CnAdminActionCard/index.js +4 -0
- package/src/components/CnAppNav/CnAppNav.vue +271 -32
- package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
- package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
- package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -2
- package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
- package/src/components/CnDataTable/CnDataTable.md +3 -0
- package/src/components/CnDataTable/CnDataTable.vue +260 -4
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
- package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
- package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
- package/src/components/CnIndexPage/CnIndexPage.vue +55 -2
- package/src/components/CnIndexPage/selfModeActions.js +3 -3
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +39 -1
- package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
- package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
- package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
- package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
- package/src/components/CnTimelineWidget/index.js +4 -0
- package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
- package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
- package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
- package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
- package/src/components/index.js +2 -0
- package/src/composables/useDependencyCheck.js +61 -0
- package/src/composables/useWalkthrough.js +217 -5
- package/src/css/dashboard.css +49 -0
- package/src/css/detail-page.css +19 -0
- package/src/css/table.css +81 -0
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +23 -1
- package/src/store/useObjectStore.js +8 -0
- package/src/utils/columnFilters.js +324 -0
- package/src/utils/groupAutocomplete.js +119 -0
- package/src/utils/libraryWidgetKeys.js +2 -0
- package/src/utils/pickerOptions.js +194 -0
- package/src/utils/schema.js +83 -7
- package/src/utils/timelineEvents.js +171 -0
- package/src/utils/validateManifestV2.compiled.js +2 -2
- package/src/utils/walkthroughSignals.js +94 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Nextcloud group search for the schema-driven form's group picker
|
|
3
|
+
* (a property marked `format: 'nc-group'` or `referenceType: 'nextcloud-group'`).
|
|
4
|
+
*
|
|
5
|
+
* Asks the core autocomplete OCS endpoint for groups first: every signed-in
|
|
6
|
+
* user may call it, and it returns the group's display name. The provisioning
|
|
7
|
+
* endpoint `cloud/groups` is the fallback; it also answers non-admins, but it
|
|
8
|
+
* returns only group ids, so the id doubles as the label there.
|
|
9
|
+
*
|
|
10
|
+
* Options are `{ id: <gid>, label: <display name> }`: the form stores the gid.
|
|
11
|
+
*
|
|
12
|
+
* @module utils/groupAutocomplete
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import axios from '@nextcloud/axios'
|
|
16
|
+
import { generateOcsUrl } from '@nextcloud/router'
|
|
17
|
+
|
|
18
|
+
const OCS_HEADERS = {
|
|
19
|
+
'OCS-APIRequest': 'true',
|
|
20
|
+
Accept: 'application/json',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Read the `ocs.data` payload, tolerating a bare body.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} response An axios response.
|
|
27
|
+
* @return {unknown} The data.
|
|
28
|
+
*/
|
|
29
|
+
function ocsData(response) {
|
|
30
|
+
const body = response && response.data
|
|
31
|
+
return body && body.ocs ? body.ocs.data : body
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Map one autocomplete suggestion to an option, keeping only groups.
|
|
36
|
+
*
|
|
37
|
+
* @param {object} suggestion An autocomplete suggestion.
|
|
38
|
+
* @return {{id: string, label: string}|null} The option, or null when it is not a group.
|
|
39
|
+
*/
|
|
40
|
+
function toGroupOption(suggestion) {
|
|
41
|
+
if (!suggestion || typeof suggestion !== 'object') {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
const isGroup = suggestion.source === 'groups'
|
|
45
|
+
|| suggestion.shareType === 1
|
|
46
|
+
|| (suggestion.value && suggestion.value.shareType === 1)
|
|
47
|
+
if (!isGroup) {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
const gid = suggestion.id || (suggestion.value && suggestion.value.shareWith)
|
|
51
|
+
if (gid === undefined || gid === null || gid === '') {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
return { id: String(gid), label: suggestion.label || String(gid) }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Search Nextcloud groups by name or id. Fails soft: an error yields `[]`.
|
|
59
|
+
*
|
|
60
|
+
* @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
|
|
61
|
+
* @param {string} [query] The search term (empty loads a first page).
|
|
62
|
+
* @param {object} [options] Tuning options.
|
|
63
|
+
* @param {number} [options.limit] Max results (default 25).
|
|
64
|
+
* @return {Promise<Array<{id: string, label: string}>>} Picker options.
|
|
65
|
+
*/
|
|
66
|
+
export async function searchNextcloudGroups(query = '', options = {}) {
|
|
67
|
+
const { limit = 25 } = options
|
|
68
|
+
try {
|
|
69
|
+
const response = await axios.get(generateOcsUrl('core/autocomplete/get'), {
|
|
70
|
+
headers: OCS_HEADERS,
|
|
71
|
+
params: {
|
|
72
|
+
search: query || '',
|
|
73
|
+
itemType: ' ',
|
|
74
|
+
itemId: ' ',
|
|
75
|
+
'shareTypes[]': 1,
|
|
76
|
+
limit,
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
const list = ocsData(response)
|
|
80
|
+
const groups = (Array.isArray(list) ? list : [])
|
|
81
|
+
.map(toGroupOption)
|
|
82
|
+
.filter((opt) => opt !== null)
|
|
83
|
+
if (groups.length > 0) {
|
|
84
|
+
return groups
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// Fall through to the provisioning endpoint.
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const response = await axios.get(generateOcsUrl('cloud/groups'), {
|
|
91
|
+
headers: OCS_HEADERS,
|
|
92
|
+
params: { search: query || '', limit },
|
|
93
|
+
})
|
|
94
|
+
const data = ocsData(response)
|
|
95
|
+
const gids = data && Array.isArray(data.groups) ? data.groups : []
|
|
96
|
+
return gids
|
|
97
|
+
.filter((gid) => typeof gid === 'string' && gid !== '')
|
|
98
|
+
.map((gid) => ({ id: gid, label: gid }))
|
|
99
|
+
} catch {
|
|
100
|
+
return []
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a stored gid to `{ id, label }` for edit-mode display. Falls back
|
|
106
|
+
* to the gid itself when the name cannot be looked up.
|
|
107
|
+
*
|
|
108
|
+
* @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
|
|
109
|
+
* @param {string} gid The stored group id.
|
|
110
|
+
* @return {Promise<{id: string, label: string}>} The resolved option.
|
|
111
|
+
*/
|
|
112
|
+
export async function resolveNextcloudGroup(gid) {
|
|
113
|
+
const fallback = { id: String(gid), label: String(gid) }
|
|
114
|
+
if (gid === undefined || gid === null || gid === '') {
|
|
115
|
+
return fallback
|
|
116
|
+
}
|
|
117
|
+
const results = await searchNextcloudGroups(String(gid))
|
|
118
|
+
return results.find((opt) => opt.id === String(gid)) || fallback
|
|
119
|
+
}
|
|
@@ -67,6 +67,7 @@ export const BUILT_IN_WIDGET_KEYS = Object.freeze([
|
|
|
67
67
|
'integration',
|
|
68
68
|
'banner',
|
|
69
69
|
'audit-trail',
|
|
70
|
+
'timeline',
|
|
70
71
|
'header',
|
|
71
72
|
'text',
|
|
72
73
|
'divider',
|
|
@@ -131,6 +132,7 @@ export const DASHBOARD_CATALOG_WIDGET_KEYS = Object.freeze([
|
|
|
131
132
|
'tasks',
|
|
132
133
|
'text',
|
|
133
134
|
'tile',
|
|
135
|
+
'timeline',
|
|
134
136
|
'video',
|
|
135
137
|
'week-strip',
|
|
136
138
|
'workspace-filter',
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Option lists for the language and time zone pickers, and the defaults a
|
|
3
|
+
* schema can ask for with `x-default`.
|
|
4
|
+
*
|
|
5
|
+
* Both pickers store a code (`nl`, `Europe/Amsterdam`) and show a label in
|
|
6
|
+
* the user's own language, so a person picks "Nederlands" and the record
|
|
7
|
+
* stores `nl`.
|
|
8
|
+
*
|
|
9
|
+
* @module utils/pickerOptions
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getLanguage } from '@nextcloud/l10n'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* ISO 639-1 language codes. `Intl` has no list of languages, so the codes
|
|
16
|
+
* live here; the labels come from `Intl.DisplayNames` at runtime.
|
|
17
|
+
*
|
|
18
|
+
* @type {string[]}
|
|
19
|
+
*/
|
|
20
|
+
export const LANGUAGE_CODES = (''
|
|
21
|
+
+ 'aa ab af ak am an ar as av ay az ba be bg bi bm bn bo br bs '
|
|
22
|
+
+ 'ca ce ch co cr cs cu cv cy da de dv dz ee el en eo es et eu '
|
|
23
|
+
+ 'fa ff fi fj fo fr fy ga gd gl gn gu gv ha he hi ho hr ht hu '
|
|
24
|
+
+ 'hy hz ia id ie ig ii ik io is it iu ja jv ka kg ki kj kk kl '
|
|
25
|
+
+ 'km kn ko kr ks ku kv kw ky la lb lg li ln lo lt lu lv mg mh '
|
|
26
|
+
+ 'mi mk ml mn mr ms mt my na nb nd ne ng nl nn no nr nv ny oc '
|
|
27
|
+
+ 'oj om or os pa pi pl ps pt qu rm rn ro ru rw sa sc sd se sg '
|
|
28
|
+
+ 'si sk sl sm sn so sq sr ss st su sv sw ta te tg th ti tk tl '
|
|
29
|
+
+ 'tn to tr ts tt tw ty ug uk ur uz ve vi vo wa wo xh yi yo za '
|
|
30
|
+
+ 'zh zu'
|
|
31
|
+
).split(' ')
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A small fallback for runtimes without `Intl.supportedValuesOf`.
|
|
35
|
+
*
|
|
36
|
+
* @type {string[]}
|
|
37
|
+
*/
|
|
38
|
+
const FALLBACK_TIMEZONES = 'UTC Europe/Amsterdam Europe/Berlin Europe/Brussels Europe/London Europe/Paris America/New_York America/Chicago America/Los_Angeles Asia/Tokyo Australia/Sydney'.split(' ')
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The user's Nextcloud language as a BCP 47 tag (`en_GB` becomes `en-GB`).
|
|
42
|
+
*
|
|
43
|
+
* @return {string} The tag, `en` when Nextcloud reports nothing.
|
|
44
|
+
*/
|
|
45
|
+
export function currentLanguageTag() {
|
|
46
|
+
let lang
|
|
47
|
+
try {
|
|
48
|
+
lang = getLanguage()
|
|
49
|
+
} catch {
|
|
50
|
+
lang = ''
|
|
51
|
+
}
|
|
52
|
+
return String(lang || 'en').replace(/_/g, '-')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The browser's time zone (IANA id).
|
|
57
|
+
*
|
|
58
|
+
* @return {string} The time zone, `UTC` when the runtime does not say.
|
|
59
|
+
*/
|
|
60
|
+
export function currentTimezone() {
|
|
61
|
+
try {
|
|
62
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
63
|
+
} catch {
|
|
64
|
+
return 'UTC'
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Cached option lists, keyed by display language. Stable identity matters:
|
|
70
|
+
* the form's select matches the chosen option by reference.
|
|
71
|
+
*
|
|
72
|
+
* @type {Map<string, {codes: string[], labels: object}>}
|
|
73
|
+
*/
|
|
74
|
+
const languageCache = new Map()
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The language picker's codes and labels, labelled in `displayLanguage`.
|
|
78
|
+
*
|
|
79
|
+
* @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
|
|
80
|
+
* @param {string} [displayLanguage] BCP 47 tag the labels are written in; defaults to the user's language.
|
|
81
|
+
* @return {{codes: string[], labels: object}} Codes sorted by label, and `{ code: label }`.
|
|
82
|
+
*/
|
|
83
|
+
export function languageOptions(displayLanguage) {
|
|
84
|
+
const locale = displayLanguage || currentLanguageTag()
|
|
85
|
+
if (languageCache.has(locale)) {
|
|
86
|
+
return languageCache.get(locale)
|
|
87
|
+
}
|
|
88
|
+
let names
|
|
89
|
+
try {
|
|
90
|
+
names = new Intl.DisplayNames([locale, 'en'], { type: 'language' })
|
|
91
|
+
} catch {
|
|
92
|
+
names = null
|
|
93
|
+
}
|
|
94
|
+
const labels = {}
|
|
95
|
+
for (const code of LANGUAGE_CODES) {
|
|
96
|
+
let label
|
|
97
|
+
try {
|
|
98
|
+
label = (names && names.of(code)) || code
|
|
99
|
+
} catch {
|
|
100
|
+
label = code
|
|
101
|
+
}
|
|
102
|
+
// `Intl` lower-cases some names ("nederlands" is fine in a sentence,
|
|
103
|
+
// not in a list); capitalise the first letter for the list.
|
|
104
|
+
labels[code] = label.charAt(0).toLocaleUpperCase(locale) + label.slice(1)
|
|
105
|
+
}
|
|
106
|
+
const codes = [...LANGUAGE_CODES].sort((a, b) => labels[a].localeCompare(labels[b], locale))
|
|
107
|
+
const result = { codes, labels }
|
|
108
|
+
languageCache.set(locale, result)
|
|
109
|
+
return result
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** @type {{codes: string[], labels: object}|null} */
|
|
113
|
+
let timezoneCache = null
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The time zone picker's codes and labels (`Europe/Amsterdam` shows as
|
|
117
|
+
* `Europe/Amsterdam`, underscores as spaces).
|
|
118
|
+
*
|
|
119
|
+
* @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
|
|
120
|
+
* @return {{codes: string[], labels: object}} IANA ids, and `{ id: label }`.
|
|
121
|
+
*/
|
|
122
|
+
export function timezoneOptions() {
|
|
123
|
+
if (timezoneCache) {
|
|
124
|
+
return timezoneCache
|
|
125
|
+
}
|
|
126
|
+
let codes
|
|
127
|
+
try {
|
|
128
|
+
codes = typeof Intl.supportedValuesOf === 'function' ? Intl.supportedValuesOf('timeZone') : []
|
|
129
|
+
} catch {
|
|
130
|
+
codes = []
|
|
131
|
+
}
|
|
132
|
+
if (!codes.length) {
|
|
133
|
+
codes = FALLBACK_TIMEZONES
|
|
134
|
+
}
|
|
135
|
+
if (!codes.includes('UTC')) {
|
|
136
|
+
codes = ['UTC', ...codes]
|
|
137
|
+
}
|
|
138
|
+
const labels = {}
|
|
139
|
+
for (const code of codes) {
|
|
140
|
+
labels[code] = code.replace(/_/g, ' ')
|
|
141
|
+
}
|
|
142
|
+
timezoneCache = { codes: [...codes], labels }
|
|
143
|
+
return timezoneCache
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Fit a language tag to the property's `pattern`, if it has one.
|
|
148
|
+
*
|
|
149
|
+
* A schema that stores ISO 639-1 (`^[a-z]{2}$`) cannot take `en-GB`, so the
|
|
150
|
+
* tag falls back to its primary subtag. Without a pattern the full tag stays.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} tag A BCP 47 tag.
|
|
153
|
+
* @param {string|undefined} pattern The property's `pattern`.
|
|
154
|
+
* @return {string} The tag as the schema accepts it.
|
|
155
|
+
*/
|
|
156
|
+
function fitLanguageTag(tag, pattern) {
|
|
157
|
+
if (!pattern) {
|
|
158
|
+
return tag
|
|
159
|
+
}
|
|
160
|
+
let re
|
|
161
|
+
try {
|
|
162
|
+
re = new RegExp(pattern)
|
|
163
|
+
} catch {
|
|
164
|
+
return tag
|
|
165
|
+
}
|
|
166
|
+
if (re.test(tag)) {
|
|
167
|
+
return tag
|
|
168
|
+
}
|
|
169
|
+
const primary = tag.split('-')[0].toLowerCase()
|
|
170
|
+
return re.test(primary) ? primary : tag
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Resolve an `x-default` token to the value a NEW object starts with.
|
|
175
|
+
*
|
|
176
|
+
* - `current-language`: the user's Nextcloud language (BCP 47, fitted to the
|
|
177
|
+
* property's `pattern`).
|
|
178
|
+
* - `current-timezone`: the browser's time zone.
|
|
179
|
+
*
|
|
180
|
+
* @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
|
|
181
|
+
* @param {string|null} token The `x-default` value.
|
|
182
|
+
* @param {object} [field] The field descriptor (reads `validation.pattern`).
|
|
183
|
+
* @return {string|null} The value, or null for an unknown token.
|
|
184
|
+
*/
|
|
185
|
+
export function resolveDefaultToken(token, field = {}) {
|
|
186
|
+
if (token === 'current-language') {
|
|
187
|
+
const pattern = field && field.validation ? field.validation.pattern : undefined
|
|
188
|
+
return fitLanguageTag(currentLanguageTag(), pattern)
|
|
189
|
+
}
|
|
190
|
+
if (token === 'current-timezone') {
|
|
191
|
+
return currentTimezone()
|
|
192
|
+
}
|
|
193
|
+
return null
|
|
194
|
+
}
|
package/src/utils/schema.js
CHANGED
|
@@ -297,10 +297,13 @@ function truncateString(str, maxLength) {
|
|
|
297
297
|
* surface (CnFormDialog) resolves the reference to a searchable dropdown
|
|
298
298
|
* of the referenced objects (label = human name, value = UUID).
|
|
299
299
|
* 4. Nextcloud user reference: `referenceType: 'nextcloud-user'` (or
|
|
300
|
-
* `format: 'user'`/`'username'`) → `'user'`; an array of such
|
|
300
|
+
* `format: 'user'`/`'username'`/`'nc-user'`) → `'user'`; an array of such
|
|
301
301
|
* properties → `'user-multiselect'`. CnFormDialog resolves these to a
|
|
302
302
|
* searchable dropdown of real Nextcloud users (label = display name,
|
|
303
|
-
* value = UID).
|
|
303
|
+
* value = UID). A Nextcloud group (`referenceType: 'nextcloud-group'` or
|
|
304
|
+
* `format: 'nc-group'`) → `'group'`, an array of groups →
|
|
305
|
+
* `'group-multiselect'`. `format: 'language'` → `'language'` and
|
|
306
|
+
* `format: 'timezone'` → `'timezone'` (both searchable code lists).
|
|
304
307
|
* 5. Type-based: `boolean` → `'checkbox'`, `integer`/`number` → `'number'`,
|
|
305
308
|
* `array` + `items.enum` → `'multiselect'`, `array` → `'tags'`
|
|
306
309
|
* 6. Format-based: `date-time` → `'datetime'`, `date` → `'date'`,
|
|
@@ -368,9 +371,32 @@ function isUserProp(prop) {
|
|
|
368
371
|
return true
|
|
369
372
|
}
|
|
370
373
|
const format = prop.format || ''
|
|
371
|
-
return format === 'user' || format === 'username'
|
|
374
|
+
return format === 'user' || format === 'username' || format === 'nc-user'
|
|
372
375
|
}
|
|
373
376
|
|
|
377
|
+
/**
|
|
378
|
+
* Whether a (single-value) schema property represents a Nextcloud group.
|
|
379
|
+
*
|
|
380
|
+
* A property is a group field when it declares `referenceType: 'nextcloud-group'`
|
|
381
|
+
* or `format: 'nc-group'`. CnFormDialog renders it as a searchable dropdown of
|
|
382
|
+
* Nextcloud groups (label = display name, value = group id).
|
|
383
|
+
*
|
|
384
|
+
* @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
|
|
385
|
+
* @param {object} prop A schema property definition (or `items` for an array).
|
|
386
|
+
* @return {boolean} True when the property marks a Nextcloud group.
|
|
387
|
+
*/
|
|
388
|
+
function isGroupProp(prop) {
|
|
389
|
+
if (!prop || typeof prop !== 'object') {
|
|
390
|
+
return false
|
|
391
|
+
}
|
|
392
|
+
return prop.referenceType === 'nextcloud-group' || prop.format === 'nc-group'
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
|
|
397
|
+
* @param {object} prop The schema property definition.
|
|
398
|
+
* @return {string} The widget identifier (see the block above).
|
|
399
|
+
*/
|
|
374
400
|
function resolveWidget(prop) {
|
|
375
401
|
// Explicit widget hint takes priority
|
|
376
402
|
if (prop.widget) {
|
|
@@ -409,6 +435,24 @@ function resolveWidget(prop) {
|
|
|
409
435
|
return 'user-multiselect'
|
|
410
436
|
}
|
|
411
437
|
|
|
438
|
+
// Nextcloud group (`format: 'nc-group'` / `referenceType: 'nextcloud-group'`):
|
|
439
|
+
// a searchable dropdown of groups storing the group id.
|
|
440
|
+
if (isGroupProp(prop)) {
|
|
441
|
+
return 'group'
|
|
442
|
+
}
|
|
443
|
+
if (type === 'array' && isGroupProp(prop.items)) {
|
|
444
|
+
return 'group-multiselect'
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Language and time zone codes: a searchable list instead of a free-text
|
|
448
|
+
// box, so a person picks "Dutch" and the record stores `nl`.
|
|
449
|
+
if (type === 'string' && format === 'language') {
|
|
450
|
+
return 'language'
|
|
451
|
+
}
|
|
452
|
+
if (type === 'string' && format === 'timezone') {
|
|
453
|
+
return 'timezone'
|
|
454
|
+
}
|
|
455
|
+
|
|
412
456
|
// Boolean → switch/checkbox
|
|
413
457
|
if (type === 'boolean') {
|
|
414
458
|
return 'checkbox'
|
|
@@ -587,7 +631,8 @@ export function isTenantProperty(key, prop) {
|
|
|
587
631
|
* @param {boolean} [options.includeReadOnly] Whether to include readOnly properties
|
|
588
632
|
* @param {boolean} [options.hideTenant] Drop properties that hold the record's tenant (see `isTenantProperty`). Off by default, so a detail page still shows the tenant; CnFormDialog turns it on because nobody should be asked for it. `overrides[key].hidden === false` keeps one visible.
|
|
589
633
|
* @param {(text: string) => string} [options.translate] Optional display-layer translation function applied to each field's `label` and `description`. Schema property titles/descriptions are authored in English as the canonical source; consumers pass their bound `t()` (via the injected `cnTranslate`) so the rendered field label follows the user's language. When omitted, label/description are the English source strings unchanged (pure, backward-compatible).
|
|
590
|
-
* @
|
|
634
|
+
* @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
|
|
635
|
+
* @return {Array<{key: string, label: string, description: string, descriptionLong: string, type: string, format: string|null, widget: string, required: boolean, readOnly: boolean, default: unknown, enum: Array|null, enumLabels: object|null, items: object|null, referenceType: string|null, referenceSemanticType: string|null, referenceSemanticApp: string|null, reference: {schema: string|number, multiple: boolean, register?: string, labelField?: string}|null, allowCreate: boolean, userPicker: {multiple: boolean}|null, groupPicker: {multiple: boolean}|null, defaultToken: string|null, fillFrom: object|null, validation: object, order: number}>} `description` is the inline helper text (see `splitDescription`); `descriptionLong` carries the property's `x-help` text when it declares one, else the full description when it was too long to render inline, else ''. `enumLabels` maps each raw enum value to its English display label (from the property's `x-enum-labels`), or null.
|
|
591
636
|
*/
|
|
592
637
|
export function fieldsFromSchema(schema, options = {}) {
|
|
593
638
|
const { exclude = [], include = null, overrides = {}, includeReadOnly = false, hideTenant = false, translate } = options
|
|
@@ -663,11 +708,19 @@ export function fieldsFromSchema(schema, options = {}) {
|
|
|
663
708
|
|
|
664
709
|
return entries.map(([key, prop]) => {
|
|
665
710
|
const description = splitDescription(prop.description ? tr(prop.description) : '')
|
|
711
|
+
// `x-help` is the long explanation behind the (i) popover. It always
|
|
712
|
+
// shows the popover, while `description` stays the short helper line.
|
|
713
|
+
const help = typeof prop['x-help'] === 'string' && prop['x-help'].trim() !== ''
|
|
714
|
+
? tr(prop['x-help'].trim())
|
|
715
|
+
: ''
|
|
716
|
+
const labelField = prop['x-label-field']
|
|
717
|
+
|| (prop.items && prop.items['x-label-field'])
|
|
718
|
+
|| null
|
|
666
719
|
const field = {
|
|
667
720
|
key,
|
|
668
721
|
label: tr(prop.title || key),
|
|
669
722
|
description: description.short,
|
|
670
|
-
descriptionLong: description.long,
|
|
723
|
+
descriptionLong: help || description.long,
|
|
671
724
|
type: prop.type || 'string',
|
|
672
725
|
format: prop.format || null,
|
|
673
726
|
widget: resolveWidget(prop),
|
|
@@ -685,6 +738,10 @@ export function fieldsFromSchema(schema, options = {}) {
|
|
|
685
738
|
required: requiredKeys.includes(key),
|
|
686
739
|
readOnly: prop.readOnly || false,
|
|
687
740
|
default: prop.default !== undefined ? prop.default : null,
|
|
741
|
+
// A default that depends on who opens the form (`x-default:
|
|
742
|
+
// 'current-language'` / `'current-timezone'`). Resolved by the
|
|
743
|
+
// consuming form on a NEW object only; never on edit.
|
|
744
|
+
defaultToken: typeof prop['x-default'] === 'string' ? prop['x-default'] : null,
|
|
688
745
|
enum: prop.enum || null,
|
|
689
746
|
// Display labels for the enum CODES, keyed by raw value. An enum
|
|
690
747
|
// value is a stored contract value, not display text — several are
|
|
@@ -732,11 +789,23 @@ export function fieldsFromSchema(schema, options = {}) {
|
|
|
732
789
|
// form's own; absent, the reference stays same-register (the form's
|
|
733
790
|
// `register` prop is used). Consolidates the ad-hoc caseReference /
|
|
734
791
|
// approvalDecisionId / x-mirror-of variants onto one convention.
|
|
792
|
+
//
|
|
793
|
+
// `x-label-field` names the referenced object's property that labels
|
|
794
|
+
// an option and that an inline create writes the typed term to
|
|
795
|
+
// (`reference.labelField`, default `name` at the consumer).
|
|
735
796
|
reference: (normalizeRef(prop.$ref) !== null)
|
|
736
|
-
? { schema: normalizeRef(prop.$ref), multiple: false, ...(prop['x-external-register'] ? { register: prop['x-external-register'] } : {}) }
|
|
797
|
+
? { schema: normalizeRef(prop.$ref), multiple: false, ...(prop['x-external-register'] ? { register: prop['x-external-register'] } : {}), ...(labelField ? { labelField } : {}) }
|
|
737
798
|
: (prop.type === 'array' && prop.items && normalizeRef(prop.items.$ref) !== null)
|
|
738
|
-
? { schema: normalizeRef(prop.items.$ref), multiple: true, ...((prop.items['x-external-register'] || prop['x-external-register']) ? { register: prop.items['x-external-register'] || prop['x-external-register'] } : {}) }
|
|
799
|
+
? { schema: normalizeRef(prop.items.$ref), multiple: true, ...((prop.items['x-external-register'] || prop['x-external-register']) ? { register: prop.items['x-external-register'] || prop['x-external-register'] } : {}), ...(labelField ? { labelField } : {}) }
|
|
739
800
|
: null,
|
|
801
|
+
// Select OR create (`x-allow-create: true`, on the property or on
|
|
802
|
+
// its `items` for an array): the picker offers "Create" next to the
|
|
803
|
+
// existing objects. Only meaningful on a reference.
|
|
804
|
+
allowCreate: prop['x-allow-create'] === true
|
|
805
|
+
|| (prop.type === 'array' && !!prop.items && prop.items['x-allow-create'] === true),
|
|
806
|
+
// Template copy (`x-fill-from: { formKey: sourceKey }`): choosing a
|
|
807
|
+
// referenced object copies those of its values into this form.
|
|
808
|
+
fillFrom: (prop['x-fill-from'] && typeof prop['x-fill-from'] === 'object') ? prop['x-fill-from'] : null,
|
|
740
809
|
// Nextcloud user reference: when a property marks a NC user
|
|
741
810
|
// (`referenceType: 'nextcloud-user'`, or `format: 'user'`/
|
|
742
811
|
// `'username'`), tag it so CnFormDialog renders a searchable
|
|
@@ -749,6 +818,13 @@ export function fieldsFromSchema(schema, options = {}) {
|
|
|
749
818
|
: (prop.type === 'array' && isUserProp(prop.items))
|
|
750
819
|
? { multiple: true }
|
|
751
820
|
: null,
|
|
821
|
+
// Nextcloud group reference (`format: 'nc-group'` or
|
|
822
|
+
// `referenceType: 'nextcloud-group'`): the form stores the group id.
|
|
823
|
+
groupPicker: isGroupProp(prop)
|
|
824
|
+
? { multiple: false }
|
|
825
|
+
: (prop.type === 'array' && isGroupProp(prop.items))
|
|
826
|
+
? { multiple: true }
|
|
827
|
+
: null,
|
|
752
828
|
// Conditional immutability (AD: x-openregister-readonly-when): a
|
|
753
829
|
// property can declare it becomes read-only when another field on the
|
|
754
830
|
// same object holds a given value — e.g. a hybrid app's identity
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Turn an object's dated facts into timeline events.
|
|
8
|
+
*
|
|
9
|
+
* Pure helpers behind `CnTimelineWidget`. An event is
|
|
10
|
+
* `{ id, at, dateOnly, label, detail, source, upcoming }`, where `at` is a
|
|
11
|
+
* `Date`, `dateOnly` says the source value carried no time of day, and
|
|
12
|
+
* `upcoming` says the moment lies after `now`.
|
|
13
|
+
*
|
|
14
|
+
* @module utils/timelineEvents
|
|
15
|
+
* @spec openspec/changes/timeline-widget/specs/timeline-widget/spec.md
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Read a dotted path off an object (`@self.created`, `payment.clearedAt`).
|
|
20
|
+
*
|
|
21
|
+
* @param {object} obj The object.
|
|
22
|
+
* @param {string} path The dotted path.
|
|
23
|
+
* @return {unknown} The value, or undefined.
|
|
24
|
+
*/
|
|
25
|
+
export function readPath(obj, path) {
|
|
26
|
+
if (!obj || typeof path !== 'string' || path === '') {
|
|
27
|
+
return undefined
|
|
28
|
+
}
|
|
29
|
+
if (Object.hasOwn(obj, path)) {
|
|
30
|
+
return obj[path]
|
|
31
|
+
}
|
|
32
|
+
return path.split('.').reduce((acc, part) => ((acc === null || acc === undefined) ? undefined : acc[part]), obj)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parse a stored date value. A bare `YYYY-MM-DD` is a calendar day and is
|
|
37
|
+
* read as local midnight, not UTC, so it never shows as the day before.
|
|
38
|
+
*
|
|
39
|
+
* @param {unknown} raw The stored value.
|
|
40
|
+
* @return {{at: Date, dateOnly: boolean}|null} The moment, or null when it is not a date.
|
|
41
|
+
*/
|
|
42
|
+
export function parseMoment(raw) {
|
|
43
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
if (typeof raw === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(raw)) {
|
|
47
|
+
const [y, m, d] = raw.split('-').map(Number)
|
|
48
|
+
return { at: new Date(y, m - 1, d), dateOnly: true }
|
|
49
|
+
}
|
|
50
|
+
const at = new Date(raw)
|
|
51
|
+
return Number.isNaN(at.getTime()) ? null : { at, dateOnly: false }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Build one event, or null when the value is not a date.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} spec `{ id, raw, label, detail, source }`.
|
|
58
|
+
* @param {Date} now The reference moment for `upcoming`.
|
|
59
|
+
* @return {object|null} The event.
|
|
60
|
+
*/
|
|
61
|
+
function makeEvent(spec, now) {
|
|
62
|
+
const moment = parseMoment(spec.raw)
|
|
63
|
+
if (!moment) {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
id: spec.id,
|
|
68
|
+
at: moment.at,
|
|
69
|
+
dateOnly: moment.dateOnly,
|
|
70
|
+
label: spec.label || '',
|
|
71
|
+
detail: spec.detail || '',
|
|
72
|
+
source: spec.source,
|
|
73
|
+
upcoming: moment.at.getTime() > now.getTime(),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Events from the object's own date properties.
|
|
79
|
+
*
|
|
80
|
+
* @param {object} object The object.
|
|
81
|
+
* @param {Array<{field: string, label?: string}>} fields The configured date fields.
|
|
82
|
+
* @param {Date} [now] The reference moment.
|
|
83
|
+
* @return {Array<object>} One event per field that holds a date.
|
|
84
|
+
*/
|
|
85
|
+
export function fieldEvents(object, fields, now = new Date()) {
|
|
86
|
+
return (Array.isArray(fields) ? fields : [])
|
|
87
|
+
.filter((f) => f && typeof f.field === 'string')
|
|
88
|
+
.map((f) => makeEvent({ id: `field:${f.field}`, raw: readPath(object, f.field), label: f.label || f.field, source: 'field' }, now))
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Events from related objects: one per row that holds a date in `dateField`.
|
|
94
|
+
*
|
|
95
|
+
* @param {Array<object>} rows The related objects.
|
|
96
|
+
* @param {{label?: string, dateField?: string, titleField?: string, schema?: string}} config The related-source config.
|
|
97
|
+
* @param {Date} [now] The reference moment.
|
|
98
|
+
* @return {Array<object>} The events.
|
|
99
|
+
*/
|
|
100
|
+
export function relatedEvents(rows, config, now = new Date()) {
|
|
101
|
+
const cfg = config || {}
|
|
102
|
+
const dateField = cfg.dateField || '@self.created'
|
|
103
|
+
return (Array.isArray(rows) ? rows : [])
|
|
104
|
+
.map((row, i) => {
|
|
105
|
+
const self = (row && row['@self']) || {}
|
|
106
|
+
const id = String(self.id || row.id || i)
|
|
107
|
+
const title = cfg.titleField ? readPath(row, cfg.titleField) : (row && (row.title || row.name)) || self.name
|
|
108
|
+
return makeEvent({
|
|
109
|
+
id: `related:${cfg.schema || ''}:${id}`,
|
|
110
|
+
raw: readPath(row, dateField),
|
|
111
|
+
label: cfg.label || '',
|
|
112
|
+
detail: title === undefined || title === null ? '' : String(title),
|
|
113
|
+
source: 'related',
|
|
114
|
+
}, now)
|
|
115
|
+
})
|
|
116
|
+
.filter(Boolean)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Events from audit-trail entries.
|
|
121
|
+
*
|
|
122
|
+
* @param {Array<object>} entries The audit-trail rows.
|
|
123
|
+
* @param {(action: string, actor: string) => string} describe Builds the label from action and actor.
|
|
124
|
+
* @param {Date} [now] The reference moment.
|
|
125
|
+
* @return {Array<object>} The events.
|
|
126
|
+
*/
|
|
127
|
+
export function auditEvents(entries, describe, now = new Date()) {
|
|
128
|
+
return (Array.isArray(entries) ? entries : [])
|
|
129
|
+
.map((e, i) => makeEvent({
|
|
130
|
+
id: `audit:${e && (e.id || e.uuid) ? (e.id || e.uuid) : i}`,
|
|
131
|
+
raw: e && (e.created || e.creationDateTime || e.timestamp),
|
|
132
|
+
label: describe(String((e && (e.action || e.event)) || ''), String((e && (e.actorDisplayName || e.userName || e.user || e.actor)) || '')),
|
|
133
|
+
source: 'audit',
|
|
134
|
+
}, now))
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Events from OpenRegister's timeline (notes, calls, messages).
|
|
140
|
+
*
|
|
141
|
+
* @param {Array<object>} entries The timeline rows.
|
|
142
|
+
* @param {string} noteLabel The label for an entry without a kind.
|
|
143
|
+
* @param {Date} [now] The reference moment.
|
|
144
|
+
* @return {Array<object>} The events.
|
|
145
|
+
*/
|
|
146
|
+
export function timelineEntryEvents(entries, noteLabel, now = new Date()) {
|
|
147
|
+
return (Array.isArray(entries) ? entries : [])
|
|
148
|
+
.map((e, i) => makeEvent({
|
|
149
|
+
id: `timeline:${e && e.id ? e.id : i}`,
|
|
150
|
+
raw: e && e.created,
|
|
151
|
+
label: (e && (e.kindTitle || e.kind)) || noteLabel,
|
|
152
|
+
detail: [e && e.author, e && e.message].filter(Boolean).join(': '),
|
|
153
|
+
source: 'timeline',
|
|
154
|
+
}, now))
|
|
155
|
+
.filter(Boolean)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Sort events by time; ties keep their input order.
|
|
160
|
+
*
|
|
161
|
+
* @param {Array<object>} events The events.
|
|
162
|
+
* @param {'asc'|'desc'} [order] Oldest first (default) or newest first.
|
|
163
|
+
* @return {Array<object>} A new, sorted array.
|
|
164
|
+
*/
|
|
165
|
+
export function sortEvents(events, order = 'asc') {
|
|
166
|
+
const dir = order === 'desc' ? -1 : 1
|
|
167
|
+
return (events || [])
|
|
168
|
+
.map((e, i) => ({ e, i }))
|
|
169
|
+
.sort((a, b) => ((a.e.at.getTime() - b.e.at.getTime()) * dir) || (a.i - b.i))
|
|
170
|
+
.map(({ e }) => e)
|
|
171
|
+
}
|