@conduction/nextcloud-vue 2.120.0 → 2.121.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 (72) hide show
  1. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +53 -1
  2. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  3. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +546 -523
  4. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  5. package/dist/esm/components/CnIndexPage/useNamedSource.js +9 -0
  6. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  7. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js +9 -0
  8. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js.map +1 -0
  9. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js +214 -0
  10. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js.map +1 -0
  11. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js +125 -0
  12. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js.map +1 -0
  13. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js +9 -0
  14. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js.map +1 -0
  15. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js +163 -0
  16. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js.map +1 -0
  17. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js +131 -0
  18. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js.map +1 -0
  19. package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js +127 -0
  20. package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js.map +1 -0
  21. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js +9 -0
  22. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js.map +1 -0
  23. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js +82 -0
  24. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js.map +1 -0
  25. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js +61 -0
  26. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js.map +1 -0
  27. package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js +60 -0
  28. package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js.map +1 -0
  29. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js +10 -0
  30. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js.map +1 -0
  31. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js +226 -0
  32. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js.map +1 -0
  33. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js +126 -0
  34. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js.map +1 -0
  35. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +42 -32
  36. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  37. package/dist/esm/composables/indexSources.js +116 -16
  38. package/dist/esm/composables/indexSources.js.map +1 -1
  39. package/dist/esm/index.js +4 -0
  40. package/dist/esm/index.js.map +1 -1
  41. package/dist/esm/l10n/en.json.js +42 -1
  42. package/dist/esm/l10n/en.json.js.map +1 -1
  43. package/dist/esm/l10n/nl.json.js +42 -1
  44. package/dist/esm/l10n/nl.json.js.map +1 -1
  45. package/dist/esm/library-style.js +1 -1
  46. package/dist/esm/nextcloud-vue.css +147 -0
  47. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  48. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  49. package/dist/nextcloud-vue.cjs.js +14135 -12578
  50. package/dist/nextcloud-vue.cjs.js.map +2 -2
  51. package/dist/nextcloud-vue.css +147 -0
  52. package/l10n/en.json +42 -1
  53. package/l10n/nl.json +42 -1
  54. package/package.json +1 -1
  55. package/src/components/CnIndexPage/CnIndexPage.vue +74 -3
  56. package/src/components/CnIndexPage/useNamedSource.js +9 -0
  57. package/src/components/CnReadinessChecklist/CnReadinessChecklist.vue +335 -0
  58. package/src/components/CnReadinessChecklist/index.js +9 -0
  59. package/src/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue +300 -0
  60. package/src/components/CnSchemaRulesWidget/index.js +10 -0
  61. package/src/components/CnSchemaRulesWidget/schemaRules.js +123 -0
  62. package/src/components/CnSearchIndexStatus/CnSearchIndexStatus.vue +160 -0
  63. package/src/components/CnSearchIndexStatus/index.js +10 -0
  64. package/src/components/CnSearchIndexStatus/searchIndexStatus.js +56 -0
  65. package/src/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue +328 -0
  66. package/src/components/CnShippedConfigurationSection/index.js +9 -0
  67. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +10 -0
  68. package/src/components/index.js +4 -0
  69. package/src/composables/indexSources.js +116 -16
  70. package/src/css/index-page.css +7 -0
  71. package/src/index.js +4 -0
  72. package/src/utils/libraryWidgetKeys.js +1 -0
