@conduction/nextcloud-vue 2.117.0 → 2.118.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/CnActionDialog/CnActionDialog.vue2.js +2 -1
- package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +6 -6
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js +2 -1
- package/dist/esm/components/CnAssignDialog/CnAssignDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +18 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +14 -8
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +11 -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/CnIndexPage/CnIndexPage.vue2.js +9 -4
- 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/CnMailTab/CnMailTab.vue2.js +2 -1
- package/dist/esm/components/CnMailTab/CnMailTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +71 -0
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +25 -9
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +11 -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/CnSearchPage/CnSearchPage.vue.js +1 -1
- package/dist/esm/components/CnSearchPage/CnSearchPage.vue2.js +161 -5
- package/dist/esm/components/CnSearchPage/CnSearchPage.vue2.js.map +1 -1
- package/dist/esm/components/CnSearchPage/CnSearchPage.vue3.js +9 -9
- package/dist/esm/components/CnSearchPage/CnSearchPage.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +35 -5
- 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/CnTransitionInputFields/CnTransitionInputFields.vue.js +9 -0
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue.js.map +1 -0
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js +435 -0
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue2.js.map +1 -0
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js +300 -0
- package/dist/esm/components/CnTransitionInputFields/CnTransitionInputFields.vue3.js.map +1 -0
- package/dist/esm/composables/useEndpointSource.js +1 -36
- package/dist/esm/composables/useEndpointSource.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue.js +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js +57 -336
- package/dist/esm/dialogs/CnTransitionInputDialog.vue2.js.map +1 -1
- package/dist/esm/dialogs/CnTransitionInputDialog.vue3.js +18 -155
- package/dist/esm/dialogs/CnTransitionInputDialog.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 +2 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +2 -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 +308 -229
- package/dist/esm/utils/actionsDispatcher.js +3 -2
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/interpolateUrlTokens.js +43 -0
- package/dist/esm/utils/interpolateUrlTokens.js.map +1 -0
- package/dist/esm/utils/labelsByField.js +72 -0
- package/dist/esm/utils/labelsByField.js.map +1 -0
- package/dist/esm/utils/listCreateAction.js +91 -0
- package/dist/esm/utils/listCreateAction.js.map +1 -0
- package/dist/esm/utils/transitionInputs.js +493 -0
- package/dist/esm/utils/transitionInputs.js.map +1 -0
- package/dist/esm/utils/visibleWhen.js +9 -2
- package/dist/esm/utils/visibleWhen.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +6690 -5404
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +308 -229
- package/l10n/en.json +2 -1
- package/l10n/nl.json +2 -1
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.vue +6 -6
- package/src/components/CnDashboardPage/CnDashboardPage.vue +27 -5
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +11 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +9 -4
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +86 -0
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +11 -2
- package/src/components/CnSearchPage/CnSearchPage.vue +170 -14
- package/src/components/CnTabsWidget/CnTabsWidget.vue +35 -5
- package/src/components/CnTransitionInputFields/CnTransitionInputFields.vue +750 -0
- package/src/components/CnTransitionInputFields/index.js +11 -0
- package/src/components/index.js +1 -0
- package/src/composables/useEndpointSource.js +3 -36
- package/src/dialogs/CnTransitionInputDialog.vue +73 -465
- package/src/index.js +2 -0
- package/src/utils/interpolateUrlTokens.js +40 -0
- package/src/utils/labelsByField.js +69 -0
- package/src/utils/listCreateAction.js +87 -0
- package/src/utils/transitionInputs.js +489 -0
- package/src/utils/visibleWhen.js +9 -2
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
- SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
- SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
-->
|
|
5
|
+
<template>
|
|
6
|
+
<div
|
|
7
|
+
class="cn-transition-input-fields"
|
|
8
|
+
:class="{ 'cn-transition-input-fields--columns': columns === 2 }">
|
|
9
|
+
<template v-if="summaryShown">
|
|
10
|
+
<div v-if="summary.card"
|
|
11
|
+
class="cn-transition-input-fields__card cn-transition-input-fields__wide"
|
|
12
|
+
:data-testid="summaryTestid('card')">
|
|
13
|
+
<component :is="summary.card.icon"
|
|
14
|
+
v-if="summary.card.icon"
|
|
15
|
+
:size="20"
|
|
16
|
+
class="cn-transition-input-fields__card-icon" />
|
|
17
|
+
<span class="cn-transition-input-fields__card-text">
|
|
18
|
+
<strong>{{ contextText(summary.card.title) }}</strong>
|
|
19
|
+
<span class="cn-transition-input-fields__muted">{{ cardMeta }}</span>
|
|
20
|
+
</span>
|
|
21
|
+
<span v-if="contextText(summary.card.badge)" class="cn-transition-input-fields__badge">
|
|
22
|
+
{{ contextText(summary.card.badge) }}
|
|
23
|
+
</span>
|
|
24
|
+
</div>
|
|
25
|
+
<section v-if="Array.isArray(summary.facts) && summary.facts.length > 0"
|
|
26
|
+
class="cn-transition-input-fields__wide"
|
|
27
|
+
:data-testid="summaryTestid('facts')">
|
|
28
|
+
<h3 v-if="summary.title" class="cn-transition-input-fields__section-title">
|
|
29
|
+
{{ summary.title }}
|
|
30
|
+
</h3>
|
|
31
|
+
<dl class="cn-transition-input-fields__facts">
|
|
32
|
+
<div v-for="fact in summary.facts" :key="fact.path || fact.label">
|
|
33
|
+
<dt>{{ fact.label }}</dt>
|
|
34
|
+
<dd>{{ contextText(fact.path) || fact.fallback || '' }}</dd>
|
|
35
|
+
</div>
|
|
36
|
+
</dl>
|
|
37
|
+
</section>
|
|
38
|
+
</template>
|
|
39
|
+
|
|
40
|
+
<template v-for="field in shownFields" :key="field.key">
|
|
41
|
+
<div
|
|
42
|
+
class="cn-transition-input__field"
|
|
43
|
+
:class="{
|
|
44
|
+
'cn-transition-input__field--refused': isRefused(field.key),
|
|
45
|
+
'cn-transition-input-fields__wide': field.input.wide === true,
|
|
46
|
+
}"
|
|
47
|
+
:data-testid="`cn-transition-input-${field.key}`">
|
|
48
|
+
<!-- A template fills another input; it is never sent itself. -->
|
|
49
|
+
<CnTemplateSelect v-if="field.widget === 'template'"
|
|
50
|
+
:url="field.input.url || ''"
|
|
51
|
+
:schema="field.input.schema || ''"
|
|
52
|
+
:params="field.input.params || {}"
|
|
53
|
+
:mapOption="field.input.mapOption"
|
|
54
|
+
:inputLabel="field.label"
|
|
55
|
+
:placeholder="field.input.placeholder || ''"
|
|
56
|
+
:data-testid="field.input.testid || `cn-transition-input-template-${field.key}`"
|
|
57
|
+
@apply="applyTemplate(field, $event)" />
|
|
58
|
+
|
|
59
|
+
<!-- An input that declares its choices is picked from them. -->
|
|
60
|
+
<NcSelect v-else-if="field.widget === 'select'"
|
|
61
|
+
:modelValue="selectedOption(field)"
|
|
62
|
+
:options="field.options"
|
|
63
|
+
label="label"
|
|
64
|
+
:inputLabel="labelOf(field)"
|
|
65
|
+
:clearable="!field.required"
|
|
66
|
+
:disabled="disabled"
|
|
67
|
+
:data-testid="field.input.testid || `cn-transition-input-select-${field.key}`"
|
|
68
|
+
@update:modelValue="setValue(field.key, $event && $event.value !== undefined ? $event.value : '')" />
|
|
69
|
+
|
|
70
|
+
<!-- An input that points at another record is picked, never typed. -->
|
|
71
|
+
<div v-else-if="field.widget === 'reference'" class="cn-transition-input__reference">
|
|
72
|
+
<label class="cn-transition-input__label">{{ labelOf(field) }}</label>
|
|
73
|
+
<CnResourceSelect
|
|
74
|
+
:register="field.referenceRegister"
|
|
75
|
+
:schema="String(field.reference.schema)"
|
|
76
|
+
:labelField="(field.picker && field.picker.labelField) || field.reference.labelField || 'name'"
|
|
77
|
+
:filter="(field.picker && field.picker.filter) || {}"
|
|
78
|
+
:exclude="excludedIds(field)"
|
|
79
|
+
:allowCreate="false"
|
|
80
|
+
:preload="true"
|
|
81
|
+
:inputLabel="labelOf(field)"
|
|
82
|
+
:modelValue="String(modelValue[field.key] ?? '')"
|
|
83
|
+
:data-testid="`cn-transition-input-picker-${field.key}`"
|
|
84
|
+
@update:modelValue="setValue(field.key, $event)" />
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- An object input: its sub-properties, one field at a time. -->
|
|
88
|
+
<fieldset v-else-if="field.widget === 'object'" class="cn-transition-input__object">
|
|
89
|
+
<legend>{{ labelOf(field) }}</legend>
|
|
90
|
+
<div v-for="sub in field.subFields"
|
|
91
|
+
:key="sub.key"
|
|
92
|
+
class="cn-transition-input__subfield"
|
|
93
|
+
:data-testid="`cn-transition-input-${field.key}-${sub.key}`">
|
|
94
|
+
<NcTextArea v-if="sub.widget === 'textarea'"
|
|
95
|
+
:modelValue="String(subValue(field.key, sub.key) ?? '')"
|
|
96
|
+
:label="subLabel(sub)"
|
|
97
|
+
rows="3"
|
|
98
|
+
@update:modelValue="setSubValue(field.key, sub.key, $event)" />
|
|
99
|
+
<NcCheckboxRadioSwitch v-else-if="sub.widget === 'checkbox'"
|
|
100
|
+
:modelValue="subValue(field.key, sub.key) === true"
|
|
101
|
+
type="switch"
|
|
102
|
+
@update:modelValue="setSubValue(field.key, sub.key, $event === true)">
|
|
103
|
+
{{ subLabel(sub) }}
|
|
104
|
+
</NcCheckboxRadioSwitch>
|
|
105
|
+
<NcTextField v-else
|
|
106
|
+
:modelValue="String(subValue(field.key, sub.key) ?? '')"
|
|
107
|
+
:type="sub.widget === 'number' ? 'number' : 'text'"
|
|
108
|
+
:label="subLabel(sub)"
|
|
109
|
+
@update:modelValue="setSubValue(field.key, sub.key, $event)" />
|
|
110
|
+
<p v-if="isSubRefused(field.key, sub.key)"
|
|
111
|
+
class="cn-transition-input__field-error"
|
|
112
|
+
:data-testid="`cn-transition-input-error-${field.key}.${sub.key}`">
|
|
113
|
+
{{ t('nextcloud-vue', 'This value was not accepted.') }}
|
|
114
|
+
</p>
|
|
115
|
+
</div>
|
|
116
|
+
</fieldset>
|
|
117
|
+
|
|
118
|
+
<NcCheckboxRadioSwitch v-else-if="field.widget === 'checkbox'"
|
|
119
|
+
:modelValue="modelValue[field.key] === true"
|
|
120
|
+
type="switch"
|
|
121
|
+
:disabled="disabled"
|
|
122
|
+
@update:modelValue="setValue(field.key, $event === true)">
|
|
123
|
+
{{ labelOf(field) }}
|
|
124
|
+
</NcCheckboxRadioSwitch>
|
|
125
|
+
|
|
126
|
+
<template v-else>
|
|
127
|
+
<!-- A label drawn above the field, as a form board draws it,
|
|
128
|
+
is tied to the field by its id. -->
|
|
129
|
+
<label v-if="field.input.labelOutside === true"
|
|
130
|
+
class="cn-transition-input-fields__label"
|
|
131
|
+
:for="inputId(field)">{{ labelOf(field) }}</label>
|
|
132
|
+
<NcTextArea v-if="field.widget === 'textarea'"
|
|
133
|
+
:id="inputId(field)"
|
|
134
|
+
:modelValue="String(modelValue[field.key] ?? '')"
|
|
135
|
+
:label="labelOf(field)"
|
|
136
|
+
:labelOutside="field.input.labelOutside === true"
|
|
137
|
+
:helperText="field.description || ''"
|
|
138
|
+
:rows="field.input.rows || 4"
|
|
139
|
+
:disabled="disabled"
|
|
140
|
+
resize="vertical"
|
|
141
|
+
:data-testid="field.input.testid || null"
|
|
142
|
+
@update:modelValue="setValue(field.key, $event)" />
|
|
143
|
+
<NcTextField v-else
|
|
144
|
+
:id="inputId(field)"
|
|
145
|
+
:modelValue="String(modelValue[field.key] ?? '')"
|
|
146
|
+
:type="textType(field)"
|
|
147
|
+
:label="labelOf(field)"
|
|
148
|
+
:labelOutside="field.input.labelOutside === true"
|
|
149
|
+
:helperText="field.description || ''"
|
|
150
|
+
:disabled="disabled"
|
|
151
|
+
:data-testid="field.input.testid || null"
|
|
152
|
+
@update:modelValue="setValue(field.key, $event)" />
|
|
153
|
+
<span v-if="field.input.note" class="cn-transition-input-fields__muted">
|
|
154
|
+
{{ field.input.note }}
|
|
155
|
+
</span>
|
|
156
|
+
</template>
|
|
157
|
+
|
|
158
|
+
<p v-if="isRefused(field.key)"
|
|
159
|
+
class="cn-transition-input__field-error"
|
|
160
|
+
:data-testid="`cn-transition-input-error-${field.key}`">
|
|
161
|
+
{{ refusalFor(field) }}
|
|
162
|
+
</p>
|
|
163
|
+
</div>
|
|
164
|
+
|
|
165
|
+
<!-- What the input means once filled in, such as the day a paused
|
|
166
|
+
term comes back. Shown only once it can be worked out. -->
|
|
167
|
+
<div v-if="field.previewText"
|
|
168
|
+
:key="`${field.key}-preview`"
|
|
169
|
+
class="cn-transition-input-fields__preview cn-transition-input-fields__wide"
|
|
170
|
+
:data-testid="field.input.preview.testid || `cn-transition-input-preview-${field.key}`">
|
|
171
|
+
<span class="cn-transition-input-fields__label">{{ field.input.preview.label }}</span>
|
|
172
|
+
<span :data-testid="`${field.input.preview.testid || `cn-transition-input-preview-${field.key}`}-value`">{{ field.previewText }}</span>
|
|
173
|
+
<span v-if="field.input.preview.note" class="cn-transition-input-fields__muted">
|
|
174
|
+
{{ field.input.preview.note }}
|
|
175
|
+
</span>
|
|
176
|
+
</div>
|
|
177
|
+
</template>
|
|
178
|
+
|
|
179
|
+
<NcNoteCard v-if="notice && notice.text"
|
|
180
|
+
class="cn-transition-input-fields__wide"
|
|
181
|
+
:type="notice.type || 'info'"
|
|
182
|
+
:heading="notice.heading || ''">
|
|
183
|
+
{{ notice.text }}
|
|
184
|
+
</NcNoteCard>
|
|
185
|
+
|
|
186
|
+
<!-- A key the refusal names that this form never offered is not a
|
|
187
|
+
field anybody can correct here. Saying so beats saying nothing:
|
|
188
|
+
the request sent something the action does not accept. -->
|
|
189
|
+
<p v-for="key in unofferedRefusals"
|
|
190
|
+
:key="`unoffered-${key}`"
|
|
191
|
+
class="cn-transition-input__field-error"
|
|
192
|
+
:data-testid="`cn-transition-input-error-${key}`">
|
|
193
|
+
{{ t('nextcloud-vue', 'This action does not accept the field "{field}".', { field: key }) }}
|
|
194
|
+
</p>
|
|
195
|
+
</div>
|
|
196
|
+
</template>
|
|
197
|
+
|
|
198
|
+
<script>
|
|
199
|
+
import { translate as t } from '@nextcloud/l10n'
|
|
200
|
+
import { NcCheckboxRadioSwitch, NcNoteCard, NcSelect, NcTextArea, NcTextField } from '@nextcloud/vue'
|
|
201
|
+
import CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'
|
|
202
|
+
import CnTemplateSelect from '../CnTemplateSelect/CnTemplateSelect.vue'
|
|
203
|
+
import { readPath } from '../../utils/readPath.js'
|
|
204
|
+
import { applyTemplateText, isInputFilled, resolveInputFields } from '../../utils/transitionInputs.js'
|
|
205
|
+
|
|
206
|
+
let uid = 0
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* CnTransitionInputFields — the declared inputs of a transition or an act,
|
|
210
|
+
* drawn wherever the host puts them.
|
|
211
|
+
*
|
|
212
|
+
* CnTransitionInputDialog draws them in their own dialog. A host that lists
|
|
213
|
+
* several acts and opens the chosen one's inputs in place (under its card)
|
|
214
|
+
* draws them with this component, so both read the same declarations and
|
|
215
|
+
* decide the same way. The values are a `v-model` object keyed by field;
|
|
216
|
+
* whether they are complete is `inputFieldsComplete(resolveInputFields(...))`.
|
|
217
|
+
*
|
|
218
|
+
* Next to the schema-resolved widgets an input may declare `widget` (`text`,
|
|
219
|
+
* `textarea`, `number`, `date`, `select`, `template`), `optionsSource` (a dot
|
|
220
|
+
* path into `context`), `other`, `helperFrom`, `preview`, `default` and
|
|
221
|
+
* presentation keys; see `resolveInputFields` in `utils/transitionInputs.js`.
|
|
222
|
+
*
|
|
223
|
+
* ```vue
|
|
224
|
+
* <CnTransitionInputFields
|
|
225
|
+
* v-model="values"
|
|
226
|
+
* :inputs="act.inputs"
|
|
227
|
+
* :context="{ pauseReasons }"
|
|
228
|
+
* :columns="2" />
|
|
229
|
+
* ```
|
|
230
|
+
*
|
|
231
|
+
* @spec openspec/changes/dq-abstract-forms/specs/transition-inputs/spec.md
|
|
232
|
+
*/
|
|
233
|
+
export default {
|
|
234
|
+
name: 'CnTransitionInputFields',
|
|
235
|
+
|
|
236
|
+
components: {
|
|
237
|
+
CnResourceSelect,
|
|
238
|
+
CnTemplateSelect,
|
|
239
|
+
NcCheckboxRadioSwitch,
|
|
240
|
+
NcNoteCard,
|
|
241
|
+
NcSelect,
|
|
242
|
+
NcTextArea,
|
|
243
|
+
NcTextField,
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
props: {
|
|
247
|
+
/**
|
|
248
|
+
* The declared inputs: `[{ field, required, label, widget, options,
|
|
249
|
+
* optionsSource, optionValue, optionLabel, other, helperFrom, preview,
|
|
250
|
+
* default, description, note, labelOutside, id, wide, testid, rows,
|
|
251
|
+
* placeholder }]`, plus for a `template` input `url`, `schema`,
|
|
252
|
+
* `params`, `mapOption` and `applyTo`.
|
|
253
|
+
*
|
|
254
|
+
* @type {Array<object>}
|
|
255
|
+
*/
|
|
256
|
+
inputs: {
|
|
257
|
+
type: Array,
|
|
258
|
+
default: () => [],
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The values, keyed by field.
|
|
263
|
+
*
|
|
264
|
+
* @type {object}
|
|
265
|
+
*/
|
|
266
|
+
modelValue: {
|
|
267
|
+
type: Object,
|
|
268
|
+
default: () => ({}),
|
|
269
|
+
},
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* The object's JSON Schema, which labels and types the inputs that
|
|
273
|
+
* declare no widget of their own.
|
|
274
|
+
*
|
|
275
|
+
* @type {object|null}
|
|
276
|
+
*/
|
|
277
|
+
schema: {
|
|
278
|
+
type: Object,
|
|
279
|
+
default: null,
|
|
280
|
+
},
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The record the action runs on: `picker.excludeSelf` leaves its id
|
|
284
|
+
* out, and an object input is merged over its present value.
|
|
285
|
+
*
|
|
286
|
+
* @type {object|null}
|
|
287
|
+
*/
|
|
288
|
+
currentObject: {
|
|
289
|
+
type: Object,
|
|
290
|
+
default: null,
|
|
291
|
+
},
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Register slug the reference pickers look in when the schema property
|
|
295
|
+
* names none of its own.
|
|
296
|
+
*
|
|
297
|
+
* @type {string}
|
|
298
|
+
*/
|
|
299
|
+
register: {
|
|
300
|
+
type: String,
|
|
301
|
+
default: '',
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* What `optionsSource` and the summary paths read, such as the case
|
|
306
|
+
* type's choices and the record shown in the summary.
|
|
307
|
+
*
|
|
308
|
+
* @type {object}
|
|
309
|
+
*/
|
|
310
|
+
context: {
|
|
311
|
+
type: Object,
|
|
312
|
+
default: () => ({}),
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The input keys a refusal named. Each one this form offers is marked
|
|
317
|
+
* on its own field; each one it does not is reported as a key the
|
|
318
|
+
* action does not accept.
|
|
319
|
+
*
|
|
320
|
+
* @type {string[]}
|
|
321
|
+
*/
|
|
322
|
+
fieldErrors: {
|
|
323
|
+
type: Array,
|
|
324
|
+
default: () => [],
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Whether the inputs are locked, such as while the action posts.
|
|
329
|
+
*
|
|
330
|
+
* @type {boolean}
|
|
331
|
+
*/
|
|
332
|
+
disabled: {
|
|
333
|
+
type: Boolean,
|
|
334
|
+
default: false,
|
|
335
|
+
},
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 1 stacks the inputs; 2 sets them side by side, with `wide` inputs,
|
|
339
|
+
* previews, the summary and the notice across both columns.
|
|
340
|
+
*
|
|
341
|
+
* @type {number}
|
|
342
|
+
*/
|
|
343
|
+
columns: {
|
|
344
|
+
type: Number,
|
|
345
|
+
default: 1,
|
|
346
|
+
},
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Whether a required input's label carries a ` *` marker.
|
|
350
|
+
*
|
|
351
|
+
* @type {boolean}
|
|
352
|
+
*/
|
|
353
|
+
markRequired: {
|
|
354
|
+
type: Boolean,
|
|
355
|
+
default: true,
|
|
356
|
+
},
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* A read-only block above the inputs about what the action works on:
|
|
360
|
+
* `{ when, card: { icon, title, meta: [], badge }, title, facts:
|
|
361
|
+
* [{ label, path, fallback }], testid }`. `when`, `title`, `meta`,
|
|
362
|
+
* `badge` and each fact's `path` are dot paths into `context`; the
|
|
363
|
+
* block is shown only when `when` resolves.
|
|
364
|
+
*
|
|
365
|
+
* @type {object|null}
|
|
366
|
+
*/
|
|
367
|
+
summary: {
|
|
368
|
+
type: Object,
|
|
369
|
+
default: null,
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* A note under the inputs: `{ type, heading, text }`.
|
|
374
|
+
*
|
|
375
|
+
* @type {object|null}
|
|
376
|
+
*/
|
|
377
|
+
notice: {
|
|
378
|
+
type: Object,
|
|
379
|
+
default: null,
|
|
380
|
+
},
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
emits: ['update:modelValue'],
|
|
384
|
+
|
|
385
|
+
data() {
|
|
386
|
+
uid += 1
|
|
387
|
+
return {
|
|
388
|
+
uid,
|
|
389
|
+
// The values with every change made since the host last passed
|
|
390
|
+
// them in, so two changes in one tick both survive.
|
|
391
|
+
latest: { ...this.modelValue },
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
|
|
395
|
+
computed: {
|
|
396
|
+
/** Every declared input, resolved against the schema and the context. */
|
|
397
|
+
fields() {
|
|
398
|
+
return resolveInputFields(this.inputs, {
|
|
399
|
+
schema: this.schema,
|
|
400
|
+
register: this.register,
|
|
401
|
+
context: this.context,
|
|
402
|
+
values: this.modelValue,
|
|
403
|
+
})
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
/** The inputs that are asked now. */
|
|
407
|
+
shownFields() {
|
|
408
|
+
return this.fields.filter((field) => !field.hidden)
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
/** Whether the summary block shows. */
|
|
412
|
+
summaryShown() {
|
|
413
|
+
if (!this.summary || typeof this.summary !== 'object') {
|
|
414
|
+
return false
|
|
415
|
+
}
|
|
416
|
+
return !this.summary.when || Boolean(readPath(this.context, this.summary.when))
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
/** The summary card's second line. */
|
|
420
|
+
cardMeta() {
|
|
421
|
+
const meta = Array.isArray(this.summary?.card?.meta) ? this.summary.card.meta : []
|
|
422
|
+
return meta.map((path) => this.contextText(path)).filter(Boolean).join(' · ')
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
/** The refusal's field keys, normalised to strings. */
|
|
426
|
+
refusedKeys() {
|
|
427
|
+
if (!Array.isArray(this.fieldErrors)) {
|
|
428
|
+
return []
|
|
429
|
+
}
|
|
430
|
+
return this.fieldErrors.filter((key) => typeof key === 'string' && key !== '')
|
|
431
|
+
},
|
|
432
|
+
|
|
433
|
+
/** The refused keys this form offers no field for. */
|
|
434
|
+
unofferedRefusals() {
|
|
435
|
+
const offered = this.fields.map((field) => field.key)
|
|
436
|
+
return this.refusedKeys.filter((key) => !offered.includes(key))
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
|
|
440
|
+
watch: {
|
|
441
|
+
modelValue(values) {
|
|
442
|
+
this.latest = { ...values }
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
methods: {
|
|
447
|
+
t,
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* A context value as text, '' when absent.
|
|
451
|
+
*
|
|
452
|
+
* @param {string} path Dot path into the context.
|
|
453
|
+
* @return {string}
|
|
454
|
+
*/
|
|
455
|
+
contextText(path) {
|
|
456
|
+
if (!path) {
|
|
457
|
+
return ''
|
|
458
|
+
}
|
|
459
|
+
const value = readPath(this.context, path)
|
|
460
|
+
return value === null || value === undefined ? '' : String(value)
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* The test id of one part of the summary.
|
|
465
|
+
*
|
|
466
|
+
* @param {string} part `card` or `facts`.
|
|
467
|
+
* @return {string}
|
|
468
|
+
*/
|
|
469
|
+
summaryTestid(part) {
|
|
470
|
+
return `${this.summary.testid || 'cn-transition-input-summary'}-${part}`
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* The field's label, with the required marker when asked for.
|
|
475
|
+
*
|
|
476
|
+
* @param {object} field The descriptor.
|
|
477
|
+
* @return {string}
|
|
478
|
+
*/
|
|
479
|
+
labelOf(field) {
|
|
480
|
+
return this.markRequired && field.required ? `${field.label} *` : field.label
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Label of a sub-field, with the required marker.
|
|
485
|
+
*
|
|
486
|
+
* @param {object} sub The sub-field descriptor.
|
|
487
|
+
* @return {string}
|
|
488
|
+
*/
|
|
489
|
+
subLabel(sub) {
|
|
490
|
+
return this.markRequired && sub.required ? `${sub.label} *` : sub.label
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* The element id of a text input: the declared one, or one unique to
|
|
495
|
+
* this form.
|
|
496
|
+
*
|
|
497
|
+
* @param {object} field The descriptor.
|
|
498
|
+
* @return {string}
|
|
499
|
+
*/
|
|
500
|
+
inputId(field) {
|
|
501
|
+
return field.input.id || `cn-transition-input-${this.uid}-${field.key}`
|
|
502
|
+
},
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* The native input type of a one-line input.
|
|
506
|
+
*
|
|
507
|
+
* @param {object} field The descriptor.
|
|
508
|
+
* @return {string}
|
|
509
|
+
*/
|
|
510
|
+
textType(field) {
|
|
511
|
+
return field.widget === 'number' || field.widget === 'date' ? field.widget : 'text'
|
|
512
|
+
},
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* The option a picker currently holds, or null.
|
|
516
|
+
*
|
|
517
|
+
* @param {object} field The select descriptor.
|
|
518
|
+
* @return {{value: string, label: string}|null}
|
|
519
|
+
*/
|
|
520
|
+
selectedOption(field) {
|
|
521
|
+
const value = this.modelValue[field.key]
|
|
522
|
+
return field.options.find((option) => option.value === String(value ?? '')) || null
|
|
523
|
+
},
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Ids a reference picker must not offer: the current record, when the
|
|
527
|
+
* input's `picker.excludeSelf` says so.
|
|
528
|
+
*
|
|
529
|
+
* @param {object} field The reference descriptor.
|
|
530
|
+
* @return {string[]}
|
|
531
|
+
*/
|
|
532
|
+
excludedIds(field) {
|
|
533
|
+
if (!field.picker || field.picker.excludeSelf !== true || !this.currentObject) {
|
|
534
|
+
return []
|
|
535
|
+
}
|
|
536
|
+
const self = this.currentObject
|
|
537
|
+
return [self.id, self.uuid, self['@self'] && self['@self'].id].filter((id) => id !== undefined && id !== null).map(String)
|
|
538
|
+
},
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Whether the refusal named this field.
|
|
542
|
+
*
|
|
543
|
+
* @param {string} key The field key.
|
|
544
|
+
* @return {boolean}
|
|
545
|
+
*/
|
|
546
|
+
isRefused(key) {
|
|
547
|
+
return this.refusedKeys.includes(key)
|
|
548
|
+
},
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* What to say beside a refused field. An empty one is a missing input;
|
|
552
|
+
* a filled one was refused for what is in it. The kind is decided here
|
|
553
|
+
* rather than read out of the refusal's sentence, which may be reworded.
|
|
554
|
+
*
|
|
555
|
+
* @param {object} field The descriptor.
|
|
556
|
+
* @return {string}
|
|
557
|
+
*/
|
|
558
|
+
refusalFor(field) {
|
|
559
|
+
return isInputFilled(field, this.modelValue)
|
|
560
|
+
? t('nextcloud-vue', 'This value was not accepted.')
|
|
561
|
+
: t('nextcloud-vue', 'This field is required.')
|
|
562
|
+
},
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Whether a refusal named `<input>.<sub>`.
|
|
566
|
+
*
|
|
567
|
+
* @param {string} key The input key.
|
|
568
|
+
* @param {string} sub The sub-field key.
|
|
569
|
+
* @return {boolean}
|
|
570
|
+
*/
|
|
571
|
+
isSubRefused(key, sub) {
|
|
572
|
+
return this.refusedKeys.includes(`${key}.${sub}`)
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* A typed sub-value.
|
|
577
|
+
*
|
|
578
|
+
* @param {string} key The input key.
|
|
579
|
+
* @param {string} sub The sub-field key.
|
|
580
|
+
* @return {unknown}
|
|
581
|
+
*/
|
|
582
|
+
subValue(key, sub) {
|
|
583
|
+
const group = this.modelValue[key]
|
|
584
|
+
return group && typeof group === 'object' ? group[sub] : undefined
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Store one sub-value, keeping the rest of the object input's values.
|
|
589
|
+
*
|
|
590
|
+
* @param {string} key The input key.
|
|
591
|
+
* @param {string} sub The sub-field key.
|
|
592
|
+
* @param {unknown} value The new value.
|
|
593
|
+
*/
|
|
594
|
+
setSubValue(key, sub, value) {
|
|
595
|
+
const group = this.latest[key] && typeof this.latest[key] === 'object' ? this.latest[key] : {}
|
|
596
|
+
this.setValue(key, { ...group, [sub]: value })
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Store one input's value.
|
|
601
|
+
*
|
|
602
|
+
* @param {string} key The field key.
|
|
603
|
+
* @param {unknown} value The new value.
|
|
604
|
+
*/
|
|
605
|
+
setValue(key, value) {
|
|
606
|
+
this.latest = { ...this.latest, [key]: value }
|
|
607
|
+
/**
|
|
608
|
+
* @event update:modelValue The values with this one changed.
|
|
609
|
+
* @type {object}
|
|
610
|
+
*/
|
|
611
|
+
this.$emit('update:modelValue', this.latest)
|
|
612
|
+
},
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Put a chosen template's text into the input it fills, unless that
|
|
616
|
+
* input already holds text.
|
|
617
|
+
*
|
|
618
|
+
* @param {object} field The template descriptor.
|
|
619
|
+
* @param {object|null} option The chosen template.
|
|
620
|
+
*/
|
|
621
|
+
applyTemplate(field, option) {
|
|
622
|
+
const next = applyTemplateText(this.latest, field.input.applyTo, option)
|
|
623
|
+
if (next !== this.latest) {
|
|
624
|
+
this.latest = next
|
|
625
|
+
/** @event update:modelValue The values with the template's text filled in. */
|
|
626
|
+
this.$emit('update:modelValue', next)
|
|
627
|
+
}
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
}
|
|
631
|
+
</script>
|
|
632
|
+
|
|
633
|
+
<style scoped>
|
|
634
|
+
.cn-transition-input-fields {
|
|
635
|
+
display: flex;
|
|
636
|
+
flex-direction: column;
|
|
637
|
+
gap: 12px;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
.cn-transition-input-fields--columns {
|
|
641
|
+
display: grid;
|
|
642
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
643
|
+
align-items: start;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
.cn-transition-input-fields--columns .cn-transition-input-fields__wide {
|
|
647
|
+
grid-column: 1 / -1;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
.cn-transition-input__field,
|
|
651
|
+
.cn-transition-input-fields__preview {
|
|
652
|
+
display: flex;
|
|
653
|
+
flex-direction: column;
|
|
654
|
+
gap: 6px;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.cn-transition-input__label {
|
|
658
|
+
display: block;
|
|
659
|
+
margin-bottom: 4px;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.cn-transition-input-fields__label {
|
|
663
|
+
font-size: 14px;
|
|
664
|
+
font-weight: 600;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
.cn-transition-input-fields__muted {
|
|
668
|
+
font-size: 13px;
|
|
669
|
+
color: var(--color-text-maxcontrast);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
.cn-transition-input__field-error {
|
|
673
|
+
margin: 4px 0 0;
|
|
674
|
+
font-size: 0.9em;
|
|
675
|
+
color: var(--color-error-text, var(--color-error));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
.cn-transition-input__field--refused {
|
|
679
|
+
border-inline-start: 2px solid var(--color-error);
|
|
680
|
+
padding-inline-start: 8px;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.cn-transition-input__object {
|
|
684
|
+
border: 1px solid var(--color-border);
|
|
685
|
+
border-radius: var(--border-radius);
|
|
686
|
+
padding: 8px 12px;
|
|
687
|
+
display: flex;
|
|
688
|
+
flex-direction: column;
|
|
689
|
+
gap: 8px;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
.cn-transition-input-fields__card {
|
|
693
|
+
display: flex;
|
|
694
|
+
align-items: center;
|
|
695
|
+
gap: 14px;
|
|
696
|
+
padding: 14px 16px;
|
|
697
|
+
border: 1px solid var(--color-border);
|
|
698
|
+
border-radius: var(--border-radius-large, 8px);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.cn-transition-input-fields__card-icon {
|
|
702
|
+
color: var(--color-primary-element);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
.cn-transition-input-fields__card-text {
|
|
706
|
+
display: flex;
|
|
707
|
+
flex: 1 1 auto;
|
|
708
|
+
flex-direction: column;
|
|
709
|
+
gap: 2px;
|
|
710
|
+
min-width: 0;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
.cn-transition-input-fields__badge {
|
|
714
|
+
padding: 3px 10px;
|
|
715
|
+
border-radius: 12px;
|
|
716
|
+
background: var(--color-success-light, var(--color-background-dark));
|
|
717
|
+
color: var(--color-success-text);
|
|
718
|
+
font-size: 13px;
|
|
719
|
+
font-weight: 600;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
.cn-transition-input-fields__section-title {
|
|
723
|
+
margin: 0 0 8px;
|
|
724
|
+
font-size: 14px;
|
|
725
|
+
font-weight: 600;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
.cn-transition-input-fields__facts {
|
|
729
|
+
display: grid;
|
|
730
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
731
|
+
gap: 0 24px;
|
|
732
|
+
margin: 0;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
.cn-transition-input-fields__facts > div {
|
|
736
|
+
padding: 10px 0;
|
|
737
|
+
border-bottom: 1px solid var(--color-border);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
.cn-transition-input-fields__facts dt {
|
|
741
|
+
font-size: 13px;
|
|
742
|
+
color: var(--color-text-maxcontrast);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
.cn-transition-input-fields__facts dd {
|
|
746
|
+
margin: 2px 0 0;
|
|
747
|
+
font-size: 15px;
|
|
748
|
+
font-weight: 600;
|
|
749
|
+
}
|
|
750
|
+
</style>
|