@conduction/nextcloud-vue 2.119.0 → 2.120.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 (126) hide show
  1. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js +9 -0
  2. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue.js.map +1 -0
  3. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js +226 -0
  4. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js +211 -0
  6. package/dist/esm/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js +9 -0
  8. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue.js.map +1 -0
  9. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js +64 -0
  10. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue2.js.map +1 -0
  11. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js +89 -0
  12. package/dist/esm/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue3.js.map +1 -0
  13. package/dist/esm/components/CnIcon/semanticIcons.js +2 -2
  14. package/dist/esm/components/CnIcon/semanticIcons.js.map +1 -1
  15. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js +9 -0
  16. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js.map +1 -0
  17. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js +257 -0
  18. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js.map +1 -0
  19. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js +266 -0
  20. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js.map +1 -0
  21. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue.js +1 -1
  22. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js +14 -7
  23. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js.map +1 -1
  24. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue3.js.map +1 -1
  25. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js +9 -0
  26. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js.map +1 -0
  27. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js +278 -0
  28. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js.map +1 -0
  29. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js +128 -0
  30. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js.map +1 -0
  31. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js +10 -0
  32. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js.map +1 -0
  33. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js +944 -0
  34. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js.map +1 -0
  35. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js +276 -0
  36. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js.map +1 -0
  37. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js +9 -0
  38. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js.map +1 -0
  39. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js +354 -0
  40. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js.map +1 -0
  41. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js +392 -0
  42. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js.map +1 -0
  43. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js +9 -0
  44. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js.map +1 -0
  45. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js +180 -0
  46. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js.map +1 -0
  47. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js +208 -0
  48. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js.map +1 -0
  49. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js +9 -0
  50. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js.map +1 -0
  51. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js +244 -0
  52. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js.map +1 -0
  53. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js +76 -0
  54. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js.map +1 -0
  55. package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js +290 -0
  56. package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js.map +1 -0
  57. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +9 -0
  58. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js.map +1 -0
  59. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +448 -0
  60. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -0
  61. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +303 -0
  62. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -0
  63. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +1 -1
  64. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +19 -2
  65. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
  66. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +73 -62
  67. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
  68. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +6 -0
  69. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  70. package/dist/esm/index.js +5 -0
  71. package/dist/esm/index.js.map +1 -1
  72. package/dist/esm/l10n/en.json.js +166 -1
  73. package/dist/esm/l10n/en.json.js.map +1 -1
  74. package/dist/esm/l10n/nl.json.js +166 -1
  75. package/dist/esm/l10n/nl.json.js.map +1 -1
  76. package/dist/esm/library-style.js +1 -1
  77. package/dist/esm/nextcloud-vue.css +889 -101
  78. package/dist/esm/utils/aiFeatures.js +245 -0
  79. package/dist/esm/utils/aiFeatures.js.map +1 -0
  80. package/dist/esm/utils/aiSuggestionTracker.js +182 -0
  81. package/dist/esm/utils/aiSuggestionTracker.js.map +1 -0
  82. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  83. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  84. package/dist/esm/utils/objectPermissions.js +552 -0
  85. package/dist/esm/utils/objectPermissions.js.map +1 -0
  86. package/dist/esm/utils/objectTranslations.js +180 -0
  87. package/dist/esm/utils/objectTranslations.js.map +1 -0
  88. package/dist/esm/utils/stateMachineValidation.js +254 -0
  89. package/dist/esm/utils/stateMachineValidation.js.map +1 -0
  90. package/dist/esm/utils/widgetDispatch.js +3 -1
  91. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  92. package/dist/nextcloud-vue.cjs.js +20052 -13138
  93. package/dist/nextcloud-vue.cjs.js.map +2 -2
  94. package/dist/nextcloud-vue.css +889 -101
  95. package/l10n/en.json +166 -1
  96. package/l10n/nl.json +166 -1
  97. package/package.json +1 -1
  98. package/src/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue +416 -0
  99. package/src/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue +136 -0
  100. package/src/components/CnAiFeaturesWidget/__tests__/CnAiFeaturesWidget.spec.js +213 -0
  101. package/src/components/CnAiFeaturesWidget/index.js +14 -0
  102. package/src/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue +499 -0
  103. package/src/components/CnObjectPermissionsTab/__tests__/CnObjectPermissionsTab.spec.js +186 -0
  104. package/src/components/CnObjectPermissionsTab/index.js +13 -0
  105. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +8 -1
  106. package/src/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue +424 -0
  107. package/src/components/CnObjectTranslationsWidget/__tests__/CnObjectTranslationsWidget.spec.js +189 -0
  108. package/src/components/CnObjectTranslationsWidget/index.js +13 -0
  109. package/src/components/CnStateMachineEditor/CnStateMachineEditor.vue +1332 -0
  110. package/src/components/CnStateMachineEditor/CnStateMachineFields.vue +791 -0
  111. package/src/components/CnStateMachineEditor/CnStateMachineNode.vue +442 -0
  112. package/src/components/CnStateMachineEditor/CnStateMachinePanel.vue +326 -0
  113. package/src/components/CnStateMachineEditor/index.js +8 -0
  114. package/src/components/CnStateMachineEditor/stateMachineFields.js +287 -0
  115. package/src/components/CnTimelineWidget/CnTimelineManage.vue +827 -0
  116. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +29 -3
  117. package/src/components/CnWidgetGrid/builtInWidgets.js +6 -0
  118. package/src/components/index.js +4 -0
  119. package/src/index.js +5 -0
  120. package/src/utils/aiFeatures.js +241 -0
  121. package/src/utils/aiSuggestionTracker.js +178 -0
  122. package/src/utils/libraryWidgetKeys.js +1 -0
  123. package/src/utils/objectPermissions.js +548 -0
  124. package/src/utils/objectTranslations.js +176 -0
  125. package/src/utils/stateMachineValidation.js +250 -0
  126. package/src/utils/widgetDispatch.js +3 -1
