@conduction/nextcloud-vue 2.61.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 +394 -27
- 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/CnBannerWidget/CnBannerWidget.vue2.js +79 -2
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +38 -25
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +21 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +4 -1
- package/dist/esm/components/CnBrandStripe/CnBrandStripe.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 +30 -6
- 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 +256 -23
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +119 -51
- 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/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.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/CnObjectSidebar/CnFilesTab.vue2.js +53 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +22 -5
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnReportsPage/CnReportsPage.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/CnStorePage/CnStorePage.vue2.js.map +1 -1
- package/dist/esm/components/CnStorePage/CnStorePage.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/CnWikiPage/CnWikiPage.vue2.js.map +1 -1
- package/dist/esm/components/CnWikiPage/CnWikiPage.vue3.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 +48 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +48 -0
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +557 -9
- 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 +9779 -5236
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +557 -9
- package/l10n/en.json +48 -0
- package/l10n/nl.json +48 -0
- package/package.json +3 -2
- package/scripts/manifest-schema-hash.js +41 -0
- package/scripts/record-manifest-schema-hash.js +52 -0
- package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
- package/src/components/CnAdminActionCard/index.js +4 -0
- package/src/components/CnAppNav/CnAppNav.vue +394 -27
- package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
- package/src/components/CnBannerWidget/CnBannerWidget.vue +113 -2
- package/src/components/CnBrandStripe/CnBrandStripe.md +16 -0
- package/src/components/CnBrandStripe/CnBrandStripe.vue +58 -5
- package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
- package/src/components/CnDashboardPage/CnDashboardPage.vue +32 -8
- package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
- package/src/components/CnDataTable/CnDataTable.md +4 -1
- package/src/components/CnDataTable/CnDataTable.vue +302 -21
- 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/CnLinkCardsPage/CnLinkCardsPage.vue +35 -2
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +39 -1
- package/src/components/CnObjectSidebar/CnFilesTab.vue +65 -0
- package/src/components/CnReportsPage/CnReportsPage.vue +7 -0
- package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
- package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
- package/src/components/CnStorePage/CnStorePage.vue +7 -0
- 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/CnWikiPage/CnWikiPage.vue +7 -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 +82 -2
- package/src/index.js +2 -0
- package/src/public/index.js +5 -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,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Per-column sorting and filtering for table headers.
|
|
8
|
+
*
|
|
9
|
+
* Pure helpers: which column can sort, which can filter and how, and the
|
|
10
|
+
* translation between a header filter's state and the OpenRegister query
|
|
11
|
+
* parameters the facet sidebar already sends. A header filter writes into the
|
|
12
|
+
* SAME active-filter map (`{ paramKey: values[] }`) the sidebar uses, so the
|
|
13
|
+
* fetch, the route query and saved views need nothing new.
|
|
14
|
+
*
|
|
15
|
+
* Parameter shapes, as OpenRegister reads them:
|
|
16
|
+
* - equality and "any of": `key=value`, `key[]=a&key[]=b`
|
|
17
|
+
* - ranges: `key[gte]=from`, `key[lte]=to`
|
|
18
|
+
*
|
|
19
|
+
* @module utils/columnFilters
|
|
20
|
+
* @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { schemaRefSlug } from './schemaRefSlug.js'
|
|
24
|
+
|
|
25
|
+
/** Column keys that mark a value computed on the client, never stored. */
|
|
26
|
+
const COMPUTED_MARKERS = ['aggregate', 'compute', 'computed', 'virtual']
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Normalise a `$ref` to the referenced schema slug, or null.
|
|
30
|
+
*
|
|
31
|
+
* @param {unknown} ref A `$ref` value.
|
|
32
|
+
* @return {string|number|null} The slug.
|
|
33
|
+
*/
|
|
34
|
+
function refSlug(ref) {
|
|
35
|
+
if (typeof ref === 'number' && !Number.isNaN(ref)) {
|
|
36
|
+
return ref
|
|
37
|
+
}
|
|
38
|
+
if (typeof ref !== 'string' || ref === '') {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
const tail = ref.includes('/') ? ref.substring(ref.lastIndexOf('/') + 1) : ref
|
|
42
|
+
const slug = tail === '' ? '' : schemaRefSlug(tail)
|
|
43
|
+
return slug !== '' ? slug : null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The schema property behind a column, or null when the column has none.
|
|
48
|
+
*
|
|
49
|
+
* @param {object} column The column definition.
|
|
50
|
+
* @param {object|null} schema The table's schema.
|
|
51
|
+
* @return {object|null} The property.
|
|
52
|
+
*/
|
|
53
|
+
export function columnProperty(column, schema) {
|
|
54
|
+
if (!column || !schema || !schema.properties || typeof column.key !== 'string') {
|
|
55
|
+
return null
|
|
56
|
+
}
|
|
57
|
+
const prop = schema.properties[column.key]
|
|
58
|
+
return (prop && typeof prop === 'object') ? prop : null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether a column computes its value on the client, so the server cannot
|
|
63
|
+
* sort or filter on it.
|
|
64
|
+
*
|
|
65
|
+
* @param {object} column The column definition.
|
|
66
|
+
* @return {boolean} True for a computed column.
|
|
67
|
+
*/
|
|
68
|
+
function isComputedColumn(column) {
|
|
69
|
+
return COMPUTED_MARKERS.some((marker) => column[marker] !== undefined && column[marker] !== null && column[marker] !== false)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Whether a column header sorts.
|
|
74
|
+
*
|
|
75
|
+
* `sortable: false` always wins, and so does an explicit `sortable: true`.
|
|
76
|
+
* Otherwise, with a schema, a column sorts when a stored schema property backs
|
|
77
|
+
* it: OpenRegister orders on schema properties, so a widget or computed column
|
|
78
|
+
* with no property behind it stays unsortable. Without a schema nothing says
|
|
79
|
+
* the server can sort, so only an explicit flag makes the column sortable,
|
|
80
|
+
* which is how such tables behaved before.
|
|
81
|
+
*
|
|
82
|
+
* @param {object} column The column definition.
|
|
83
|
+
* @param {object|null} schema The table's schema.
|
|
84
|
+
* @return {boolean} True when the header sorts.
|
|
85
|
+
* @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md#requirement-every-backed-column-sorts-by-default
|
|
86
|
+
*/
|
|
87
|
+
export function isColumnSortable(column, schema) {
|
|
88
|
+
if (!column || typeof column !== 'object') {
|
|
89
|
+
return false
|
|
90
|
+
}
|
|
91
|
+
if (column.sortable === false) {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
if (column.sortable === true) {
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
if (!schema || isComputedColumn(column)) {
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
const prop = columnProperty(column, schema)
|
|
101
|
+
return !!prop && prop.type !== 'object'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Display options for an enum filter: schema `enum` (or `items.enum`) with
|
|
106
|
+
* `x-enum-labels`, else a column `enum` hint, else a badge column's colour
|
|
107
|
+
* map keys.
|
|
108
|
+
*
|
|
109
|
+
* @param {object} column The column definition.
|
|
110
|
+
* @param {object|null} prop The schema property.
|
|
111
|
+
* @return {Array<{value: string, label: string}>} The options, or [].
|
|
112
|
+
*/
|
|
113
|
+
function enumOptions(column, prop) {
|
|
114
|
+
const p = prop || {}
|
|
115
|
+
const labels = p.enumLabels || p['x-enum-labels'] || (p.items && (p.items.enumLabels || p.items['x-enum-labels'])) || {}
|
|
116
|
+
let values = null
|
|
117
|
+
if (Array.isArray(p.enum) && p.enum.length > 0) {
|
|
118
|
+
values = p.enum
|
|
119
|
+
} else if (p.items && Array.isArray(p.items.enum) && p.items.enum.length > 0) {
|
|
120
|
+
values = p.items.enum
|
|
121
|
+
} else if (Array.isArray(column.enum) && column.enum.length > 0) {
|
|
122
|
+
values = column.enum
|
|
123
|
+
} else if (column.widget === 'badge' && column.widgetProps && column.widgetProps.colorMap) {
|
|
124
|
+
values = Object.keys(column.widgetProps.colorMap)
|
|
125
|
+
}
|
|
126
|
+
if (!values) {
|
|
127
|
+
return []
|
|
128
|
+
}
|
|
129
|
+
return values
|
|
130
|
+
.filter((v) => v !== null && v !== undefined && v !== '')
|
|
131
|
+
.map((v) => ({ value: String(v), label: String(labels[v] !== undefined ? labels[v] : v) }))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The reference a column points at, for a searchable object filter: a schema
|
|
136
|
+
* `$ref` (or `items.$ref`), else an `fkResolve` cell widget's config.
|
|
137
|
+
*
|
|
138
|
+
* @param {object} column The column definition.
|
|
139
|
+
* @param {object|null} prop The schema property.
|
|
140
|
+
* @return {{schema: (string|number), register: string, labelField: string}|null} The reference.
|
|
141
|
+
*/
|
|
142
|
+
function referenceTarget(column, prop) {
|
|
143
|
+
const widgetProps = (column.widget === 'fkResolve' && column.widgetProps) || null
|
|
144
|
+
const p = prop || {}
|
|
145
|
+
const schemaRef = refSlug(p.$ref) ?? (p.items ? refSlug(p.items.$ref) : null)
|
|
146
|
+
const schema = schemaRef ?? (widgetProps && widgetProps.schema ? widgetProps.schema : null)
|
|
147
|
+
if (schema === null || schema === undefined || schema === '') {
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
const register = (widgetProps && widgetProps.register)
|
|
151
|
+
|| p['x-external-register']
|
|
152
|
+
|| (p.items && p.items['x-external-register'])
|
|
153
|
+
|| ''
|
|
154
|
+
const labelField = (widgetProps && widgetProps.labelField) || ''
|
|
155
|
+
return { schema, register, labelField }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* How a column filters, or null when it does not.
|
|
160
|
+
*
|
|
161
|
+
* Kinds: `enum` (checkbox list), `boolean` (yes, no, any), `reference`
|
|
162
|
+
* (searchable list of the referenced objects), `number` and `date` (from and
|
|
163
|
+
* to), and `string` (equals). `filterable: false` on the column turns it
|
|
164
|
+
* off. A column with no schema property behind it only filters when it
|
|
165
|
+
* carries an enum hint (`enum`, or a badge widget's colour map), because the
|
|
166
|
+
* server cannot filter on a value it does not store.
|
|
167
|
+
*
|
|
168
|
+
* @param {object} column The column definition.
|
|
169
|
+
* @param {object|null} schema The table's schema.
|
|
170
|
+
* @return {{key: string, kind: string, label: string, options: Array<{value: string, label: string}>, reference: object|null, dateTime: boolean}|null} The filter definition.
|
|
171
|
+
* @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md#requirement-every-backed-column-filters-from-its-header
|
|
172
|
+
*/
|
|
173
|
+
export function columnFilterDef(column, schema) {
|
|
174
|
+
if (!column || typeof column !== 'object' || typeof column.key !== 'string' || column.key === '') {
|
|
175
|
+
return null
|
|
176
|
+
}
|
|
177
|
+
if (column.filterable === false || isComputedColumn(column)) {
|
|
178
|
+
return null
|
|
179
|
+
}
|
|
180
|
+
const prop = columnProperty(column, schema)
|
|
181
|
+
const base = { key: column.key, label: column.label || (prop && prop.title) || column.key, options: [], reference: null, dateTime: false }
|
|
182
|
+
|
|
183
|
+
const options = enumOptions(column, prop)
|
|
184
|
+
if (options.length > 0) {
|
|
185
|
+
return { ...base, kind: 'enum', options }
|
|
186
|
+
}
|
|
187
|
+
if (!prop) {
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
const reference = referenceTarget(column, prop)
|
|
191
|
+
if (reference) {
|
|
192
|
+
return { ...base, kind: 'reference', reference }
|
|
193
|
+
}
|
|
194
|
+
const type = Array.isArray(prop.type) ? prop.type.find((t) => t !== 'null') : prop.type
|
|
195
|
+
const format = prop.format || ''
|
|
196
|
+
if (type === 'boolean') {
|
|
197
|
+
return { ...base, kind: 'boolean' }
|
|
198
|
+
}
|
|
199
|
+
if (type === 'number' || type === 'integer') {
|
|
200
|
+
return { ...base, kind: 'number' }
|
|
201
|
+
}
|
|
202
|
+
if (type === 'string' && (format === 'date' || format === 'date-time')) {
|
|
203
|
+
return { ...base, kind: 'date', dateTime: format === 'date-time' }
|
|
204
|
+
}
|
|
205
|
+
if (type === 'string') {
|
|
206
|
+
return { ...base, kind: 'string' }
|
|
207
|
+
}
|
|
208
|
+
return null
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Read one active-filter entry as a list of strings.
|
|
213
|
+
*
|
|
214
|
+
* @param {unknown} raw An active-filter value: an array, a scalar, or absent.
|
|
215
|
+
* @return {Array<string>} The values.
|
|
216
|
+
*/
|
|
217
|
+
function asList(raw) {
|
|
218
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
219
|
+
return []
|
|
220
|
+
}
|
|
221
|
+
return (Array.isArray(raw) ? raw : [raw]).filter((v) => v !== undefined && v !== null && v !== '').map((v) => String(v))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The parameter keys one column filter owns.
|
|
226
|
+
*
|
|
227
|
+
* @param {object} def A filter definition from `columnFilterDef`.
|
|
228
|
+
* @return {Array<string>} The keys.
|
|
229
|
+
*/
|
|
230
|
+
export function columnFilterParamKeys(def) {
|
|
231
|
+
if (!def) {
|
|
232
|
+
return []
|
|
233
|
+
}
|
|
234
|
+
if (def.kind === 'number' || def.kind === 'date') {
|
|
235
|
+
return [`${def.key}[gte]`, `${def.key}[lte]`]
|
|
236
|
+
}
|
|
237
|
+
return [def.key]
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* A column filter's current state, read from the active-filter map.
|
|
242
|
+
*
|
|
243
|
+
* @param {object} def A filter definition.
|
|
244
|
+
* @param {object} activeFilters The active-filter map (`{ paramKey: values }`).
|
|
245
|
+
* @return {object} `{ values }` for enum and reference, `{ value }` for boolean and string, `{ from, to }` for number and date.
|
|
246
|
+
*/
|
|
247
|
+
export function columnFilterState(def, activeFilters) {
|
|
248
|
+
const filters = (activeFilters && typeof activeFilters === 'object') ? activeFilters : {}
|
|
249
|
+
if (!def) {
|
|
250
|
+
return {}
|
|
251
|
+
}
|
|
252
|
+
if (def.kind === 'enum' || def.kind === 'reference') {
|
|
253
|
+
return { values: asList(filters[def.key]) }
|
|
254
|
+
}
|
|
255
|
+
if (def.kind === 'number' || def.kind === 'date') {
|
|
256
|
+
const from = asList(filters[`${def.key}[gte]`])[0] || ''
|
|
257
|
+
const to = asList(filters[`${def.key}[lte]`])[0] || ''
|
|
258
|
+
// A date-time column is queried up to the end of the day; the input
|
|
259
|
+
// shows the date the person picked.
|
|
260
|
+
return {
|
|
261
|
+
from: def.kind === 'date' ? from.slice(0, 10) : from,
|
|
262
|
+
to: def.kind === 'date' ? to.slice(0, 10) : to,
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return { value: asList(filters[def.key])[0] || '' }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Whether a column filter is active.
|
|
270
|
+
*
|
|
271
|
+
* @param {object} def A filter definition.
|
|
272
|
+
* @param {object} activeFilters The active-filter map.
|
|
273
|
+
* @return {boolean} True when any of its parameters is set.
|
|
274
|
+
*/
|
|
275
|
+
export function isColumnFilterActive(def, activeFilters) {
|
|
276
|
+
const filters = (activeFilters && typeof activeFilters === 'object') ? activeFilters : {}
|
|
277
|
+
return columnFilterParamKeys(def).some((key) => asList(filters[key]).length > 0)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The active-filter entries a column filter state produces. Every key the
|
|
282
|
+
* column owns is present; an empty list clears that key, so changing a range
|
|
283
|
+
* from "from and to" to "from" drops the old upper bound.
|
|
284
|
+
*
|
|
285
|
+
* @param {object} def A filter definition.
|
|
286
|
+
* @param {object} state The column filter state (see `columnFilterState`).
|
|
287
|
+
* @return {object} `{ paramKey: values[] }`.
|
|
288
|
+
* @spec openspec/changes/table-header-sort-and-filter/specs/cn-data-table/spec.md#requirement-a-header-filter-speaks-the-sidebars-query-language
|
|
289
|
+
*/
|
|
290
|
+
export function columnFilterParams(def, state) {
|
|
291
|
+
const s = state || {}
|
|
292
|
+
if (!def) {
|
|
293
|
+
return {}
|
|
294
|
+
}
|
|
295
|
+
if (def.kind === 'enum' || def.kind === 'reference') {
|
|
296
|
+
return { [def.key]: asList(s.values) }
|
|
297
|
+
}
|
|
298
|
+
if (def.kind === 'number' || def.kind === 'date') {
|
|
299
|
+
const from = s.from === undefined || s.from === null ? '' : String(s.from).trim()
|
|
300
|
+
let to = s.to === undefined || s.to === null ? '' : String(s.to).trim()
|
|
301
|
+
if (def.kind === 'date' && def.dateTime && to !== '' && to.length === 10) {
|
|
302
|
+
to = `${to}T23:59:59`
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
[`${def.key}[gte]`]: from === '' ? [] : [from],
|
|
306
|
+
[`${def.key}[lte]`]: to === '' ? [] : [to],
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (def.kind === 'boolean') {
|
|
310
|
+
return { [def.key]: (s.value === 'true' || s.value === 'false') ? [s.value] : [] }
|
|
311
|
+
}
|
|
312
|
+
const value = s.value === undefined || s.value === null ? '' : String(s.value).trim()
|
|
313
|
+
return { [def.key]: value === '' ? [] : [value] }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Clear-all entries for one column: every key it owns, emptied.
|
|
318
|
+
*
|
|
319
|
+
* @param {object} def A filter definition.
|
|
320
|
+
* @return {object} `{ paramKey: [] }`.
|
|
321
|
+
*/
|
|
322
|
+
export function clearedColumnFilterParams(def) {
|
|
323
|
+
return Object.fromEntries(columnFilterParamKeys(def).map((key) => [key, []]))
|
|
324
|
+
}
|
|
@@ -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
|
+
}
|