@@ -0,0 +1,335 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <div class="cn-readiness-checklist" data-testid="cn-readiness-checklist">
7
+ <p v-if="description" class="cn-readiness-checklist__description">
8
+ {{ description }}
9
+ </p>
10
+
11
+ <NcLoadingIcon v-if="loading" :size="32" :name="t('nextcloud-vue', 'Loading')" />
12
+
13
+ <NcNoteCard v-else-if="loadError" type="error" data-testid="cn-readiness-checklist-error">
14
+ {{ loadError }}
15
+ </NcNoteCard>
16
+
17
+ <template v-else>
18
+ <NcNoteCard v-if="blockingText && blockingItems.length > 0"
19
+ type="error"
20
+ data-testid="cn-readiness-checklist-blocking">
21
+ {{ blockingText }} {{ blockingItems.map((item) => item.label).join(', ') }}
22
+ </NcNoteCard>
23
+
24
+ <p v-if="shownItems.length === 0" class="cn-readiness-checklist__empty" data-testid="cn-readiness-checklist-empty">
25
+ {{ emptyText || t('nextcloud-vue', 'Nothing to check.') }}
26
+ </p>
27
+
28
+ <section v-for="group in shownGroups"
29
+ :key="group.id"
30
+ class="cn-readiness-checklist__group"
31
+ :data-testid="`cn-readiness-checklist-group-${group.id}`">
32
+ <h4 v-if="group.title" class="cn-readiness-checklist__group-title">
33
+ {{ group.title }}
34
+ </h4>
35
+ <ul class="cn-readiness-checklist__list">
36
+ <li v-for="item in group.items"
37
+ :key="item.id"
38
+ class="cn-readiness-checklist__item"
39
+ :data-testid="`cn-readiness-item-${item.id}`">
40
+ <!-- The state is a word, not a colour (WCAG 1.4.1); the
41
+ colour repeats it. -->
42
+ <span class="cn-readiness-checklist__state"
43
+ :class="`cn-readiness-checklist__state--${stateOf(item)}`"
44
+ data-testid="cn-readiness-item-state">
45
+ {{ stateLabel(item) }}
46
+ </span>
47
+ <span class="cn-readiness-checklist__body">
48
+ <a v-if="item.fixHref"
49
+ :href="item.fixHref"
50
+ class="cn-readiness-checklist__label"
51
+ data-testid="cn-readiness-item-fix">{{ item.label }}</a>
52
+ <span v-else class="cn-readiness-checklist__label">{{ item.label }}</span>
53
+ <span v-if="item.explanation" class="cn-readiness-checklist__explanation">{{ item.explanation }}</span>
54
+ <span v-if="item.failure" class="cn-readiness-checklist__failure">
55
+ {{ t('nextcloud-vue', 'This could not be read: {reason}', { reason: item.failure }) }}
56
+ </span>
57
+ </span>
58
+ </li>
59
+ </ul>
60
+ </section>
61
+ </template>
62
+ </div>
63
+ </template>
64
+
65
+ <script>
66
+ import axios from '@nextcloud/axios'
67
+ import { translate as t } from '@nextcloud/l10n'
68
+ import { generateUrl } from '@nextcloud/router'
69
+ import { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
70
+ import { readPath } from '../../utils/readPath.js'
71
+
72
+ /**
73
+ * The fields an item is read from by default.
74
+ *
75
+ * @type {Readonly<Record<string, string>>}
76
+ */
77
+ const DEFAULT_FIELDS = Object.freeze({
78
+ id: 'id',
79
+ label: 'label',
80
+ explanation: 'explanation',
81
+ met: 'met',
82
+ fixHref: 'fixHref',
83
+ failure: 'failure',
84
+ group: 'group',
85
+ blocking: 'blocking',
86
+ })
87
+
88
+ /**
89
+ * CnReadinessChecklist: a list of things an instance needs, each with its
90
+ * state in words (met, open, unknown), an explanation and a link to the
91
+ * screen that fixes it. Reported, never gated: nothing here blocks the app.
92
+ *
93
+ * Items come from `items`, or are read from an app endpoint (`source`, an app
94
+ * path resolved with `generateUrl`, the list at `itemsPath`) and re-read when
95
+ * the reader comes back to the tab. `fields` maps an item's keys onto the
96
+ * row's (`{ label: 'title', met: 'done', fixHref: 'screen' }`). Items carrying
97
+ * a `group` render under that group's heading (`groups`, in order).
98
+ *
99
+ * @spec openspec/changes/readiness-checklist/specs/readiness-checklist/spec.md
100
+ */
101
+ export default {
102
+ name: 'CnReadinessChecklist',
103
+
104
+ components: { NcLoadingIcon, NcNoteCard },
105
+
106
+ inject: {
107
+ /** The host app's translate, for texts the page declares. */
108
+ cnTranslate: { default: () => (key) => key },
109
+ },
110
+
111
+ props: {
112
+ /** The items: `{ id, label, explanation?, met (true|false|null), fixHref?, failure?, group?, blocking? }`. */
113
+ items: { type: Array, default: () => [] },
114
+ /** An app path to read the items from (`/apps/<app>/api/setup/status`). */
115
+ source: { type: String, default: '' },
116
+ /** Where the items sit in the answer. */
117
+ itemsPath: { type: String, default: 'items' },
118
+ /** Item key to row key, over the defaults. */
119
+ fields: { type: Object, default: () => ({}) },
120
+ /** Headings for grouped items, in order: `[{ id, title }]`. */
121
+ groups: { type: Array, default: () => [] },
122
+ /** A line above the list. */
123
+ description: { type: String, default: '' },
124
+ /** The words for each state: `{ met, unmet, unknown }`. */
125
+ statusLabels: { type: Object, default: () => ({}) },
126
+ /** Said when there is nothing to check. */
127
+ emptyText: { type: String, default: '' },
128
+ /** Said when the source could not be read. */
129
+ errorText: { type: String, default: '' },
130
+ /** Said before the unmet items marked `blocking` ("Cannot run until this is installed:"). */
131
+ blockingText: { type: String, default: '' },
132
+ },
133
+
134
+ emits: ['loaded'],
135
+
136
+ data() {
137
+ return {
138
+ loading: false,
139
+ loadError: '',
140
+ loadedItems: null,
141
+ }
142
+ },
143
+
144
+ computed: {
145
+ /** @return {Array<object>} The items, normalised. */
146
+ shownItems() {
147
+ const raw = this.loadedItems !== null ? this.loadedItems : this.items
148
+ const map = { ...DEFAULT_FIELDS, ...(this.fields || {}) }
149
+ return (Array.isArray(raw) ? raw : []).map((row, index) => {
150
+ const read = (key) => readPath(row, map[key])
151
+ const met = read('met')
152
+ return {
153
+ id: String(read('id') ?? index),
154
+ label: this.text(read('label') ?? read('id') ?? ''),
155
+ explanation: this.text(read('explanation') ?? ''),
156
+ met: met === true || met === false ? met : null,
157
+ fixHref: typeof read('fixHref') === 'string' ? read('fixHref') : '',
158
+ failure: read('failure') ? String(read('failure')) : '',
159
+ group: read('group') ? String(read('group')) : '',
160
+ blocking: read('blocking') === true,
161
+ }
162
+ })
163
+ },
164
+
165
+ /** @return {Array<object>} The unmet blocking items. */
166
+ blockingItems() {
167
+ return this.shownItems.filter((item) => item.blocking && item.met !== true)
168
+ },
169
+
170
+ /** @return {Array<{id: string, title: string, items: Array<object>}>} The groups that hold items, in order. */
171
+ shownGroups() {
172
+ const declared = (Array.isArray(this.groups) ? this.groups : []).map((group) => ({
173
+ id: String(group.id),
174
+ title: this.text(group.title || ''),
175
+ items: this.shownItems.filter((item) => item.group === String(group.id)),
176
+ }))
177
+ const known = new Set(declared.map((group) => group.id))
178
+ const rest = this.shownItems.filter((item) => !known.has(item.group))
179
+ const all = rest.length > 0 ? [{ id: 'items', title: '', items: rest }, ...declared] : declared
180
+ return all.filter((group) => group.items.length > 0)
181
+ },
182
+ },
183
+
184
+ watch: {
185
+ source() {
186
+ this.reload()
187
+ },
188
+ },
189
+
190
+ mounted() {
191
+ this.reload()
192
+ window.addEventListener('focus', this.onFocus)
193
+ },
194
+
195
+ beforeUnmount() {
196
+ window.removeEventListener('focus', this.onFocus)
197
+ },
198
+
199
+ methods: {
200
+ t,
201
+
202
+ /**
203
+ * A declared text through the host's translate.
204
+ *
205
+ * @param {string} value The text.
206
+ * @return {string} The translated text.
207
+ */
208
+ text(value) {
209
+ return value ? String(this.cnTranslate(String(value))) : ''
210
+ },
211
+
212
+ /**
213
+ * @param {object} item The item.
214
+ * @return {string} `met`, `unmet` or `unknown`.
215
+ */
216
+ stateOf(item) {
217
+ if (item.met === true) {
218
+ return 'met'
219
+ }
220
+ return item.met === false ? 'unmet' : 'unknown'
221
+ },
222
+
223
+ /**
224
+ * The word for an item's state.
225
+ *
226
+ * @param {object} item The item.
227
+ * @return {string} The word.
228
+ */
229
+ stateLabel(item) {
230
+ const state = this.stateOf(item)
231
+ const own = this.statusLabels && this.statusLabels[state]
232
+ if (own) {
233
+ return this.text(own)
234
+ }
235
+ return {
236
+ met: t('nextcloud-vue', 'Done'),
237
+ unmet: t('nextcloud-vue', 'Still open'),
238
+ unknown: t('nextcloud-vue', 'Unknown'),
239
+ }[state]
240
+ },
241
+
242
+ /**
243
+ * Read the items again, from the source. The source asks the instance
244
+ * rather than a stored flag, so a fix made elsewhere shows on return.
245
+ *
246
+ * @return {Promise<void>}
247
+ */
248
+ async reload() {
249
+ if (!this.source) {
250
+ this.loadedItems = null
251
+ return
252
+ }
253
+ this.loading = this.loadedItems === null
254
+ this.loadError = ''
255
+ try {
256
+ const { data } = await axios.get(generateUrl(this.source))
257
+ const rows = Array.isArray(data) ? data : readPath(data, this.itemsPath)
258
+ this.loadedItems = Array.isArray(rows) ? rows : []
259
+ /** @event loaded The source answered. Payload: the whole answer. */
260
+ this.$emit('loaded', data)
261
+ } catch {
262
+ this.loadedItems = []
263
+ this.loadError = this.errorText ? this.text(this.errorText) : t('nextcloud-vue', 'The checklist could not be read.')
264
+ } finally {
265
+ this.loading = false
266
+ }
267
+ },
268
+
269
+ /** Re-read when the reader comes back from fixing something. */
270
+ onFocus() {
271
+ if (this.source) {
272
+ this.reload()
273
+ }
274
+ },
275
+ },
276
+ }
277
+ </script>
278
+
279
+ <style scoped>
280
+ .cn-readiness-checklist__description,
281
+ .cn-readiness-checklist__empty {
282
+ color: var(--color-text-maxcontrast);
283
+ margin: 0 0 12px;
284
+ }
285
+
286
+ .cn-readiness-checklist__group-title {
287
+ margin: 12px 0 4px;
288
+ }
289
+
290
+ .cn-readiness-checklist__list {
291
+ list-style: none;
292
+ margin: 0;
293
+ padding: 0;
294
+ }
295
+
296
+ .cn-readiness-checklist__item {
297
+ display: flex;
298
+ gap: calc(var(--default-grid-baseline, 4px) * 3);
299
+ padding-block: calc(var(--default-grid-baseline, 4px) * 2);
300
+ border-block-end: 1px solid var(--color-border);
301
+ }
302
+
303
+ .cn-readiness-checklist__state {
304
+ flex: 0 0 auto;
305
+ min-width: 6em;
306
+ font-weight: bold;
307
+ }
308
+
309
+ .cn-readiness-checklist__state--met {
310
+ color: var(--color-success-text, var(--color-success));
311
+ }
312
+
313
+ .cn-readiness-checklist__state--unmet {
314
+ color: var(--color-error-text, var(--color-error));
315
+ }
316
+
317
+ .cn-readiness-checklist__state--unknown {
318
+ color: var(--color-text-maxcontrast);
319
+ }
320
+
321
+ .cn-readiness-checklist__body {
322
+ display: flex;
323
+ flex-direction: column;
324
+ gap: 2px;
325
+ min-width: 0;
326
+ }
327
+
328
+ .cn-readiness-checklist__explanation {
329
+ color: var(--color-text-maxcontrast);
330
+ }
331
+
332
+ .cn-readiness-checklist__failure {
333
+ color: var(--color-error-text, var(--color-error));
334
+ }
335
+ </style>
@@ -0,0 +1,9 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+
6
+ import CnReadinessChecklist from './CnReadinessChecklist.vue'
7
+
8
+ export default CnReadinessChecklist
9
+ export { CnReadinessChecklist }
@@ -0,0 +1,300 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <div class="cn-schema-rules" data-testid="cn-schema-rules">
7
+ <h4 v-if="title !== null">
8
+ {{ title ? text(title) : t('nextcloud-vue', 'Rules on this schema') }}
9
+ </h4>
10
+ <p class="cn-schema-rules__hint">
11
+ {{ description ? text(description) : t('nextcloud-vue', 'Everything the platform checks when an object is saved, in the order it checks it.') }}
12
+ </p>
13
+
14
+ <NcLoadingIcon v-if="loading" :size="32" :name="t('nextcloud-vue', 'Loading')" />
15
+ <p v-else-if="error"
16
+ class="cn-schema-rules__error"
17
+ role="alert"
18
+ data-testid="cn-schema-rules-error">
19
+ {{ error }}
20
+ </p>
21
+ <p v-else-if="rules.length === 0" class="cn-schema-rules__empty" data-testid="cn-schema-rules-empty">
22
+ {{ emptyText ? text(emptyText) : t('nextcloud-vue', 'Nothing is checked on this schema yet.') }}
23
+ </p>
24
+ <table v-else class="cn-schema-rules__table" data-testid="cn-schema-rules-table">
25
+ <thead>
26
+ <tr>
27
+ <th scope="col">
28
+ {{ t('nextcloud-vue', 'Rule') }}
29
+ </th>
30
+ <th scope="col">
31
+ {{ t('nextcloud-vue', 'Kind') }}
32
+ </th>
33
+ <th scope="col">
34
+ {{ t('nextcloud-vue', 'On') }}
35
+ </th>
36
+ <th scope="col">
37
+ {{ t('nextcloud-vue', 'Last run') }}
38
+ </th>
39
+ <th scope="col">
40
+ <span class="hidden-visually">{{ t('nextcloud-vue', 'Try') }}</span>
41
+ </th>
42
+ </tr>
43
+ </thead>
44
+ <tbody>
45
+ <tr v-for="rule in rules" :key="rule.id" data-testid="cn-schema-rules-row">
46
+ <th scope="row">
47
+ {{ rule.label || rule.key || rule.id }}
48
+ </th>
49
+ <td data-testid="cn-schema-rules-kind">
50
+ {{ kindLabel(rule) }}
51
+ </td>
52
+ <td>{{ rule.enabled === false ? t('nextcloud-vue', 'Off') : t('nextcloud-vue', 'On') }}</td>
53
+ <td>{{ rule.lastRun || t('nextcloud-vue', 'Never') }}</td>
54
+ <td>
55
+ <NcButton variant="tertiary"
56
+ :disabled="trying === rule.id || trialObject.trim() === ''"
57
+ :data-testid="`cn-schema-rules-try-${rule.id}`"
58
+ @click="tryRule(rule)">
59
+ {{ t('nextcloud-vue', 'Try it') }}
60
+ </NcButton>
61
+ </td>
62
+ </tr>
63
+ </tbody>
64
+ </table>
65
+
66
+ <div class="cn-schema-rules__trial">
67
+ <NcTextField :modelValue="trialObject"
68
+ :label="objectLabel ? text(objectLabel) : t('nextcloud-vue', 'Object to try a rule against')"
69
+ :placeholder="objectPlaceholder ? text(objectPlaceholder) : t('nextcloud-vue', 'Paste an object id')"
70
+ data-testid="cn-schema-rules-object-id"
71
+ @update:modelValue="(v) => (trialObject = v)" />
72
+ <p class="cn-schema-rules__hint">
73
+ {{ t('nextcloud-vue', 'A trial writes nothing. It reads the object as it stands and says what the rule would have decided.') }}
74
+ </p>
75
+ <div v-if="trace" class="cn-schema-rules__trace" data-testid="cn-schema-rules-trace">
76
+ <p class="cn-schema-rules__verdict">
77
+ {{ verdictLine }}
78
+ </p>
79
+ <p v-if="trace.operand" data-testid="cn-schema-rules-operand">
80
+ {{ t('nextcloud-vue', 'It read {operand} and found {value}.', { operand: trace.operand, value: trace.operandValue || t('nextcloud-vue', 'nothing') }) }}
81
+ </p>
82
+ <p v-if="trace.message">
83
+ {{ trace.message }}
84
+ </p>
85
+ </div>
86
+ </div>
87
+ </div>
88
+ </template>
89
+
90
+ <script>
91
+ import { translate as t } from '@nextcloud/l10n'
92
+ import { NcButton, NcLoadingIcon, NcTextField } from '@nextcloud/vue'
93
+ import { evaluateRule, fetchRuleVocabulary, fetchSchemaRules, kindsByName, traceOf } from './schemaRules.js'
94
+
95
+ /**
96
+ * CnSchemaRulesWidget: the rules OpenRegister's engine holds for one schema,
97
+ * in the order it evaluates them, each with its kind (from the engine's own
98
+ * vocabulary), whether it is on and when it last ran, and a dry run against
99
+ * one stored object that writes nothing and shows the trace (verdict, the
100
+ * operand it read and what it found).
101
+ *
102
+ * The inventory is an admin read: a refusal is said in one sentence instead
103
+ * of an empty table that would read as "nothing is checked".
104
+ *
105
+ * @spec openspec/changes/schema-rules-widget/specs/schema-rules-widget/spec.md
106
+ */
107
+ export default {
108
+ name: 'CnSchemaRulesWidget',
109
+
110
+ components: { NcButton, NcLoadingIcon, NcTextField },
111
+
112
+ inject: {
113
+ /** The host app's translate, for the texts the page declares. */
114
+ cnTranslate: { default: () => (key) => key },
115
+ },
116
+
117
+ props: {
118
+ /** The register slug the trial object lives in. */
119
+ register: { type: String, required: true },
120
+ /** The schema slug whose rules are listed. */
121
+ schema: { type: String, required: true },
122
+ /** The object a trial runs against, pre-filled; the reader can change it. */
123
+ objectId: { type: String, default: '' },
124
+ /** The heading; `null` draws none. */
125
+ title: { type: String, default: '' },
126
+ /** The line under the heading. */
127
+ description: { type: String, default: '' },
128
+ /** Said when the schema has no rules. */
129
+ emptyText: { type: String, default: '' },
130
+ /** The label of the trial object field. */
131
+ objectLabel: { type: String, default: '' },
132
+ /** The placeholder of the trial object field. */
133
+ objectPlaceholder: { type: String, default: '' },
134
+ /** Said when the inventory is refused (403). */
135
+ refusedText: { type: String, default: '' },
136
+ /** Whether to read nothing yet (a record still being created). */
137
+ disabled: { type: Boolean, default: false },
138
+ },
139
+
140
+ data() {
141
+ return {
142
+ loading: false,
143
+ error: '',
144
+ rules: [],
145
+ kinds: {},
146
+ trialObject: this.objectId,
147
+ trying: '',
148
+ trace: null,
149
+ }
150
+ },
151
+
152
+ computed: {
153
+ /** @return {string} What the trial decided, as a sentence. */
154
+ verdictLine() {
155
+ const verdicts = {
156
+ fired: t('nextcloud-vue', 'The rule fired.'),
157
+ no_match: t('nextcloud-vue', 'The rule did not match this object.'),
158
+ refused: t('nextcloud-vue', 'The rule refused the save.'),
159
+ error: t('nextcloud-vue', 'The rule could not be evaluated.'),
160
+ }
161
+ return verdicts[this.trace && this.trace.verdict] || (this.trace && this.trace.verdict) || ''
162
+ },
163
+ },
164
+
165
+ watch: {
166
+ objectId(value) {
167
+ this.trialObject = value
168
+ },
169
+
170
+ schema() {
171
+ this.load()
172
+ },
173
+ },
174
+
175
+ mounted() {
176
+ if (!this.disabled) {
177
+ this.load()
178
+ }
179
+ },
180
+
181
+ methods: {
182
+ t,
183
+
184
+ /**
185
+ * @param {string} value A declared text.
186
+ * @return {string} It, translated by the host.
187
+ */
188
+ text(value) {
189
+ return value ? String(this.cnTranslate(String(value))) : ''
190
+ },
191
+
192
+ /**
193
+ * The word for a rule's kind, from the vocabulary, else the kind the
194
+ * rule carries: a blank would read as "this rule has no kind".
195
+ *
196
+ * @param {object} rule An inventory row.
197
+ * @return {string} The label.
198
+ */
199
+ kindLabel(rule) {
200
+ const kind = String((rule && rule.kind) ?? '')
201
+ return String((this.kinds[kind] && this.kinds[kind].description) || kind)
202
+ },
203
+
204
+ /**
205
+ * Read the vocabulary and the inventory.
206
+ *
207
+ * @return {Promise<void>}
208
+ */
209
+ async load() {
210
+ if (!this.schema) {
211
+ return
212
+ }
213
+ this.loading = true
214
+ this.error = ''
215
+ this.kinds = kindsByName(await fetchRuleVocabulary())
216
+ try {
217
+ this.rules = await fetchSchemaRules(this.schema)
218
+ } catch (e) {
219
+ this.rules = []
220
+ const refused = Number(e && e.response && e.response.status) === 403
221
+ this.error = refused
222
+ ? (this.refusedText ? this.text(this.refusedText) : t('nextcloud-vue', 'Only an administrator can read the rules of this schema.'))
223
+ : t('nextcloud-vue', 'The rules could not be read. Ask an administrator.')
224
+ }
225
+ this.loading = false
226
+ },
227
+
228
+ /**
229
+ * Try one rule against the trial object, keyed on the rule's id.
230
+ *
231
+ * @param {object} rule An inventory row.
232
+ * @return {Promise<void>}
233
+ */
234
+ async tryRule(rule) {
235
+ const ruleId = String((rule && rule.id) ?? '')
236
+ const objectId = this.trialObject.trim()
237
+ if (ruleId === '' || objectId === '') {
238
+ return
239
+ }
240
+ this.trying = ruleId
241
+ this.trace = null
242
+ try {
243
+ this.trace = traceOf(await evaluateRule(this.schema, ruleId, this.register, objectId))
244
+ } catch {
245
+ this.trace = { verdict: 'error', operand: '', operandValue: '', message: t('nextcloud-vue', 'That object could not be read.') }
246
+ }
247
+ this.trying = ''
248
+ },
249
+ },
250
+ }
251
+ </script>
252
+
253
+ <style scoped>
254
+ .cn-schema-rules__hint {
255
+ font-size: 12px;
256
+ color: var(--color-text-maxcontrast);
257
+ margin: 4px 0 12px;
258
+ }
259
+
260
+ .cn-schema-rules__table {
261
+ width: 100%;
262
+ border-collapse: collapse;
263
+ }
264
+
265
+ .cn-schema-rules__table th,
266
+ .cn-schema-rules__table td {
267
+ text-align: start;
268
+ padding: 6px 8px;
269
+ border-bottom: 1px solid var(--color-border);
270
+ }
271
+
272
+ .cn-schema-rules__trial {
273
+ margin-top: 16px;
274
+ border-top: 1px solid var(--color-border);
275
+ padding-top: 12px;
276
+ }
277
+
278
+ .cn-schema-rules__trace {
279
+ margin-top: 8px;
280
+ padding: 8px 12px;
281
+ border-radius: var(--border-radius-large, 8px);
282
+ background-color: var(--color-background-hover);
283
+ }
284
+
285
+ .cn-schema-rules__trace p {
286
+ margin: 0;
287
+ }
288
+
289
+ .cn-schema-rules__verdict {
290
+ font-weight: bold;
291
+ }
292
+
293
+ .cn-schema-rules__error {
294
+ color: var(--color-error-text);
295
+ }
296
+
297
+ .cn-schema-rules__empty {
298
+ color: var(--color-text-maxcontrast);
299
+ }
300
+ </style>
@@ -0,0 +1,10 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+
6
+ import CnSchemaRulesWidget from './CnSchemaRulesWidget.vue'
7
+
8
+ export default CnSchemaRulesWidget
9
+ export { CnSchemaRulesWidget }
10
+ export { evaluateRule, fetchRuleVocabulary, fetchSchemaRules, kindsByName, traceOf } from './schemaRules.js'