@conduction/nextcloud-vue 2.132.0 → 2.133.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.
Files changed (86) hide show
  1. package/dist/esm/components/CnActionButtons/CnActionButtons.vue.js +1 -1
  2. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +14 -0
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +5 -4
  5. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  6. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue.js +9 -0
  7. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue.js.map +1 -0
  8. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue2.js +237 -0
  9. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue2.js.map +1 -0
  10. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue3.js +103 -0
  11. package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue3.js.map +1 -0
  12. package/dist/esm/components/CnConditionListEditor/conditionOperators.js +121 -0
  13. package/dist/esm/components/CnConditionListEditor/conditionOperators.js.map +1 -0
  14. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +12 -0
  15. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  16. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +1 -1
  17. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  18. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +8 -1
  19. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  21. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
  22. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +46 -7
  23. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
  24. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +1 -1
  25. package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
  26. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
  27. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +59 -3
  28. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +197 -127
  30. package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
  31. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  32. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +42 -2
  33. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  34. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  35. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  36. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +1 -1
  37. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  38. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  39. package/dist/esm/index.js +2 -0
  40. package/dist/esm/index.js.map +1 -1
  41. package/dist/esm/l10n/en.json.js +17 -1
  42. package/dist/esm/l10n/en.json.js.map +1 -1
  43. package/dist/esm/l10n/nl.json.js +17 -1
  44. package/dist/esm/l10n/nl.json.js.map +1 -1
  45. package/dist/esm/library-style.js +1 -1
  46. package/dist/esm/nextcloud-vue.css +277 -166
  47. package/dist/esm/store/plugins/liveUpdates.js +13 -4
  48. package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
  49. package/dist/esm/store/useObjectStore.js +28 -5
  50. package/dist/esm/store/useObjectStore.js.map +1 -1
  51. package/dist/esm/utils/detailActionModel.js +6 -0
  52. package/dist/esm/utils/detailActionModel.js.map +1 -1
  53. package/dist/esm/utils/listLenses.js +20 -1
  54. package/dist/esm/utils/listLenses.js.map +1 -1
  55. package/dist/esm/utils/objectRows.js +59 -0
  56. package/dist/esm/utils/objectRows.js.map +1 -0
  57. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  58. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  59. package/dist/nextcloud-vue.cjs.js +5961 -5127
  60. package/dist/nextcloud-vue.cjs.js.map +2 -2
  61. package/dist/nextcloud-vue.css +277 -166
  62. package/l10n/en.json +17 -1
  63. package/l10n/nl.json +17 -1
  64. package/package.json +1 -1
  65. package/src/components/CnActionButtons/CnActionButtons.vue +15 -0
  66. package/src/components/CnConditionListEditor/CnConditionListEditor.vue +346 -0
  67. package/src/components/CnConditionListEditor/conditionOperators.js +126 -0
  68. package/src/components/CnConditionListEditor/index.js +17 -0
  69. package/src/components/CnDetailPage/CnDetailPage.md +1 -0
  70. package/src/components/CnDetailPage/CnDetailPage.vue +13 -1
  71. package/src/components/CnIndexPage/CnIndexPage.vue +8 -1
  72. package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +47 -8
  73. package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +139 -2
  74. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +42 -2
  75. package/src/components/CnTabsWidget/CnTabsWidget.vue +1 -1
  76. package/src/components/index.js +1 -0
  77. package/src/css/look-board-index.css +6 -0
  78. package/src/css/look-board.css +35 -7
  79. package/src/index.js +2 -0
  80. package/src/schemas/app-manifest-v2.schema.json +17 -1
  81. package/src/store/plugins/liveUpdates.js +13 -4
  82. package/src/store/useObjectStore.js +28 -5
  83. package/src/utils/detailActionModel.js +6 -0
  84. package/src/utils/listLenses.js +20 -1
  85. package/src/utils/objectRows.js +55 -0
  86. package/src/utils/validateManifestV2.compiled.js +3 -3
package/l10n/en.json CHANGED
@@ -2363,7 +2363,23 @@
2363
2363
  "Viewed": "Viewed",
2364
2364
  "w{week}": "w{week}",
2365
2365
  "Icon": "Icon",
