@conduction/nextcloud-vue 2.84.0 → 2.85.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/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +131 -3
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +54 -18
- package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue.js +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +232 -4
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js +95 -51
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue.js +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +114 -0
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +2 -2
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +45 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +22 -12
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +23 -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.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +3 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +18 -5
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +548 -77
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +191 -82
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +11 -2
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +95 -0
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +24 -2
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +70 -2
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +67 -5
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js +181 -124
- package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue.js +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js +5 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue2.js.map +1 -1
- package/dist/esm/components/CnSettingsPage/CnSettingsPage.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue.js +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +134 -10
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +6 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +6 -3
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue.js +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js +140 -7
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnToggleTableWidget/CnToggleTableWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue.js +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js +37 -9
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue2.js.map +1 -1
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js +6 -5
- package/dist/esm/components/CnVersionInfoCard/CnVersionInfoCard.vue3.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue.js +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +2 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js +53 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js +3 -2
- package/dist/esm/components/CnWidgetCardGrid/CnWidgetCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +135 -0
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue.js +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js +19 -0
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js +37 -19
- package/dist/esm/components/CnWidgetWrapper/CnWidgetWrapper.vue3.js.map +1 -1
- package/dist/esm/composables/useListView.js +21 -0
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +23 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +41 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/library-style.js +1 -1
- package/dist/esm/nextcloud-vue.css +742 -397
- package/dist/esm/store/useObjectStore.js +16 -2
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/boardColumns.js +8 -1
- package/dist/esm/utils/boardColumns.js.map +1 -1
- package/dist/esm/utils/boardReferenceColumns.js +278 -0
- package/dist/esm/utils/boardReferenceColumns.js.map +1 -0
- package/dist/esm/utils/buildManifest.js +4 -1
- package/dist/esm/utils/buildManifest.js.map +1 -1
- package/dist/esm/utils/gridStack.js +40 -1
- package/dist/esm/utils/gridStack.js.map +1 -1
- package/dist/esm/utils/humaniseKey.js +41 -0
- package/dist/esm/utils/humaniseKey.js.map +1 -0
- package/dist/esm/utils/notificationRuleLabel.js +6 -23
- package/dist/esm/utils/notificationRuleLabel.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 +8 -2
- package/dist/esm/utils/widgetDispatch.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +15075 -12590
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +742 -397
- package/l10n/en.json +23 -1
- package/l10n/nl.json +41 -1
- package/package.json +1 -1
- package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +160 -8
- package/src/components/CnBoardView/CnBoardView.vue +262 -6
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +116 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +57 -2
- package/src/components/CnDetailPage/CnDetailPage.md +1 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +23 -4
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +3 -1
- package/src/components/CnFactsWidget/CnFactsWidget.vue +18 -5
- package/src/components/CnIndexPage/CnIndexPage.vue +586 -25
- package/src/components/CnIndexPage/useSelfFetchList.js +10 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +123 -3
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +70 -2
- package/src/components/CnRegisterMapping/CnRegisterMapping.md +5 -0
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +231 -96
- package/src/components/CnSettingsPage/CnSettingsPage.vue +5 -1
- package/src/components/CnStatWidget/CnStatWidget.vue +136 -11
- package/src/components/CnTabsWidget/CnTabsWidget.vue +6 -3
- package/src/components/CnToggleTableWidget/CnToggleTableWidget.vue +140 -7
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.md +2 -1
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +42 -13
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +2 -1
- package/src/components/CnWidgetCardGrid/CnWidgetCardGrid.vue +55 -2
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +135 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.md +10 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +44 -1
- package/src/composables/useListView.js +21 -0
- package/src/css/board.css +65 -4
- package/src/css/dialog.css +4 -2
- package/src/css/index-page.css +49 -0
- package/src/css/kpi-card.css +13 -0
- package/src/css/look-board.css +96 -7
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +115 -5
- package/src/store/useObjectStore.js +16 -2
- package/src/utils/boardColumns.js +8 -1
- package/src/utils/boardReferenceColumns.js +274 -0
- package/src/utils/buildManifest.js +4 -1
- package/src/utils/gridStack.js +39 -0
- package/src/utils/humaniseKey.js +38 -0
- package/src/utils/notificationRuleLabel.js +5 -23
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/widgetDispatch.js +8 -2
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* Board columns from a REFERENCED field (screens-board-reference-columns).
|
|
6
|
+
*
|
|
7
|
+
* A status that is an enum or a lifecycle list carries its stages on the
|
|
8
|
+
* field, and `boardColumns.js` reads them there. A status that is a reference
|
|
9
|
+
* to another object (a case's `status` pointing at a statusType row) carries
|
|
10
|
+
* nothing on the field: the stages are the rows of the referenced schema. This
|
|
11
|
+
* module turns those rows into the same `states` list `stagesOf()` reads, plus
|
|
12
|
+
* the map from a row's raw value (the referenced id) to its column.
|
|
13
|
+
*
|
|
14
|
+
* 🔴 THE COLUMNS ARE STILL THE SCHEMA'S, NOT THE DATA'S. They are the
|
|
15
|
+
* referenced rows, fetched, not the values present on the loaded cards, so an
|
|
16
|
+
* empty stage is still a column to drop a card into. The one exception is a
|
|
17
|
+
* field with no referenced schema at all (a Nextcloud user id): there the
|
|
18
|
+
* values present are all there is, and a "nobody" column is always added so
|
|
19
|
+
* work without an owner stays visible and can be dropped back to nobody.
|
|
20
|
+
*
|
|
21
|
+
* 🔴 MERGED COLUMNS RESOLVE PER CARD. Referenced rows are often per parent
|
|
22
|
+
* (every case type has its own "In behandeling"). Merged by name they make one
|
|
23
|
+
* column, and a drop must then write the row of THAT card's parent. A column
|
|
24
|
+
* with no row for the card's parent is not a place the card can go, and the
|
|
25
|
+
* drop is refused rather than written to some other parent's row.
|
|
26
|
+
*
|
|
27
|
+
* Pure apart from `fetchReferenceObjects`: no store, no Vue.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { buildHeaders, buildQueryString, prefixUrl } from './headers.js'
|
|
31
|
+
import { readPath } from './readPath.js'
|
|
32
|
+
import { schemaRefSlug } from './schemaRefSlug.js'
|
|
33
|
+
|
|
34
|
+
/** The column of cards with no value, for a `values` column field. */
|
|
35
|
+
export const NO_VALUE_KEY = '__none__'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The id a reference value carries: a string as is, an object by its `id`,
|
|
39
|
+
* `uuid` or `@self.id`.
|
|
40
|
+
*
|
|
41
|
+
* @param {unknown} value A reference as a row holds it.
|
|
42
|
+
* @return {string} The id, '' when there is none.
|
|
43
|
+
*/
|
|
44
|
+
export function refIdOf(value) {
|
|
45
|
+
if (value === null || value === undefined) {
|
|
46
|
+
return ''
|
|
47
|
+
}
|
|
48
|
+
if (typeof value === 'object') {
|
|
49
|
+
const id = value.id ?? value.uuid ?? value['@self']?.id ?? value.value ?? ''
|
|
50
|
+
return id === null || id === undefined ? '' : String(id)
|
|
51
|
+
}
|
|
52
|
+
return String(value)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A value as text: a string or number as is, an object by its name.
|
|
57
|
+
*
|
|
58
|
+
* @param {unknown} value The raw value.
|
|
59
|
+
* @return {string} The text.
|
|
60
|
+
*/
|
|
61
|
+
function textOf(value) {
|
|
62
|
+
if (value === null || value === undefined) {
|
|
63
|
+
return ''
|
|
64
|
+
}
|
|
65
|
+
if (typeof value === 'object') {
|
|
66
|
+
return String(value.displayName ?? value.name ?? value.title ?? value.label ?? '').trim()
|
|
67
|
+
}
|
|
68
|
+
return String(value).trim()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The column fields a board offers, normalised. Without `columnFields` the
|
|
73
|
+
* board has one, its `statusField`, read from the schema as before.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} board The board configuration (manifest `config.board`).
|
|
76
|
+
* @return {Array<{field: string, label: string, source: (object|null), kind: string, readOnly: boolean, moveHandler: string, moveTargetsHandler: string}>} The fields.
|
|
77
|
+
* @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-a-board-can-switch-the-field-its-columns-come-from
|
|
78
|
+
*/
|
|
79
|
+
export function columnFieldsOf(board) {
|
|
80
|
+
const declared = Array.isArray(board?.columnFields) ? board.columnFields : []
|
|
81
|
+
return declared
|
|
82
|
+
.map((entry) => (typeof entry === 'string' ? { field: entry } : entry))
|
|
83
|
+
.filter((entry) => entry && typeof entry.field === 'string' && entry.field !== '')
|
|
84
|
+
.map((entry) => ({
|
|
85
|
+
field: entry.field,
|
|
86
|
+
label: typeof entry.label === 'string' && entry.label !== '' ? entry.label : entry.field,
|
|
87
|
+
source: entry.source && typeof entry.source === 'object' ? entry.source : null,
|
|
88
|
+
kind: entry.kind === 'values' ? 'values' : (entry.source ? 'reference' : 'schema'),
|
|
89
|
+
readOnly: entry.readOnly === true,
|
|
90
|
+
moveHandler: typeof entry.moveHandler === 'string' ? entry.moveHandler : '',
|
|
91
|
+
moveTargetsHandler: typeof entry.moveTargetsHandler === 'string' ? entry.moveTargetsHandler : '',
|
|
92
|
+
valueLabels: entry.valueLabels && typeof entry.valueLabels === 'object' ? entry.valueLabels : null,
|
|
93
|
+
emptyLabel: typeof entry.emptyLabel === 'string' ? entry.emptyLabel : '',
|
|
94
|
+
}))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The register and schema a reference column reads its rows from: the
|
|
99
|
+
* source's own, else the page's register and the property's `$ref`.
|
|
100
|
+
*
|
|
101
|
+
* @param {object} source The column field's `source`.
|
|
102
|
+
* @param {object} options The context.
|
|
103
|
+
* @param {string} options.register The page's register.
|
|
104
|
+
* @param {object} [options.property] The field's schema property.
|
|
105
|
+
* @return {{register: string, schema: string}} Both, '' when unknown.
|
|
106
|
+
*/
|
|
107
|
+
export function referenceTargetOf(source, { register = '', property = null } = {}) {
|
|
108
|
+
const schema = source?.schema || schemaRefSlug(property?.$ref ?? property?.items?.$ref ?? '')
|
|
109
|
+
return {
|
|
110
|
+
register: String(source?.register || register || ''),
|
|
111
|
+
schema: schema === '' || schema === null || schema === undefined ? '' : String(schema),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The stages of a referenced field: the referenced rows, narrowed to one
|
|
117
|
+
* parent when asked, ordered, and merged by a field when asked.
|
|
118
|
+
*
|
|
119
|
+
* @param {object} options The call.
|
|
120
|
+
* @param {Array<object>} options.refs The referenced rows.
|
|
121
|
+
* @param {string} [options.labelField] The row field that names a column.
|
|
122
|
+
* @param {string} [options.orderField] The numeric row field the columns are ordered by.
|
|
123
|
+
* @param {string} [options.colorField] The row field that holds the dot colour.
|
|
124
|
+
* @param {string} [options.mergeBy] The row field whose equal values make one column.
|
|
125
|
+
* @param {{field: string, rowField: string}|null} [options.narrowBy] The parent link.
|
|
126
|
+
* @param {string} [options.narrowValue] The parent the board is narrowed to, '' for all.
|
|
127
|
+
* @return {{states: Array<{key: string, label: string, color: string}>, keyByValue: Map<string, string>, refsByKey: Map<string, Array<object>>}} The stages.
|
|
128
|
+
* @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-columns-can-be-the-rows-a-field-references
|
|
129
|
+
*/
|
|
130
|
+
export function buildReferenceStages({
|
|
131
|
+
refs = [],
|
|
132
|
+
labelField = '',
|
|
133
|
+
orderField = '',
|
|
134
|
+
colorField = '',
|
|
135
|
+
mergeBy = '',
|
|
136
|
+
narrowBy = null,
|
|
137
|
+
narrowValue = '',
|
|
138
|
+
} = {}) {
|
|
139
|
+
const list = (Array.isArray(refs) ? refs : []).filter((ref) => ref && typeof ref === 'object' && refIdOf(ref) !== '')
|
|
140
|
+
const narrowed = narrowBy && narrowBy.field && narrowValue !== ''
|
|
141
|
+
? list.filter((ref) => refIdOf(readPath(ref, narrowBy.field)) === String(narrowValue))
|
|
142
|
+
: list
|
|
143
|
+
|
|
144
|
+
const orderOf = (ref) => {
|
|
145
|
+
const n = Number(orderField ? readPath(ref, orderField) : NaN)
|
|
146
|
+
return Number.isFinite(n) ? n : Number.MAX_SAFE_INTEGER
|
|
147
|
+
}
|
|
148
|
+
const ordered = narrowed
|
|
149
|
+
.map((ref, index) => ({ ref, index }))
|
|
150
|
+
.sort((a, b) => (orderOf(a.ref) - orderOf(b.ref)) || (a.index - b.index))
|
|
151
|
+
.map((entry) => entry.ref)
|
|
152
|
+
|
|
153
|
+
const states = []
|
|
154
|
+
const keyByValue = new Map()
|
|
155
|
+
const refsByKey = new Map()
|
|
156
|
+
for (const ref of ordered) {
|
|
157
|
+
const id = refIdOf(ref)
|
|
158
|
+
const label = textOf(labelField ? readPath(ref, labelField) : (ref.name ?? ref.title)) || id
|
|
159
|
+
const key = mergeBy ? (textOf(readPath(ref, mergeBy)) || id) : id
|
|
160
|
+
keyByValue.set(id, key)
|
|
161
|
+
if (!refsByKey.has(key)) {
|
|
162
|
+
refsByKey.set(key, [])
|
|
163
|
+
const color = colorField ? readPath(ref, colorField) : ref.color
|
|
164
|
+
states.push({ key, label, color: typeof color === 'string' ? color : '' })
|
|
165
|
+
}
|
|
166
|
+
refsByKey.get(key).push(ref)
|
|
167
|
+
}
|
|
168
|
+
return { states, keyByValue, refsByKey }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The stages of a field with no referenced schema (a user id): the values
|
|
173
|
+
* present on the rows, by label, then one column for no value.
|
|
174
|
+
*
|
|
175
|
+
* @param {object} options The call.
|
|
176
|
+
* @param {Array<object>} options.rows The rows the list holds.
|
|
177
|
+
* @param {string} options.field The row field.
|
|
178
|
+
* @param {object} [options.valueLabels] Raw value to the label to show.
|
|
179
|
+
* @param {string} [options.emptyLabel] The no-value column's label.
|
|
180
|
+
* @return {{states: Array<{key: string, label: string, color: string}>, keyByValue: Map<string, string>, refsByKey: Map<string, Array<object>>}} The stages.
|
|
181
|
+
* @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-a-field-without-a-referenced-schema-takes-its-values
|
|
182
|
+
*/
|
|
183
|
+
export function buildValueStages({ rows = [], field = '', valueLabels = null, emptyLabel = 'Nobody' } = {}) {
|
|
184
|
+
const labels = valueLabels && typeof valueLabels === 'object' ? valueLabels : {}
|
|
185
|
+
const seen = new Map()
|
|
186
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
187
|
+
const raw = readPath(row, field)
|
|
188
|
+
const id = refIdOf(raw)
|
|
189
|
+
if (id !== '' && !seen.has(id)) {
|
|
190
|
+
seen.set(id, typeof labels[id] === 'string' ? labels[id] : (textOf(raw) || id))
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const states = [...seen.entries()]
|
|
194
|
+
.sort((a, b) => a[1].localeCompare(b[1]))
|
|
195
|
+
.map(([key, label]) => ({ key, label, color: '' }))
|
|
196
|
+
states.push({ key: NO_VALUE_KEY, label: emptyLabel, color: '' })
|
|
197
|
+
const keyByValue = new Map(states.map((state) => [state.key, state.key]))
|
|
198
|
+
keyByValue.set('', NO_VALUE_KEY)
|
|
199
|
+
return { states, keyByValue, refsByKey: new Map() }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The column a row sits in, under a value-to-column map.
|
|
204
|
+
*
|
|
205
|
+
* @param {object} row The row.
|
|
206
|
+
* @param {string} field The row field.
|
|
207
|
+
* @param {Map<string, string>} keyByValue The value-to-column map.
|
|
208
|
+
* @return {string} The column key, or the raw value when the map has none
|
|
209
|
+
* (which lands the card in the off-board column).
|
|
210
|
+
*/
|
|
211
|
+
export function columnKeyOfRow(row, field, keyByValue) {
|
|
212
|
+
const id = refIdOf(readPath(row, field))
|
|
213
|
+
return keyByValue.has(id) ? keyByValue.get(id) : id
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* What a drop on a column writes for one card: the referenced row of that
|
|
218
|
+
* column which belongs to the card's parent, or null when the column has none
|
|
219
|
+
* (the card cannot go there).
|
|
220
|
+
*
|
|
221
|
+
* @param {object} options The call.
|
|
222
|
+
* @param {object} options.card The card.
|
|
223
|
+
* @param {string} options.toKey The column it was dropped on.
|
|
224
|
+
* @param {string} options.kind `reference` or `values`.
|
|
225
|
+
* @param {Map<string, Array<object>>} [options.refsByKey] The column's rows.
|
|
226
|
+
* @param {{field: string, rowField: string}|null} [options.narrowBy] The parent link.
|
|
227
|
+
* @return {{value: (string|null), target: (object|null)}|null} The value to write and its row, or null.
|
|
228
|
+
* @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-a-drop-writes-the-reference
|
|
229
|
+
*/
|
|
230
|
+
export function resolveDropValue({ card, toKey, kind, refsByKey = new Map(), narrowBy = null } = {}) {
|
|
231
|
+
if (kind === 'values') {
|
|
232
|
+
return { value: toKey === NO_VALUE_KEY ? null : String(toKey), target: null }
|
|
233
|
+
}
|
|
234
|
+
const candidates = refsByKey.get(String(toKey)) || []
|
|
235
|
+
if (candidates.length === 0) {
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
if (narrowBy && narrowBy.field && narrowBy.rowField) {
|
|
239
|
+
const parent = refIdOf(readPath(card, narrowBy.rowField))
|
|
240
|
+
const match = candidates.find((ref) => refIdOf(readPath(ref, narrowBy.field)) === parent)
|
|
241
|
+
return match ? { value: refIdOf(match), target: match } : null
|
|
242
|
+
}
|
|
243
|
+
return { value: refIdOf(candidates[0]), target: candidates[0] }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Fetch the rows a reference column is built from, one list request.
|
|
248
|
+
*
|
|
249
|
+
* @param {object} options The call.
|
|
250
|
+
* @param {string} options.register The register slug.
|
|
251
|
+
* @param {string} options.schema The schema slug.
|
|
252
|
+
* @param {object} [options.filter] Equality filters, e.g. `{ isFinal: false }`.
|
|
253
|
+
* @param {number} [options.limit] The page size, 500 by default.
|
|
254
|
+
* @return {Promise<Array<object>>} The rows.
|
|
255
|
+
* @spec openspec/changes/screens-board-reference-columns/specs/board-view/spec.md#requirement-columns-can-be-the-rows-a-field-references
|
|
256
|
+
*/
|
|
257
|
+
export async function fetchReferenceObjects({ register, schema, filter = {}, limit = 500 } = {}) {
|
|
258
|
+
if (!register || !schema) {
|
|
259
|
+
return []
|
|
260
|
+
}
|
|
261
|
+
const params = { _limit: limit }
|
|
262
|
+
for (const [key, value] of Object.entries(filter || {})) {
|
|
263
|
+
if (value !== undefined && value !== null && typeof value !== 'object') {
|
|
264
|
+
params[key] = typeof value === 'boolean' ? String(value) : value
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const url = prefixUrl(`/apps/openregister/api/objects/${encodeURIComponent(register)}/${encodeURIComponent(schema)}${buildQueryString(params)}`)
|
|
268
|
+
const response = await fetch(url, { headers: buildHeaders() })
|
|
269
|
+
if (!response.ok) {
|
|
270
|
+
throw new Error(`list returned ${response.status}`)
|
|
271
|
+
}
|
|
272
|
+
const data = await response.json()
|
|
273
|
+
return Array.isArray(data?.results) ? data.results : (Array.isArray(data) ? data : [])
|
|
274
|
+
}
|
|
@@ -137,7 +137,10 @@ export function mergeMenuItems(target, incoming) {
|
|
|
137
137
|
target.push(copy)
|
|
138
138
|
return
|
|
139
139
|
}
|
|
140
|
-
|
|
140
|
+
// `query` and `params` belong to `route`: an entry that takes its route
|
|
141
|
+
// from here without them opens the list unfiltered (a "board" entry to
|
|
142
|
+
// `Cases` with `{ _view: 'board' }` opened the table).
|
|
143
|
+
for (const key of ['label', 'icon', 'route', 'query', 'params', 'order', 'section', 'featureFlag', 'permission', 'visibleIf', 'href', 'action']) {
|
|
141
144
|
if (existing[key] === undefined && item[key] !== undefined) {
|
|
142
145
|
existing[key] = item[key]
|
|
143
146
|
}
|
package/src/utils/gridStack.js
CHANGED
|
@@ -53,3 +53,42 @@ export function readGridGeometry(items) {
|
|
|
53
53
|
}
|
|
54
54
|
return map
|
|
55
55
|
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Where the items under a growing cell go so they keep their order: every item
|
|
59
|
+
* the growth would overlap moves down by the growth, and so does every item
|
|
60
|
+
* that move would overlap in turn. Items beside the cell stay put.
|
|
61
|
+
*
|
|
62
|
+
* GridStack resolves the same collision itself, but not in order: when
|
|
63
|
+
* pipelinq's size-to-content banner grew from two rows to three, the
|
|
64
|
+
* full-width card under the stat tiles landed below the lists.
|
|
65
|
+
*
|
|
66
|
+
* @spec openspec/changes/dashboard-growth-keeps-order/specs/dashboard-grid/spec.md#requirement-a-growing-cell-keeps-the-order-of-the-cards-under-it
|
|
67
|
+
* @param {Array<{x: number, y: number, w: number, h: number}>} nodes The grid's items (the growing one among them).
|
|
68
|
+
* @param {{x: number, y: number, w: number, h: number}} node The growing item.
|
|
69
|
+
* @param {number} h Its new height, in grid rows.
|
|
70
|
+
* @return {Array<{node: object, y: number}>} The items to move and their new row, bottom-most first.
|
|
71
|
+
*/
|
|
72
|
+
export function planGrowth(nodes, node, h) {
|
|
73
|
+
const delta = h - node.h
|
|
74
|
+
if (!(delta > 0)) {
|
|
75
|
+
return []
|
|
76
|
+
}
|
|
77
|
+
const moved = new Map()
|
|
78
|
+
const overlaps = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h
|
|
79
|
+
const queue = [{ x: node.x, y: node.y, w: node.w, h }]
|
|
80
|
+
while (queue.length > 0) {
|
|
81
|
+
const rect = queue.shift()
|
|
82
|
+
for (const other of nodes) {
|
|
83
|
+
if (other === node || moved.has(other) || !overlaps(rect, other)) {
|
|
84
|
+
continue
|
|
85
|
+
}
|
|
86
|
+
const next = { x: other.x, y: other.y + delta, w: other.w, h: other.h }
|
|
87
|
+
moved.set(other, next.y)
|
|
88
|
+
queue.push(next)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return [...moved.entries()]
|
|
92
|
+
.map(([item, y]) => ({ node: item, y }))
|
|
93
|
+
.sort((a, b) => b.y - a.y)
|
|
94
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* Make an identifier-shaped key readable for a person. Pure: no Vue.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Split camelCase, snake_case and kebab-case into words, keep acronyms, and
|
|
10
|
+
* capitalise only the first word.
|
|
11
|
+
*
|
|
12
|
+
* `caseAssigned` → "Case assigned", `object_created` → "Object created",
|
|
13
|
+
* `newBRPRecord` → "New BRP record".
|
|
14
|
+
*
|
|
15
|
+
* @param {string} key The key.
|
|
16
|
+
* @return {string} A readable sentence-case label, or the key when it has no words.
|
|
17
|
+
* @spec openspec/changes/data-widget-nested-objects/specs/data-widget-nested-objects/spec.md#requirement-a-nested-object-reads-as-a-list-per-key
|
|
18
|
+
*/
|
|
19
|
+
export function humaniseKey(key) {
|
|
20
|
+
const words = String(key || '')
|
|
21
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
22
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
23
|
+
.split(/[\s_\-.]+/)
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
if (words.length === 0) {
|
|
26
|
+
return String(key || '')
|
|
27
|
+
}
|
|
28
|
+
return words
|
|
29
|
+
.map((word, index) => {
|
|
30
|
+
const isAcronym = word.length > 1 && word === word.toUpperCase() && /[A-Z]/.test(word)
|
|
31
|
+
if (isAcronym) {
|
|
32
|
+
return word
|
|
33
|
+
}
|
|
34
|
+
const lower = word.toLowerCase()
|
|
35
|
+
return index === 0 ? lower.charAt(0).toUpperCase() + lower.slice(1) : lower
|
|
36
|
+
})
|
|
37
|
+
.join(' ')
|
|
38
|
+
}
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
* @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md
|
|
29
29
|
*/
|
|
30
30
|
|
|
31
|
+
import { humaniseKey } from './humaniseKey.js'
|
|
32
|
+
|
|
31
33
|
/**
|
|
32
34
|
* Pick the string for the current language out of a string or per-locale map.
|
|
33
35
|
*
|
|
@@ -54,34 +56,14 @@ export function resolveLocalised(value, language = 'en') {
|
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
/**
|
|
57
|
-
* Make a rule key readable
|
|
58
|
-
* words, keep acronyms, and capitalise only the first word.
|
|
59
|
-
*
|
|
60
|
-
* `caseAssigned` → "Case assigned", `object_created` → "Object created",
|
|
61
|
-
* `newBRPRecord` → "New BRP record".
|
|
59
|
+
* Make a rule key readable (`caseAssigned` → "Case assigned"); see `humaniseKey`.
|
|
62
60
|
*
|
|
63
61
|
* @param {string} key The rule key.
|
|
64
62
|
* @return {string} A readable sentence-case label, or the key when it has no words.
|
|
63
|
+
* @spec openspec/changes/notification-rule-labels-and-runtime-version/specs/notification-preferences/spec.md#requirement-a-notification-rule-reads-as-a-label
|
|
65
64
|
*/
|
|
66
65
|
export function humaniseRuleKey(key) {
|
|
67
|
-
|
|
68
|
-
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
69
|
-
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
|
|
70
|
-
.split(/[\s_\-.]+/)
|
|
71
|
-
.filter(Boolean)
|
|
72
|
-
if (words.length === 0) {
|
|
73
|
-
return String(key || '')
|
|
74
|
-
}
|
|
75
|
-
return words
|
|
76
|
-
.map((word, index) => {
|
|
77
|
-
const isAcronym = word.length > 1 && word === word.toUpperCase() && /[A-Z]/.test(word)
|
|
78
|
-
if (isAcronym) {
|
|
79
|
-
return word
|
|
80
|
-
}
|
|
81
|
-
const lower = word.toLowerCase()
|
|
82
|
-
return index === 0 ? lower.charAt(0).toUpperCase() + lower.slice(1) : lower
|
|
83
|
-
})
|
|
84
|
-
.join(' ')
|
|
66
|
+
return humaniseKey(key)
|
|
85
67
|
}
|
|
86
68
|
|
|
87
69
|
/**
|