@@ -0,0 +1,791 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <div class="cn-sm-fields" :class="{ 'cn-sm-fields--row': row }">
7
+ <template v-for="(field, fieldIndex) in fields" :key="field.key || `${field.type}-${fieldIndex}`">
8
+ <!-- One line of fields side by side. -->
9
+ <CnStateMachineFields
10
+ v-if="field.type === 'row'"
11
+ :fields="field.fields"
12
+ :target="target"
13
+ :path="path"
14
+ :disabled="isDisabled(field)"
15
+ row />
16
+
17
+ <!-- A group: collapsible, switchable, or a nested object. -->
18
+ <div v-else-if="field.type === 'group'" class="cn-sm-group" :class="{ 'cn-sm-group--section': field.section }">
19
+ <button
20
+ v-if="field.collapsible"
21
+ type="button"
22
+ class="cn-sm-group__toggle"
23
+ :aria-expanded="isOpen(field) ? 'true' : 'false'"
24
+ :aria-controls="idFor(field, 'body')"
25
+ @click="toggleOpen(field)">
26
+ <span>{{ panel.tr(field.label) }}</span>
27
+ <ChevronDown v-if="isOpen(field)" :size="18" />
28
+ <ChevronRight v-else :size="18" />
29
+ </button>
30
+ <h4 v-else-if="field.label && !field.optional" class="cn-sm-group__heading">
31
+ {{ panel.tr(field.label) }}
32
+ </h4>
33
+
34
+ <p v-if="field.lockable && panel.locked && field.lockedNotice" class="cn-sm-group__notice" role="note">
35
+ {{ panel.tr(field.lockedNotice) }}
36
+ </p>
37
+
38
+ <div v-if="!field.collapsible || isOpen(field)" :id="idFor(field, 'body')" class="cn-sm-group__body">
39
+ <div v-if="field.optional" class="cn-sm-field cn-sm-field--check">
40
+ <input
41
+ :id="idFor(field, 'enabled')"
42
+ type="checkbox"
43
+ :checked="panel.isEnabled(childPath(field))"
44
+ :disabled="isDisabled(field)"
45
+ @change="panel.setEnabled(childPath(field), $event.target.checked)">
46
+ <label :for="idFor(field, 'enabled')">{{ panel.tr(field.toggleLabel || field.label) }}</label>
47
+ </div>
48
+ <CnStateMachineFields
49
+ v-if="!field.optional || panel.isEnabled(childPath(field))"
50
+ :class="{ 'cn-sm-fields--boxed': field.optional }"
51
+ :fields="field.fields"
52
+ :target="field.key ? target[field.key] : target"
53
+ :path="childPath(field)"
54
+ :disabled="isDisabled(field)" />
55
+ </div>
56
+ </div>
57
+
58
+ <!-- A list of strings or of objects with one edited text. -->
59
+ <fieldset v-else-if="field.type === 'list'" class="cn-sm-list" :class="{ 'cn-sm-list--section': field.section }">
60
+ <legend class="cn-sm-field__label" :class="{ 'cn-sm-list__legend--section': field.section }">
61
+ {{ panel.tr(field.label) }}
62
+ </legend>
63
+ <div
64
+ v-for="(item, index) in target[field.key]"
65
+ :key="itemKey(field, item, index)"
66
+ class="cn-sm-list__item"
67
+ :draggable="field.reorderable && !isDisabled(field) ? 'true' : 'false'"
68
+ @dragstart="onItemDragStart(field, index, $event)"
69
+ @dragover.prevent
70
+ @drop.stop.prevent="onItemDrop(field, index)">
71
+ <span v-if="field.reorderable" class="cn-sm-list__handle" aria-hidden="true">
72
+ <DragVertical :size="16" />
73
+ </span>
74
+ <input
75
+ type="text"
76
+ class="cn-sm-field__input"
77
+ :value="itemText(field, item)"
78
+ :placeholder="field.itemLabel ? panel.tr(field.itemLabel) : ''"
79
+ :aria-label="itemAriaLabel(field, index)"
80
+ :disabled="isDisabled(field)"
81
+ @input="setItemText(field, index, $event.target.value)">
82
+ <template v-if="field.reorderable">
83
+ <NcButton
84
+ variant="tertiary"
85
+ :disabled="isDisabled(field) || index === 0"
86
+ :aria-label="moveLabel(field, index, 'up')"
87
+ @click="move(field, index, index - 1)">
88
+ <template #icon>
89
+ <ArrowUp :size="16" />
90
+ </template>
91
+ </NcButton>
92
+ <NcButton
93
+ variant="tertiary"
94
+ :disabled="isDisabled(field) || index === target[field.key].length - 1"
95
+ :aria-label="moveLabel(field, index, 'down')"
96
+ @click="move(field, index, index + 1)">
97
+ <template #icon>
98
+ <ArrowDown :size="16" />
99
+ </template>
100
+ </NcButton>
101
+ </template>
102
+ <NcButton
103
+ variant="tertiary"
104
+ :disabled="isDisabled(field)"
105
+ :aria-label="removeLabel(field, index)"
106
+ @click="removeItem(field, index)">
107
+ <template #icon>
108
+ <Close :size="16" />
109
+ </template>
110
+ </NcButton>
111
+ </div>
112
+ <NcButton variant="secondary" :disabled="isDisabled(field)" @click="addItem(field)">
113
+ {{ panel.tr(field.addLabel) || t('nextcloud-vue', 'Add') }}
114
+ </NcButton>
115
+ </fieldset>
116
+
117
+ <!-- A list of objects whose fields depend on their type. -->
118
+ <fieldset v-else-if="field.type === 'typed-list'" class="cn-sm-list" :class="{ 'cn-sm-list--section': field.section }">
119
+ <legend class="cn-sm-field__label" :class="{ 'cn-sm-list__legend--section': field.section }">
120
+ {{ panel.tr(field.label) }}
121
+ </legend>
122
+ <div
123
+ v-for="(item, index) in target[field.key]"
124
+ :key="itemKey(field, item, index)"
125
+ class="cn-sm-list__typed">
126
+ <div class="cn-sm-field">
127
+ <label :for="idFor(field, `type-${index}`)" class="cn-sm-field__label">
128
+ {{ field.typeLabel ? panel.tr(field.typeLabel) : t('nextcloud-vue', 'Type') }}
129
+ </label>
130
+ <select
131
+ :id="idFor(field, `type-${index}`)"
132
+ class="cn-sm-field__input"
133
+ :value="item[typeKey(field)]"
134
+ :disabled="isDisabled(field)"
135
+ @change="panel.set(item, typeKey(field), $event.target.value)">
136
+ <option
137
+ v-for="type in typesFor(field, item)"
138
+ :key="type.value"
139
+ :value="type.value">
140
+ {{ panel.tr(type.label) }}
141
+ </option>
142
+ </select>
143
+ </div>
144
+ <CnStateMachineFields
145
+ :fields="fieldsOfType(field, item)"
146
+ :target="item"
147
+ :path="`${childPath(field)}.${index}`"
148
+ :disabled="isDisabled(field)" />
149
+ <NcButton
150
+ variant="tertiary"
151
+ class="cn-sm-list__remove"
152
+ :disabled="isDisabled(field)"
153
+ :aria-label="panel.tr(field.removeLabel) || t('nextcloud-vue', 'Remove')"
154
+ @click="removeItem(field, index)">
155
+ <template #icon>
156
+ <Close :size="16" />
157
+ </template>
158
+ </NcButton>
159
+ </div>
160
+ <NcButton variant="secondary" :disabled="isDisabled(field)" @click="addItem(field)">
161
+ {{ panel.tr(field.addLabel) || t('nextcloud-vue', 'Add') }}
162
+ </NcButton>
163
+ </fieldset>
164
+
165
+ <!-- Several values picked from a set of options. -->
166
+ <fieldset v-else-if="field.type === 'checkboxes'" class="cn-sm-list">
167
+ <legend class="cn-sm-field__label">
168
+ {{ panel.tr(field.label) }}
169
+ </legend>
170
+ <div v-for="(option, index) in panel.optionsOf(field)" :key="String(option.value)" class="cn-sm-field cn-sm-field--check">
171
+ <input
172
+ :id="idFor(field, `option-${index}`)"
173
+ type="checkbox"
174
+ :checked="(target[field.key] || []).includes(option.value)"
175
+ :disabled="isDisabled(field)"
176
+ @change="toggleOption(field, option.value)">
177
+ <label :for="idFor(field, `option-${index}`)">{{ option.label }}</label>
178
+ </div>
179
+ </fieldset>
180
+
181
+ <!-- A single check box. -->
182
+ <div v-else-if="field.type === 'checkbox'" class="cn-sm-field cn-sm-field--check">
183
+ <input
184
+ :id="idFor(field)"
185
+ type="checkbox"
186
+ :checked="!!target[field.key]"
187
+ :disabled="isDisabled(field)"
188
+ @change="panel.set(target, field.key, $event.target.checked)">
189
+ <label :for="idFor(field)">{{ panel.tr(field.label) }}</label>
190
+ </div>
191
+
192
+ <!-- One value from a set of options. -->
193
+ <div v-else-if="field.type === 'select'" class="cn-sm-field" :class="widthClass(field)">
194
+ <label :for="idFor(field)" class="cn-sm-field__label" :class="{ 'cn-sm-visually-hidden': field.hideLabel }">
195
+ {{ panel.tr(field.label) }}
196
+ </label>
197
+ <select
198
+ :id="idFor(field)"
199
+ class="cn-sm-field__input"
200
+ :disabled="isDisabled(field)"
201
+ @change="onSelect(field, $event.target.value)">
202
+ <option
203
+ v-for="(option, index) in selectOptions(field)"
204
+ :key="index"
205
+ :value="String(index)"
206
+ :selected="sameValue(option.value, target[field.key])">
207
+ {{ option.label }}
208
+ </option>
209
+ </select>
210
+ </div>
211
+
212
+ <!-- Free text, a longer text, or a number. -->
213
+ <div v-else class="cn-sm-field" :class="widthClass(field)">
214
+ <label :for="idFor(field)" class="cn-sm-field__label" :class="{ 'cn-sm-visually-hidden': field.hideLabel }">
215
+ {{ panel.tr(field.label) }}
216
+ </label>
217
+ <textarea
218
+ v-if="field.type === 'textarea'"
219
+ :id="idFor(field)"
220
+ class="cn-sm-field__input"
221
+ :rows="field.rows || 3"
222
+ :value="target[field.key] ?? ''"
223
+ :placeholder="field.placeholder ? panel.tr(field.placeholder) : ''"
224
+ :aria-describedby="field.hint ? idFor(field, 'hint') : undefined"
225
+ :disabled="isDisabled(field)"
226
+ @input="panel.set(target, field.key, $event.target.value)" />
227
+ <input
228
+ v-else
229
+ :id="idFor(field)"
230
+ class="cn-sm-field__input"
231
+ :type="field.type === 'number' ? 'number' : (field.inputType || 'text')"
232
+ :min="field.min"
233
+ :max="field.max"
234
+ :value="target[field.key] ?? ''"
235
+ :placeholder="field.placeholder ? panel.tr(field.placeholder) : ''"
236
+ :aria-describedby="field.hint ? idFor(field, 'hint') : undefined"
237
+ :disabled="isDisabled(field)"
238
+ @input="onInput(field, $event.target.value)">
239
+ <p v-if="field.hint" :id="idFor(field, 'hint')" class="cn-sm-field__hint">
240
+ {{ panel.tr(field.hint) }}
241
+ </p>
242
+ </div>
243
+ </template>
244
+ </div>
245
+ </template>
246
+
247
+ <script>
248
+ import { translate as t } from '@nextcloud/l10n'
249
+ import { NcButton } from '@nextcloud/vue'
250
+ import ArrowDown from 'vue-material-design-icons/ArrowDown.vue'
251
+ import ArrowUp from 'vue-material-design-icons/ArrowUp.vue'
252
+ import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
253
+ import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
254
+ import Close from 'vue-material-design-icons/Close.vue'
255
+ import DragVertical from 'vue-material-design-icons/DragVertical.vue'
256
+ import { newListItem } from './stateMachineFields.js'
257
+
258
+ /**
259
+ * Renders field descriptors against one object of a property panel's copy.
260
+ *
261
+ * Every write goes through the panel's injected `set`, so the panel owns the
262
+ * copy and hears each change. Recursive for groups, rows and typed lists.
263
+ */
264
+ export default {
265
+ name: 'CnStateMachineFields',
266
+
267
+ components: { NcButton, ArrowDown, ArrowUp, ChevronDown, ChevronRight, Close, DragVertical },
268
+
269
+ inject: ['cnStateMachinePanel'],
270
+
271
+ props: {
272
+ /** The field descriptors to render. */
273
+ fields: {
274
+ type: Array,
275
+ default: () => [],
276
+ },
277
+
278
+ /** The object of the panel's copy these fields edit. */
279
+ target: {
280
+ type: Object,
281
+ required: true,
282
+ },
283
+
284
+ /** Where `target` sits in the copy, for ids and optional-group state. */
285
+ path: {
286
+ type: String,
287
+ default: '',
288
+ },
289
+
290
+ /** Disable every control. */
291
+ disabled: {
292
+ type: Boolean,
293
+ default: false,
294
+ },
295
+
296
+ /** Lay the fields out on one line. */
297
+ row: {
298
+ type: Boolean,
299
+ default: false,
300
+ },
301
+ },
302
+
303
+ data() {
304
+ return {
305
+ open: {},
306
+ dragIndex: null,
307
+ dragKey: null,
308
+ }
309
+ },
310
+
311
+ computed: {
312
+ /**
313
+ * @return {object} The owning panel's API.
314
+ */
315
+ panel() {
316
+ return this.cnStateMachinePanel
317
+ },
318
+ },
319
+
320
+ methods: {
321
+ t,
322
+
323
+ /**
324
+ * @param {object} field A descriptor.
325
+ * @return {boolean} Whether its controls are disabled.
326
+ */
327
+ isDisabled(field) {
328
+ return this.disabled || (field.lockable === true && this.panel.locked === true)
329
+ },
330
+
331
+ /**
332
+ * @param {object} field A descriptor.
333
+ * @return {string} The path of the object a group or list edits.
334
+ */
335
+ childPath(field) {
336
+ if (!field.key) {
337
+ return this.path
338
+ }
339
+ return this.path ? `${this.path}.${field.key}` : field.key
340
+ },
341
+
342
+ /**
343
+ * @param {object} field A descriptor.
344
+ * @param {string} [suffix] A part name.
345
+ * @return {string} A DOM id unique within the editor.
346
+ */
347
+ idFor(field, suffix = '') {
348
+ const raw = [this.panel.uid, this.childPath(field) || field.type, suffix].filter(Boolean).join('-')
349
+ return raw.replace(/[^A-Za-z0-9_-]/g, '-')
350
+ },
351
+
352
+ /**
353
+ * @param {object} field A descriptor.
354
+ * @return {string|null} The width modifier class.
355
+ */
356
+ widthClass(field) {
357
+ return field.width === 'narrow' ? 'cn-sm-field--narrow' : null
358
+ },
359
+
360
+ /**
361
+ * @param {object} field A collapsible group.
362
+ * @return {boolean} Whether it is open.
363
+ */
364
+ isOpen(field) {
365
+ const key = this.childPath(field) || field.label
366
+ return this.open[key] ?? field.open === true
367
+ },
368
+
369
+ /**
370
+ * @param {object} field A collapsible group.
371
+ * @return {void}
372
+ */
373
+ toggleOpen(field) {
374
+ const key = this.childPath(field) || field.label
375
+ this.open = { ...this.open, [key]: !this.isOpen(field) }
376
+ },
377
+
378
+ /**
379
+ * @param {object} field A text or number descriptor.
380
+ * @param {string} raw The input's value.
381
+ * @return {void}
382
+ */
383
+ onInput(field, raw) {
384
+ if (field.type === 'number') {
385
+ this.panel.set(this.target, field.key, raw === '' ? null : Number(raw))
386
+ return
387
+ }
388
+ this.panel.set(this.target, field.key, raw)
389
+ },
390
+
391
+ /**
392
+ * @param {object} field A select descriptor.
393
+ * @return {Array<object>} Its options, the empty option first.
394
+ */
395
+ selectOptions(field) {
396
+ const options = this.panel.optionsOf(field)
397
+ if (field.emptyOption) {
398
+ return [{ value: field.emptyOption.value ?? null, label: this.panel.tr(field.emptyOption.label || '') }, ...options]
399
+ }
400
+ return options
401
+ },
402
+
403
+ /**
404
+ * @param {unknown} a One value.
405
+ * @param {unknown} b Another.
406
+ * @return {boolean} Whether they are the same option, treating null and undefined alike.
407
+ */
408
+ sameValue(a, b) {
409
+ return (a ?? null) === (b ?? null)
410
+ },
411
+
412
+ /**
413
+ * @param {object} field A select descriptor.
414
+ * @param {string} index The chosen option's index.
415
+ * @return {void}
416
+ */
417
+ onSelect(field, index) {
418
+ const option = this.selectOptions(field)[Number(index)]
419
+ this.panel.set(this.target, field.key, option ? option.value : null)
420
+ },
421
+
422
+ /**
423
+ * @param {object} field A checkboxes descriptor.
424
+ * @param {unknown} value The option toggled.
425
+ * @return {void}
426
+ */
427
+ toggleOption(field, value) {
428
+ const current = [...(this.target[field.key] || [])]
429
+ const at = current.indexOf(value)
430
+ if (at >= 0) {
431
+ current.splice(at, 1)
432
+ } else {
433
+ current.push(value)
434
+ }
435
+ this.panel.set(this.target, field.key, current)
436
+ },
437
+
438
+ /**
439
+ * @param {object} field A list descriptor.
440
+ * @param {unknown} item An entry.
441
+ * @param {number} index Its index.
442
+ * @return {string} A stable key for the entry.
443
+ */
444
+ itemKey(field, item, index) {
445
+ if (field.idKey && item && typeof item === 'object' && item[field.idKey] !== undefined) {
446
+ return String(item[field.idKey])
447
+ }
448
+ return `${field.key}-${index}`
449
+ },
450
+
451
+ /**
452
+ * @param {object} field A list descriptor.
453
+ * @param {unknown} item An entry.
454
+ * @return {string} The text the entry's input shows.
455
+ */
456
+ itemText(field, item) {
457
+ if (field.itemType === 'object') {
458
+ return (item && item[field.itemKey || 'label']) ?? ''
459
+ }
460
+ return item ?? ''
461
+ },
462
+
463
+ /**
464
+ * @param {object} field A list descriptor.
465
+ * @param {number} index The entry.
466
+ * @param {string} value The new text.
467
+ * @return {void}
468
+ */
469
+ setItemText(field, index, value) {
470
+ const list = this.target[field.key]
471
+ if (field.itemType === 'object') {
472
+ this.panel.set(list[index], field.itemKey || 'label', value)
473
+ return
474
+ }
475
+ this.panel.set(list, index, value)
476
+ },
477
+
478
+ /**
479
+ * @param {object} field A list descriptor.
480
+ * @param {number} index The entry.
481
+ * @return {string} The entry input's accessible name.
482
+ */
483
+ itemAriaLabel(field, index) {
484
+ const label = this.panel.tr(field.itemLabel || field.label)
485
+ return this.target[field.key].length > 1 ? `${label} ${index + 1}` : label
486
+ },
487
+
488
+ /**
489
+ * @param {object} field A list descriptor.
490
+ * @param {number} index The entry.
491
+ * @return {string} The remove button's accessible name.
492
+ */
493
+ removeLabel(field, index) {
494
+ const label = this.panel.tr(field.removeLabel) || t('nextcloud-vue', 'Remove')
495
+ return this.target[field.key].length > 1 ? `${label} ${index + 1}` : label
496
+ },
497
+
498
+ /**
499
+ * @param {object} field A list descriptor.
500
+ * @param {number} index The entry.
501
+ * @param {string} direction `up` or `down`.
502
+ * @return {string} The move button's accessible name.
503
+ */
504
+ moveLabel(field, index, direction) {
505
+ const label = direction === 'up' ? t('nextcloud-vue', 'Move up') : t('nextcloud-vue', 'Move down')
506
+ return `${label}: ${this.itemText(field, this.target[field.key][index]) || index + 1}`
507
+ },
508
+
509
+ /**
510
+ * @param {object} field A list descriptor.
511
+ * @return {void}
512
+ */
513
+ addItem(field) {
514
+ this.panel.push(this.target[field.key], newListItem(field))
515
+ },
516
+
517
+ /**
518
+ * @param {object} field A list descriptor.
519
+ * @param {number} index The entry.
520
+ * @return {void}
521
+ */
522
+ removeItem(field, index) {
523
+ this.panel.splice(this.target[field.key], index)
524
+ },
525
+
526
+ /**
527
+ * @param {object} field A list descriptor.
528
+ * @param {number} from The entry's index.
529
+ * @param {number} to Its new index.
530
+ * @return {void}
531
+ */
532
+ move(field, from, to) {
533
+ this.panel.move(this.target[field.key], from, to)
534
+ },
535
+
536
+ /**
537
+ * @param {object} field A list descriptor.
538
+ * @param {number} index The dragged entry.
539
+ * @param {DragEvent} event The drag event.
540
+ * @return {void}
541
+ */
542
+ onItemDragStart(field, index, event) {
543
+ if (!field.reorderable || this.isDisabled(field)) {
544
+ return
545
+ }
546
+ this.dragIndex = index
547
+ this.dragKey = field.key
548
+ if (event.dataTransfer) {
549
+ event.dataTransfer.effectAllowed = 'move'
550
+ event.dataTransfer.setData('text/plain', String(index))
551
+ }
552
+ },
553
+
554
+ /**
555
+ * @param {object} field A list descriptor.
556
+ * @param {number} index The entry dropped on.
557
+ * @return {void}
558
+ */
559
+ onItemDrop(field, index) {
560
+ if (this.dragKey === field.key && this.dragIndex !== null) {
561
+ this.move(field, this.dragIndex, index)
562
+ }
563
+ this.dragIndex = null
564
+ this.dragKey = null
565
+ },
566
+
567
+ /**
568
+ * @param {object} field A typed-list descriptor.
569
+ * @return {string} The key holding an entry's type.
570
+ */
571
+ typeKey(field) {
572
+ return field.typeKey || 'type'
573
+ },
574
+
575
+ /**
576
+ * The types an entry can be set to: the offered ones, plus its own when
577
+ * that is one no longer offered, so a stored entry still shows what it is.
578
+ *
579
+ * @param {object} field A typed-list descriptor.
580
+ * @param {object} item The entry.
581
+ * @return {Array<object>} The types.
582
+ */
583
+ typesFor(field, item) {
584
+ const own = item[this.typeKey(field)]
585
+ return (field.types || []).filter((type) => !type.hidden || type.value === own)
586
+ },
587
+
588
+ /**
589
+ * @param {object} field A typed-list descriptor.
590
+ * @param {object} item The entry.
591
+ * @return {Array<object>} The fields of the entry's type.
592
+ */
593
+ fieldsOfType(field, item) {
594
+ const type = (field.types || []).find((candidate) => candidate.value === item[this.typeKey(field)])
595
+ return type && Array.isArray(type.fields) ? type.fields : []
596
+ },
597
+ },
598
+ }
599
+ </script>
600
+
601
+ <style scoped>
602
+ .cn-sm-fields {
603
+ display: flex;
604
+ flex-direction: column;
605
+ gap: 12px;
606
+ min-width: 0;
607
+ }
608
+
609
+ .cn-sm-fields--row {
610
+ flex-direction: row;
611
+ align-items: flex-end;
612
+ gap: 8px;
613
+ }
614
+
615
+ .cn-sm-fields--row > * {
616
+ flex: 1 1 0;
617
+ min-width: 0;
618
+ }
619
+
620
+ .cn-sm-fields--row > .cn-sm-field--narrow {
621
+ flex: 0 0 88px;
622
+ }
623
+
624
+ .cn-sm-fields--boxed {
625
+ padding: 8px;
626
+ border-radius: var(--border-radius, 8px);
627
+ background: var(--color-background-dark, #ededed);
628
+ }
629
+
630
+ .cn-sm-field {
631
+ display: flex;
632
+ flex-direction: column;
633
+ gap: 4px;
634
+ min-width: 0;
635
+ }
636
+
637
+ .cn-sm-field--check {
638
+ flex-direction: row;
639
+ align-items: center;
640
+ gap: 8px;
641
+ }
642
+
643
+ .cn-sm-field__label,
644
+ .cn-sm-field > label {
645
+ font-size: 14px;
646
+ font-weight: 600;
647
+ color: var(--color-main-text, #222);
648
+ }
649
+
650
+ .cn-sm-field--check > label {
651
+ font-weight: 400;
652
+ }
653
+
654
+ .cn-sm-field__input {
655
+ width: 100%;
656
+ min-height: 36px;
657
+ box-sizing: border-box;
658
+ padding: 6px 8px;
659
+ border: 1px solid var(--color-border-maxcontrast, #7f7f7f);
660
+ border-radius: var(--border-radius-element, 8px);
661
+ background: var(--color-main-background, #fff);
662
+ color: var(--color-main-text, #222);
663
+ font: inherit;
664
+ }
665
+
666
+ textarea.cn-sm-field__input {
667
+ resize: vertical;
668
+ }
669
+
670
+ .cn-sm-field__input:focus-visible {
671
+ outline: 2px solid var(--color-primary-element, #0082c9);
672
+ outline-offset: 1px;
673
+ }
674
+
675
+ .cn-sm-field__hint {
676
+ margin: 0;
677
+ font-size: 13px;
678
+ color: var(--color-text-maxcontrast, #6b6b6b);
679
+ }
680
+
681
+ .cn-sm-list {
682
+ display: flex;
683
+ flex-direction: column;
684
+ gap: 6px;
685
+ min-width: 0;
686
+ margin: 0;
687
+ padding: 0;
688
+ border: 0;
689
+ }
690
+
691
+ .cn-sm-list--section,
692
+ .cn-sm-group--section {
693
+ padding-top: 12px;
694
+ border-top: 1px solid var(--color-border, #d0d0d0);
695
+ }
696
+
697
+ .cn-sm-list legend {
698
+ padding: 0;
699
+ margin-bottom: 6px;
700
+ }
701
+
702
+ .cn-sm-list__legend--section,
703
+ .cn-sm-group__heading {
704
+ margin: 0 0 6px;
705
+ font-size: 12px;
706
+ font-weight: 600;
707
+ letter-spacing: 0.04em;
708
+ text-transform: uppercase;
709
+ color: var(--color-text-maxcontrast, #6b6b6b);
710
+ }
711
+
712
+ .cn-sm-list__item {
713
+ display: flex;
714
+ align-items: center;
715
+ gap: 4px;
716
+ }
717
+
718
+ .cn-sm-list__handle {
719
+ display: inline-flex;
720
+ cursor: grab;
721
+ color: var(--color-text-maxcontrast, #6b6b6b);
722
+ }
723
+
724
+ .cn-sm-list__typed {
725
+ position: relative;
726
+ display: flex;
727
+ flex-direction: column;
728
+ gap: 6px;
729
+ padding: 8px 44px 8px 8px;
730
+ border-radius: var(--border-radius, 8px);
731
+ background: var(--color-background-dark, #ededed);
732
+ }
733
+
734
+ .cn-sm-list__remove {
735
+ position: absolute;
736
+ top: 4px;
737
+ inset-inline-end: 4px;
738
+ }
739
+
740
+ .cn-sm-group {
741
+ display: flex;
742
+ flex-direction: column;
743
+ gap: 8px;
744
+ }
745
+
746
+ .cn-sm-group__toggle {
747
+ display: flex;
748
+ align-items: center;
749
+ justify-content: space-between;
750
+ width: 100%;
751
+ min-height: 34px;
752
+ padding: 4px 0;
753
+ border: 0;
754
+ background: none;
755
+ color: var(--color-text-maxcontrast, #6b6b6b);
756
+ font-size: 12px;
757
+ font-weight: 600;
758
+ letter-spacing: 0.04em;
759
+ text-transform: uppercase;
760
+ cursor: pointer;
761
+ }
762
+
763
+ .cn-sm-group__toggle:focus-visible {
764
+ outline: 2px solid var(--color-primary-element, #0082c9);
765
+ outline-offset: 2px;
766
+ }
767
+
768
+ .cn-sm-group__notice {
769
+ margin: 0;
770
+ padding: 8px;
771
+ border-inline-start: 4px solid var(--color-warning, #c28900);
772
+ border-radius: var(--border-radius, 8px);
773
+ background: var(--color-background-dark, #ededed);
774
+ font-size: 13px;
775
+ }
776
+
777
+ .cn-sm-group__body {
778
+ display: flex;
779
+ flex-direction: column;
780
+ gap: 8px;
781
+ }
782
+
783
+ .cn-sm-visually-hidden {
784
+ position: absolute;
785
+ width: 1px;
786
+ height: 1px;
787
+ overflow: hidden;
788
+ clip-path: inset(50%);
789
+ white-space: nowrap;
790
+ }
791
+ </style>