2366
- "Shown when you point at the view in the menu.": "Shown when you point at the view in the menu."
2366
+ "Shown when you point at the view in the menu.": "Shown when you point at the view in the menu.",
2367
+ "No main contact yet": "No main contact yet",
2368
+ "is": "is",
2369
+ "is not": "is not",
2370
+ "greater than": "greater than",
2371
+ "at least": "at least",
2372
+ "less than": "less than",
2373
+ "at most": "at most",
2374
+ "is filled in": "is filled in",
2375
+ "{field} is {value}": "{field} is {value}",
2376
+ "{field} is not {value}": "{field} is not {value}",
2377
+ "{field} is greater than {value}": "{field} is greater than {value}",
2378
+ "{field} is at least {value}": "{field} is at least {value}",
2379
+ "{field} is less than {value}": "{field} is less than {value}",
2380
+ "{field} is at most {value}": "{field} is at most {value}",
2381
+ "{field} is filled in": "{field} is filled in",
2382
+ "No conditions: the rule always applies.": "No conditions: the rule always applies."
2367
2383
  },
2368
2384
  "plurals": {
2369
2385
  "{count} member": [
package/l10n/nl.json CHANGED
@@ -2613,7 +2613,23 @@
2613
2613
  "Viewed": "Ingezien",
2614
2614
  "w{week}": "w{week}",
2615
2615
  "Icon": "Pictogram",
2616
- "Shown when you point at the view in the menu.": "Te zien als u in het menu op de weergave wijst."
2616
+ "Shown when you point at the view in the menu.": "Te zien als u in het menu op de weergave wijst.",
2617
+ "No main contact yet": "Nog geen hoofdcontact",
2618
+ "is": "is",
2619
+ "is not": "is niet",
2620
+ "greater than": "groter dan",
2621
+ "at least": "minstens",
2622
+ "less than": "kleiner dan",
2623
+ "at most": "hoogstens",
2624
+ "is filled in": "is ingevuld",
2625
+ "{field} is {value}": "{field} {value} is",
2626
+ "{field} is not {value}": "{field} niet {value} is",
2627
+ "{field} is greater than {value}": "{field} groter is dan {value}",
2628
+ "{field} is at least {value}": "{field} minstens {value} is",
2629
+ "{field} is less than {value}": "{field} kleiner is dan {value}",
2630
+ "{field} is at most {value}": "{field} hoogstens {value} is",
2631
+ "{field} is filled in": "{field} is ingevuld",
2632
+ "No conditions: the rule always applies.": "Geen voorwaarden: de regel geldt altijd."
2617
2633
  },
2618
2634
  "plurals": {
2619
2635
  "{count} member": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.132.0",
3
+ "version": "2.133.0",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
6
6
  "bugs": {
@@ -160,6 +160,7 @@
160
160
  :dialogTitle="tr(confirmEntry.confirmTitle || confirmEntry.label)"
161
161
  :message="tr(confirmEntry.confirmMessage) || defaultConfirmMessage"
162
162
  :variant="confirmEntry.variant === 'error' ? 'error' : 'primary'"
163
+ v-bind="confirmButtonProps"
163
164
  @confirm="onConfirmProceed"
164
165
  @close="confirmEntry = null" />
165
166
 
@@ -471,6 +472,20 @@ export default {
471
472
  },
472
473
 
473
474
  computed: {
475
+ /**
476
+ * The confirm button's words and icon from the action
477
+ * (`confirmLabel`, `confirmIcon`); none set leaves the dialog's own.
478
+ *
479
+ * @return {object}
480
+ */
481
+ confirmButtonProps() {
482
+ const entry = this.confirmEntry || {}
483
+ return {
484
+ ...(typeof entry.confirmLabel === 'string' && entry.confirmLabel !== '' ? { confirmLabel: this.tr(entry.confirmLabel) } : {}),
485
+ ...(typeof entry.confirmIcon === 'string' && entry.confirmIcon !== '' ? { confirmIcon: entry.confirmIcon } : {}),
486
+ }
487
+ },
488
+
474
489
  /**
475
490
  * Effective translate function: the injected `cnTranslate` (the host
476
491
  * app's bound `t()`), identity by default.
@@ -0,0 +1,346 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+
6
+ <template>
7
+ <fieldset class="cn-condition-list">
8
+ <legend v-if="label" class="cn-condition-list__label">
9
+ {{ label }}
10
+ </legend>
11
+ <p v-if="rows.length === 0" class="cn-condition-list__empty">
12
+ {{ emptyText || t('nextcloud-vue', 'No conditions: the rule always applies.') }}
13
+ </p>
14
+ <div
15
+ v-for="(row, i) in rows"
16
+ :key="i"
17
+ class="cn-condition-list__row">
18
+ <NcSelect
19
+ class="cn-condition-list__field"
20
+ :modelValue="fieldOption(row.field)"
21
+ :options="fieldOptions"
22
+ label="label"
23
+ :inputLabel="t('nextcloud-vue', 'Field')"
24
+ :clearable="false"
25
+ @update:modelValue="setField(i, $event)" />
26
+ <NcSelect
27
+ class="cn-condition-list__operator"
28
+ :modelValue="operatorOption(row.operator)"
29
+ :options="operatorOptions(row.field)"
30
+ label="label"
31
+ :inputLabel="t('nextcloud-vue', 'Operator')"
32
+ :clearable="false"
33
+ :disabled="!row.field"
34
+ @update:modelValue="setOperator(i, $event)" />
35
+ <template v-if="takesValue(row.operator)">
36
+ <NcSelect
37
+ v-if="valueOptions(row.field)"
38
+ class="cn-condition-list__value"
39
+ :modelValue="valueOption(row)"
40
+ :options="valueOptions(row.field)"
41
+ label="label"
42
+ :inputLabel="t('nextcloud-vue', 'Value')"
43
+ :clearable="false"
44
+ @update:modelValue="setValue(i, $event ? $event.id : '')" />
45
+ <NcTextField
46
+ v-else
47
+ class="cn-condition-list__value"
48
+ :type="inputType(row.field)"
49
+ :modelValue="row.value === undefined || row.value === null ? '' : String(row.value)"
50
+ :label="t('nextcloud-vue', 'Value')"
51
+ @update:modelValue="setValue(i, typedValue(row.field, $event))" />
52
+ </template>
53
+ <NcButton
54
+ variant="tertiary"
55
+ :aria-label="t('nextcloud-vue', 'Remove condition')"
56
+ @click="remove(i)">
57
+ <template #icon>
58
+ <Close :size="18" />
59
+ </template>
60
+ </NcButton>
61
+ </div>
62
+ <NcButton variant="tertiary" :disabled="fields.length === 0" @click="add">
63
+ <template #icon>
64
+ <Plus :size="18" />
65
+ </template>
66
+ {{ t('nextcloud-vue', 'Add condition') }}
67
+ </NcButton>
68
+ </fieldset>
69
+ </template>
70
+
71
+ <script>
72
+ import { translate as t } from '@nextcloud/l10n'
73
+ import { NcButton, NcSelect, NcTextField } from '@nextcloud/vue'
74
+ import Close from 'vue-material-design-icons/Close.vue'
75
+ import Plus from 'vue-material-design-icons/Plus.vue'
76
+ import { operatorLabel, operatorsForType, operatorTakesValue } from './conditionOperators.js'
77
+
78
+ /**
79
+ * CnConditionListEditor - the conditions of a rule as rows over an object's
80
+ * own fields: field, operator, value. All rows hold together. The operators
81
+ * follow the field's type (see `operatorsForType`), "is filled in" needs no
82
+ * value, and the value is a choice for a choice or yes/no field.
83
+ *
84
+ * v-model on `{ field, operator, value }[]`, in both dialects (`value`/`input`
85
+ * and `modelValue`/`update:modelValue`). Read a condition back as words with
86
+ * `conditionSentence` from `./conditionOperators.js`.
87
+ */
88
+ export default {
89
+ name: 'CnConditionListEditor',
90
+
91
+ components: { NcButton, NcSelect, NcTextField, Close, Plus },
92
+
93
+ props: {
94
+ /**
95
+ * The conditions.
96
+ *
97
+ * @type {Array<{field: string, operator: string, value?: unknown}>}
98
+ */
99
+ value: { type: Array, default: () => [] },
100
+ /**
101
+ * The same value under Vue 3's v-model name; wins over `value` when passed.
102
+ *
103
+ * @type {Array}
104
+ */
105
+ modelValue: { type: Array, default: undefined },
106
+ /**
107
+ * The fields a condition may name: `{ id, label, type, options?, format? }`.
108
+ * `type` is number, integer, date, boolean, presence or anything else (text);
109
+ * `options` (`[{ id, label }]`) make the value a choice.
110
+ *
111
+ * @type {Array<{id: string, label: string, type?: string, options?: Array}>}
112
+ */
113
+ fields: { type: Array, default: () => [] },
114
+ /**
115
+ * The legend above the rows, already translated.
116
+ *
117
+ * @type {string}
118
+ */
119
+ label: { type: String, default: '' },
120
+ /**
121
+ * What an empty list says, already translated.
122
+ *
123
+ * @type {string}
124
+ */
125
+ emptyText: { type: String, default: '' },
126
+ },
127
+
128
+ emits: ['input', 'update:modelValue'],
129
+
130
+ data() {
131
+ return {
132
+ rows: this.copy(this.modelValue !== undefined ? this.modelValue : this.value),
133
+ }
134
+ },
135
+
136
+ computed: {
137
+ /**
138
+ * The field choices.
139
+ *
140
+ * @return {Array<{id: string, label: string}>}
141
+ */
142
+ fieldOptions() {
143
+ return this.fields.map((f) => ({ id: f.id, label: f.label || f.id }))
144
+ },
145
+ },
146
+
147
+ watch: {
148
+ value(next) {
149
+ if (this.modelValue === undefined) {
150
+ this.rows = this.copy(next)
151
+ }
152
+ },
153
+
154
+ modelValue(next) {
155
+ if (next !== undefined) {
156
+ this.rows = this.copy(next)
157
+ }
158
+ },
159
+ },
160
+
161
+ methods: {
162
+ t,
163
+
164
+ /**
165
+ * A detached copy of the bound conditions.
166
+ *
167
+ * @param {Array} list The conditions.
168
+ * @return {Array} The copy.
169
+ */
170
+ copy(list) {
171
+ return (Array.isArray(list) ? list : []).map((row) => ({ ...row }))
172
+ },
173
+
174
+ /**
175
+ * The field with this id.
176
+ *
177
+ * @param {string} id The field id.
178
+ * @return {object|undefined}
179
+ */
180
+ fieldOf(id) {
181
+ return this.fields.find((f) => f.id === id)
182
+ },
183
+
184
+ fieldOption(id) {
185
+ return this.fieldOptions.find((o) => o.id === id) || null
186
+ },
187
+
188
+ operatorOptions(fieldId) {
189
+ return operatorsForType(this.fieldOf(fieldId)?.type).map((id) => ({ id, label: operatorLabel(id) }))
190
+ },
191
+
192
+ operatorOption(operator) {
193
+ return operator ? { id: operator, label: operatorLabel(operator) } : null
194
+ },
195
+
196
+ takesValue(operator) {
197
+ return operatorTakesValue(operator)
198
+ },
199
+
200
+ /**
201
+ * The value choices of a field, or null when its value is typed.
202
+ *
203
+ * @param {string} fieldId The field id.
204
+ * @return {Array<{id: string, label: string}>|null}
205
+ */
206
+ valueOptions(fieldId) {
207
+ const field = this.fieldOf(fieldId)
208
+ if (field?.type === 'boolean') {
209
+ return [
210
+ { id: true, label: t('nextcloud-vue', 'Yes') },
211
+ { id: false, label: t('nextcloud-vue', 'No') },
212
+ ]
213
+ }
214
+ if (Array.isArray(field?.options) && field.options.length > 0) {
215
+ return field.options.map((o) => (typeof o === 'object'
216
+ ? { id: o.id ?? o.value, label: o.label ?? String(o.id ?? o.value) }
217
+ : { id: o, label: String(o) }))
218
+ }
219
+ return null
220
+ },
221
+
222
+ valueOption(row) {
223
+ return (this.valueOptions(row.field) || []).find((o) => String(o.id) === String(row.value)) || null
224
+ },
225
+
226
+ inputType(fieldId) {
227
+ const type = this.fieldOf(fieldId)?.type
228
+ if (type === 'number' || type === 'integer') {
229
+ return 'number'
230
+ }
231
+ return type === 'date' ? 'date' : 'text'
232
+ },
233
+
234
+ /**
235
+ * A typed value as stored: a number for a number field.
236
+ *
237
+ * @param {string} fieldId The field id.
238
+ * @param {string} raw What was typed.
239
+ * @return {string|number}
240
+ */
241
+ typedValue(fieldId, raw) {
242
+ const type = this.fieldOf(fieldId)?.type
243
+ if ((type === 'number' || type === 'integer') && raw !== '' && !Number.isNaN(Number(raw))) {
244
+ return Number(raw)
245
+ }
246
+ return raw
247
+ },
248
+
249
+ setField(i, option) {
250
+ const field = this.fieldOf(option?.id)
251
+ const operators = operatorsForType(field?.type)
252
+ this.rows[i] = { field: option?.id || '', operator: operators[0], value: field?.type === 'boolean' ? true : '' }
253
+ this.emitRows()
254
+ },
255
+
256
+ setOperator(i, option) {
257
+ const row = { ...this.rows[i], operator: option?.id || 'eq' }
258
+ if (!operatorTakesValue(row.operator)) {
259
+ delete row.value
260
+ } else if (row.value === undefined) {
261
+ row.value = ''
262
+ }
263
+ this.rows[i] = row
264
+ this.emitRows()
265
+ },
266
+
267
+ setValue(i, value) {
268
+ this.rows[i] = { ...this.rows[i], value }
269
+ this.emitRows()
270
+ },
271
+
272
+ add() {
273
+ this.rows.push({ field: '', operator: 'eq', value: '' })
274
+ this.emitRows()
275
+ },
276
+
277
+ remove(i) {
278
+ this.rows.splice(i, 1)
279
+ this.emitRows()
280
+ },
281
+
282
+ /**
283
+ * Tell the consumer, in both v-model dialects.
284
+ *
285
+ * @return {void}
286
+ */
287
+ emitRows() {
288
+ const next = this.copy(this.rows)
289
+ /**
290
+ * The conditions changed.
291
+ *
292
+ * @event input
293
+ * @type {Array}
294
+ */
295
+ this.$emit('input', next)
296
+ /**
297
+ * The conditions changed (Vue 3 v-model).
298
+ *
299
+ * @event update:modelValue
300
+ * @type {Array}
301
+ */
302
+ this.$emit('update:modelValue', next)
303
+ },
304
+ },
305
+ }
306
+ </script>
307
+
308
+ <style scoped>
309
+ .cn-condition-list {
310
+ display: flex;
311
+ flex-direction: column;
312
+ gap: 8px;
313
+ border: 0;
314
+ margin: 0;
315
+ padding: 0;
316
+ min-width: 0;
317
+ }
318
+
319
+ .cn-condition-list__label {
320
+ font-weight: 600;
321
+ padding: 0;
322
+ margin-bottom: 4px;
323
+ }
324
+
325
+ .cn-condition-list__empty {
326
+ color: var(--color-text-maxcontrast);
327
+ margin: 0;
328
+ }
329
+
330
+ .cn-condition-list__row {
331
+ display: grid;
332
+ grid-template-columns: minmax(0, 2fr) minmax(0, 1.4fr) minmax(0, 1.6fr) auto;
333
+ gap: 8px;
334
+ align-items: end;
335
+ }
336
+
337
+ .cn-condition-list__row > :nth-child(3):last-child {
338
+ grid-column: 4;
339
+ }
340
+
341
+ @media (max-width: 640px) {
342
+ .cn-condition-list__row {
343
+ grid-template-columns: minmax(0, 1fr) auto;
344
+ }
345
+ }
346
+ </style>
@@ -0,0 +1,126 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * The operators a condition can use on a field of a given type, and a
6
+ * condition written as the words of a rule ("Bouwkosten groter is dan
7
+ * € 100.000"). Shared by CnConditionListEditor and the apps that list rules.
8
+ *
9
+ * @module components/CnConditionListEditor/conditionOperators
10
+ */
11
+
12
+ import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
13
+
14
+ /**
15
+ * Every operator, in the order the editor offers them.
16
+ *
17
+ * `present` needs no value: the field is filled in.
18
+ *
19
+ * @type {ReadonlyArray<string>}
20
+ */
21
+ export const CONDITION_OPERATORS = Object.freeze(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'present'])
22
+
23
+ const OPERATORS_BY_TYPE = Object.freeze({
24
+ number: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'present'],
25
+ integer: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'present'],
26
+ date: ['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'present'],
27
+ boolean: ['eq'],
28
+ presence: ['present'],
29
+ })
30
+
31
+ const DEFAULT_OPERATORS = Object.freeze(['eq', 'neq', 'present'])
32
+
33
+ /**
34
+ * The operators that fit a field type.
35
+ *
36
+ * Numbers and dates compare; yes/no is only "is"; a `presence` field (a
37
+ * document, a relation) can only be filled in; text and choices say is, is not
38
+ * or is filled in.
39
+ *
40
+ * @param {string} type The field type.
41
+ * @return {string[]} The operator ids.
42
+ */
43
+ export function operatorsForType(type) {
44
+ return [...(OPERATORS_BY_TYPE[type] || DEFAULT_OPERATORS)]
45
+ }
46
+
47
+ /**
48
+ * The label of an operator in the editor's operator choice.
49
+ *
50
+ * @param {string} operator The operator id.
51
+ * @return {string} The translated label.
52
+ */
53
+ export function operatorLabel(operator) {
54
+ switch (operator) {
55
+ case 'eq': return t('nextcloud-vue', 'is')
56
+ case 'neq': return t('nextcloud-vue', 'is not')
57
+ case 'gt': return t('nextcloud-vue', 'greater than')
58
+ case 'gte': return t('nextcloud-vue', 'at least')
59
+ case 'lt': return t('nextcloud-vue', 'less than')
60
+ case 'lte': return t('nextcloud-vue', 'at most')
61
+ case 'present': return t('nextcloud-vue', 'is filled in')
62
+ default: return String(operator || '')
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Whether an operator needs a value.
68
+ *
69
+ * @param {string} operator The operator id.
70
+ * @return {boolean} False for `present`.
71
+ */
72
+ export function operatorTakesValue(operator) {
73
+ return operator !== 'present'
74
+ }
75
+
76
+ /**
77
+ * A condition's value as a reader sees it, formatted by its field.
78
+ *
79
+ * @param {unknown} value The stored value.
80
+ * @param {object|undefined} field The field (`type`, `format`, `options`).
81
+ * @return {string} The formatted value.
82
+ */
83
+ export function formatConditionValue(value, field) {
84
+ const type = field?.type
85
+ if (type === 'boolean') {
86
+ const yes = value === true || value === 'true' || value === 1 || value === '1'
87
+ return yes ? t('nextcloud-vue', 'yes') : t('nextcloud-vue', 'no')
88
+ }
89
+ if (Array.isArray(field?.options)) {
90
+ const option = field.options.find((o) => String(o?.id ?? o?.value ?? o) === String(value))
91
+ if (option && typeof option === 'object') {
92
+ return String(option.label ?? option.id ?? value)
93
+ }
94
+ }
95
+ if ((type === 'number' || type === 'integer') && value !== '' && value !== null && !Number.isNaN(Number(value))) {
96
+ const options = field?.format === 'currency'
97
+ ? { style: 'currency', currency: field.currency || 'EUR', maximumFractionDigits: 0 }
98
+ : {}
99
+ return new Intl.NumberFormat(getCanonicalLocale() || undefined, options).format(Number(value))
100
+ }
101
+ return String(value ?? '')
102
+ }
103
+
104
+ /**
105
+ * A condition as the clause of a rule, after "Als" / "If".
106
+ *
107
+ * @param {{field: string, operator: string, value?: unknown}} condition The condition.
108
+ * @param {Array<{id: string, label?: string, type?: string}>} fields The fields the condition may name.
109
+ * @return {string} The translated clause, for example "Bouwkosten groter is dan € 100.000".
110
+ */
111
+ export function conditionSentence(condition, fields = []) {
112
+ const field = (fields || []).find((f) => f?.id === condition?.field)
113
+ const vars = {
114
+ field: field?.label || condition?.field || '',
115
+ value: formatConditionValue(condition?.value, field),
116
+ }
117
+ switch (condition?.operator) {
118
+ case 'neq': return t('nextcloud-vue', '{field} is not {value}', vars)
119
+ case 'gt': return t('nextcloud-vue', '{field} is greater than {value}', vars)
120
+ case 'gte': return t('nextcloud-vue', '{field} is at least {value}', vars)
121
+ case 'lt': return t('nextcloud-vue', '{field} is less than {value}', vars)
122
+ case 'lte': return t('nextcloud-vue', '{field} is at most {value}', vars)
123
+ case 'present': return t('nextcloud-vue', '{field} is filled in', vars)
124
+ default: return t('nextcloud-vue', '{field} is {value}', vars)
125
+ }
126
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+
6
+ import CnConditionListEditor from './CnConditionListEditor.vue'
7
+
8
+ export { CnConditionListEditor }
9
+ export {
10
+ CONDITION_OPERATORS,
11
+ conditionSentence,
12
+ formatConditionValue,
13
+ operatorLabel,
14
+ operatorsForType,
15
+ operatorTakesValue,
16
+ } from './conditionOperators.js'
17
+ export default CnConditionListEditor
@@ -184,6 +184,7 @@ export default {
184
184
  | `loadingLabel` | String | `'Loading...'` | Message shown below the spinner during loading |
185
185
  | `band` | Array | `[]` | A row of widgets across the whole page, above the body and the side column (manifest `config.band`). Each entry is a widget id from `widgets`, a widget definition, or `{ widgetId, span, chrome }`: `span` is the entry's share of the row in twelfths (default 12), and `chrome` is `card` or `bare` (a banner defaults to bare). Each entry follows its `visibleWhen`. |
186
186
  | `recordLastChange` | Boolean | `false` | Read who changed the record last (as `headerMetaLastChange` does) for the widgets on the page: a facts card reads it as `_lastChange.userName` / `_lastChange.created` (DqContact "Historie"). |
187
+ | `cardButtons` (`card-buttons`) | String | `'compact'` | How tall the labelled buttons in the page's card title rows stand in the board look: `compact` (34px, setting and case-type pages) or `regular` (the 40px control height, the case page's tab cards: DqZaakContact "Live gesprek"). Manifest `config.cardButtons`. |
187
188
  | `pageFilters` | Array | `[]` | Page-level filters (manifest `config.pageFilters`), as on a dashboard: each writes its choice into the page's workspace context, which the widgets read through `@page.<key>` / `@workspace.<key>`. Drawn in the band, above its widgets (PtPortal "Periode"). |
188
189
  | `sidebarOpen` | Boolean | `true` | Whether the sidebar starts expanded |
189
190
  | `objectType` | String | `''` | Registered object type slug for the sidebar |
@@ -23,7 +23,7 @@
23
23
  <template>
24
24
  <div
25
25
  class="cn-detail-page"
26
- :class="[{ 'cn-detail-page--with-side': showsSideColumn }, boardClass]"
26
+ :class="[{ 'cn-detail-page--with-side': showsSideColumn, 'cn-detail-page--card-buttons-regular': cardButtons === 'regular' }, boardClass]"
27
27
  data-testid="cn-detail-page"
28
28
  :style="{ maxWidth: maxWidth }">
29
29
  <!-- Skip link to the page's primary action. Off screen until it takes
@@ -1841,6 +1841,18 @@ export default {
1841
1841
  default: '',
1842
1842
  },
1843
1843
 
1844
+ /**
1845
+ * How tall the labelled buttons in the page's card title rows stand in
1846
+ * the board look: `compact` (34px, setting and case-type pages) or
1847
+ * `regular` (the 40px control height, the case page's tab cards:
1848
+ * DqZaakContact "Live gesprek"). Manifest `config.cardButtons`.
1849
+ */
1850
+ cardButtons: {
1851
+ type: String,
1852
+ default: 'compact',
1853
+ validator: (value) => ['compact', 'regular'].includes(value),
1854
+ },
1855
+
1844
1856
  /**
1845
1857
  * NcDialog size for this page's create and edit form dialogs.
1846
1858
  *
@@ -8931,7 +8931,14 @@ export default {
8931
8931
  deep: true,
8932
8932
  },
8933
8933
 
8934
- '$route.query._view': function() {
8934
+ '$route.query._view': function(now, before) {
8935
+ // Leaving the mode's address (a plain link to the page) leaves the
8936
+ // mode it opened, unless the person picked another one since.
8937
+ if ((typeof now !== 'string' || now === '') && typeof before === 'string' && before !== '' && before === this.currentViewMode) {
8938
+ this.routeViewPending = false
8939
+ this.currentViewMode = this.viewMode
8940
+ return
8941
+ }
8935
8942
  this.applyViewModeFromRoute()
8936
8943
  },
8937
8944