@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.
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue.js +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +14 -0
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +5 -4
- package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue.js +9 -0
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue.js.map +1 -0
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue2.js +237 -0
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue2.js.map +1 -0
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue3.js +103 -0
- package/dist/esm/components/CnConditionListEditor/CnConditionListEditor.vue3.js.map +1 -0
- package/dist/esm/components/CnConditionListEditor/conditionOperators.js +121 -0
- package/dist/esm/components/CnConditionListEditor/conditionOperators.js.map +1 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +12 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +8 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue.js +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js +46 -7
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js +1 -1
- package/dist/esm/components/CnNcWidgetWidget/CnNcWidgetWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js +59 -3
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js +197 -127
- package/dist/esm/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +42 -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/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +1 -1
- 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/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +17 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +17 -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 +277 -166
- package/dist/esm/store/plugins/liveUpdates.js +13 -4
- package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
- package/dist/esm/store/useObjectStore.js +28 -5
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/detailActionModel.js +6 -0
- package/dist/esm/utils/detailActionModel.js.map +1 -1
- package/dist/esm/utils/listLenses.js +20 -1
- package/dist/esm/utils/listLenses.js.map +1 -1
- package/dist/esm/utils/objectRows.js +59 -0
- package/dist/esm/utils/objectRows.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +5961 -5127
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +277 -166
- package/l10n/en.json +17 -1
- package/l10n/nl.json +17 -1
- package/package.json +1 -1
- package/src/components/CnActionButtons/CnActionButtons.vue +15 -0
- package/src/components/CnConditionListEditor/CnConditionListEditor.vue +346 -0
- package/src/components/CnConditionListEditor/conditionOperators.js +126 -0
- package/src/components/CnConditionListEditor/index.js +17 -0
- package/src/components/CnDetailPage/CnDetailPage.md +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +13 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +8 -1
- package/src/components/CnNcWidgetWidget/CnNcWidgetWidget.vue +47 -8
- package/src/components/CnObjectPartiesWidget/CnObjectPartiesWidget.vue +139 -2
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +42 -2
- package/src/components/CnTabsWidget/CnTabsWidget.vue +1 -1
- package/src/components/index.js +1 -0
- package/src/css/look-board-index.css +6 -0
- package/src/css/look-board.css +35 -7
- package/src/index.js +2 -0
- package/src/schemas/app-manifest-v2.schema.json +17 -1
- package/src/store/plugins/liveUpdates.js +13 -4
- package/src/store/useObjectStore.js +28 -5
- package/src/utils/detailActionModel.js +6 -0
- package/src/utils/listLenses.js +20 -1
- package/src/utils/objectRows.js +55 -0
- 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.
|
|
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
|
|