@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,123 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * The rules OpenRegister's engine holds for a schema, read rather than kept:
6
+ * the closed sets (vocabulary), the inventory in evaluation order, and a dry
7
+ * run that writes nothing.
8
+ *
9
+ * A rule is keyed on its id (`<kind>:<schemaSlug>:<key>`), never on its
10
+ * position: the order belongs to the pipeline and moves the day a kind is
11
+ * added. Kind labels come from the vocabulary endpoint, so a kind added later
12
+ * renders instead of blanking.
13
+ */
14
+
15
+ import axios from '@nextcloud/axios'
16
+ import { generateUrl } from '@nextcloud/router'
17
+
18
+ /** Where OpenRegister publishes the three closed sets. */
19
+ export const RULE_VOCABULARY_PATH = '/apps/openregister/api/rules/vocabulary'
20
+
21
+ /**
22
+ * @param {string} schema The schema slug.
23
+ * @return {string} The url of the schema's rule inventory.
24
+ */
25
+ export function ruleInventoryUrl(schema) {
26
+ return generateUrl(`/apps/openregister/api/schemas/${encodeURIComponent(schema)}/rules`)
27
+ }
28
+
29
+ /**
30
+ * @param {string} schema The schema slug.
31
+ * @param {string} ruleId The rule id.
32
+ * @return {string} The url of the rule's dry run.
33
+ */
34
+ export function ruleEvaluateUrl(schema, ruleId) {
35
+ return generateUrl(`/apps/openregister/api/schemas/${encodeURIComponent(schema)}/rules/${encodeURIComponent(ruleId)}/evaluate`)
36
+ }
37
+
38
+ /**
39
+ * @param {unknown} data An answer.
40
+ * @return {boolean} Whether it carries the three sets.
41
+ */
42
+ export function isRuleVocabulary(data) {
43
+ return !!data && Array.isArray(data.kinds) && Array.isArray(data.verdicts) && Array.isArray(data.actions)
44
+ }
45
+
46
+ /**
47
+ * The kinds the engine publishes, keyed by kind.
48
+ *
49
+ * @param {object} vocabulary The vocabulary answer.
50
+ * @return {object} The descriptors, by kind.
51
+ */
52
+ export function kindsByName(vocabulary) {
53
+ const kinds = {}
54
+ if (!isRuleVocabulary(vocabulary)) {
55
+ return kinds
56
+ }
57
+ vocabulary.kinds.forEach((kind) => {
58
+ const name = String((kind && kind.kind) ?? '')
59
+ if (name !== '') {
60
+ kinds[name] = kind
61
+ }
62
+ })
63
+ return kinds
64
+ }
65
+
66
+ /**
67
+ * Read the vocabulary. A refusal answers the empty sets: the vocabulary is open
68
+ * to any signed-in caller, so a refusal means an instance that predates the
69
+ * engine, and each rule then shows its own kind string.
70
+ *
71
+ * @return {Promise<object>} The vocabulary, possibly empty.
72
+ */
73
+ export async function fetchRuleVocabulary() {
74
+ try {
75
+ const { data } = await axios.get(generateUrl(RULE_VOCABULARY_PATH))
76
+ return isRuleVocabulary(data) ? data : { kinds: [], verdicts: [], actions: [] }
77
+ } catch {
78
+ return { kinds: [], verdicts: [], actions: [] }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Read one schema's rules, in the order the engine evaluates them. The
84
+ * inventory is an admin read; a refusal throws for the caller to say so.
85
+ *
86
+ * @param {string} schema The schema slug.
87
+ * @return {Promise<Array<object>>} The rules.
88
+ */
89
+ export async function fetchSchemaRules(schema) {
90
+ const { data } = await axios.get(ruleInventoryUrl(schema))
91
+ return Array.isArray(data && data.rules) ? data.rules : []
92
+ }
93
+
94
+ /**
95
+ * Try one rule against one stored object, writing nothing.
96
+ *
97
+ * @param {string} schema The schema slug.
98
+ * @param {string} ruleId The rule id.
99
+ * @param {string} register The register slug the object lives in.
100
+ * @param {string} objectId The object to try it against.
101
+ * @return {Promise<object>} The trial result, carrying `trace`.
102
+ */
103
+ export async function evaluateRule(schema, ruleId, register, objectId) {
104
+ const { data } = await axios.post(ruleEvaluateUrl(schema, ruleId), { register, objectId })
105
+ return data || {}
106
+ }
107
+
108
+ /**
109
+ * The trace of a trial. `operand` and `operandValue` are what turn "why did this
110
+ * rule not fire" into a fact, so they are shown beside the verdict.
111
+ *
112
+ * @param {object} result The trial result.
113
+ * @return {{verdict: string, operand: string, operandValue: string, message: string}} The trace.
114
+ */
115
+ export function traceOf(result) {
116
+ const trace = (result && result.trace) || {}
117
+ return {
118
+ verdict: String(trace.verdict ?? ''),
119
+ operand: String(trace.operand ?? ''),
120
+ operandValue: String(trace.operandValue ?? ''),
121
+ message: String(trace.message ?? (result && result.error && result.error.message) ?? ''),
122
+ }
123
+ }
@@ -0,0 +1,160 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <div class="cn-search-index-status" data-testid="cn-search-index-status">
7
+ <NcLoadingIcon v-if="loading" :size="24" :name="t('nextcloud-vue', 'Loading')" />
8
+ <NcNoteCard v-else-if="error" type="error" data-testid="cn-search-index-error">
9
+ {{ error }}
10
+ </NcNoteCard>
11
+ <template v-else>
12
+ <dl class="cn-search-index-status__figures">
13
+ <div>
14
+ <dt>{{ t('nextcloud-vue', 'Tables in scope') }}</dt>
15
+ <dd data-testid="cn-search-index-tables">
16
+ {{ status.tableCount }}
17
+ </dd>
18
+ </div>
19
+ <div>
20
+ <dt>{{ t('nextcloud-vue', 'Indexes on them') }}</dt>
21
+ <dd data-testid="cn-search-index-indexes">
22
+ {{ status.indexCount }}
23
+ </dd>
24
+ </div>
25
+ <div>
26
+ <dt>{{ t('nextcloud-vue', 'Last run') }}</dt>
27
+ <dd data-testid="cn-search-index-last-run">
28
+ {{ lastRun }}
29
+ </dd>
30
+ </div>
31
+ <div>
32
+ <dt>{{ t('nextcloud-vue', 'Rebuild without locking') }}</dt>
33
+ <dd data-testid="cn-search-index-concurrent">
34
+ {{ concurrent }}
35
+ </dd>
36
+ </div>
37
+ </dl>
38
+ <p data-testid="cn-search-index-commands">
39
+ {{ t('nextcloud-vue', 'Run {command} status on the server for the same figures, and {command} rebuild --apply after a schema change.', { command }) }}
40
+ </p>
41
+ </template>
42
+ </div>
43
+ </template>
44
+
45
+ <script>
46
+ import { getCanonicalLocale, translate as t } from '@nextcloud/l10n'
47
+ import { NcLoadingIcon, NcNoteCard } from '@nextcloud/vue'
48
+ import { fetchSearchIndexStatus, SEARCH_INDEX_COMMAND, SEARCH_INDEX_STATUS_URL } from './searchIndexStatus.js'
49
+
50
+ /**
51
+ * CnSearchIndexStatus: the state of OpenRegister's search indexes (tables in
52
+ * scope, indexes on them, the last maintenance run, whether a rebuild locks a
53
+ * table), read live and never copied. The acts are named, not offered: a
54
+ * rebuild walks every table in scope and nothing stops one from a settings
55
+ * page, so the occ verbs are printed for somebody with a shell.
56
+ *
57
+ * @spec openspec/changes/search-index-status/specs/search-index-status/spec.md
58
+ */
59
+ export default {
60
+ name: 'CnSearchIndexStatus',
61
+
62
+ components: { NcLoadingIcon, NcNoteCard },
63
+
64
+ props: {
65
+ /** The app path OpenRegister reports its indexes at. */
66
+ source: { type: String, default: SEARCH_INDEX_STATUS_URL },
67
+ /** The occ command that acts on the indexes. */
68
+ command: { type: String, default: SEARCH_INDEX_COMMAND },
69
+ },
70
+
71
+ data() {
72
+ return {
73
+ loading: true,
74
+ error: '',
75
+ status: { concurrentRebuildSupported: false, tables: {}, tableCount: 0, indexCount: 0, lastRun: null },
76
+ }
77
+ },
78
+
79
+ computed: {
80
+ /** @return {string} When maintenance last ran, or that it never has. */
81
+ lastRun() {
82
+ if (!this.status.lastRun) {
83
+ return t('nextcloud-vue', 'Never')
84
+ }
85
+ const when = new Date(this.status.lastRun)
86
+ if (Number.isNaN(when.getTime())) {
87
+ return String(this.status.lastRun)
88
+ }
89
+ return when.toLocaleString(getCanonicalLocale(), { dateStyle: 'medium', timeStyle: 'short' })
90
+ },
91
+
92
+ /** @return {string} Whether a rebuild locks the table, in words. */
93
+ concurrent() {
94
+ return this.status.concurrentRebuildSupported === true
95
+ ? t('nextcloud-vue', 'Yes')
96
+ : t('nextcloud-vue', 'No, a rebuild locks the table it is on')
97
+ },
98
+ },
99
+
100
+ mounted() {
101
+ this.load()
102
+ },
103
+
104
+ methods: {
105
+ t,
106
+
107
+ /**
108
+ * Read what OpenRegister says about its indexes.
109
+ *
110
+ * @return {Promise<void>}
111
+ */
112
+ async load() {
113
+ this.loading = true
114
+ this.error = ''
115
+ try {
116
+ this.status = await fetchSearchIndexStatus(this.source)
117
+ } catch (e) {
118
+ this.error = String((e && e.message) || e)
119
+ } finally {
120
+ this.loading = false
121
+ }
122
+ },
123
+ },
124
+ }
125
+ </script>
126
+
127
+ <style scoped>
128
+ /* Label above value, and the label wraps: Nextcloud's settings stylesheet gives a `dt` a fixed width. */
129
+ .cn-search-index-status__figures {
130
+ display: grid;
131
+ grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
132
+ gap: 16px 24px;
133
+ margin-block: 0 12px;
134
+ }
135
+
136
+ .cn-search-index-status__figures > div {
137
+ display: flex;
138
+ flex-direction: column;
139
+ gap: 2px;
140
+ min-width: 0;
141
+ }
142
+
143
+ .cn-search-index-status__figures dt {
144
+ display: block;
145
+ float: none;
146
+ width: auto;
147
+ margin: 0;
148
+ padding: 0;
149
+ text-align: start;
150
+ white-space: normal;
151
+ overflow-wrap: anywhere;
152
+ color: var(--color-text-maxcontrast);
153
+ }
154
+
155
+ .cn-search-index-status__figures dd {
156
+ font-weight: bold;
157
+ margin: 0;
158
+ overflow-wrap: anywhere;
159
+ }
160
+ </style>
@@ -0,0 +1,10 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+
6
+ import CnSearchIndexStatus from './CnSearchIndexStatus.vue'
7
+
8
+ export default CnSearchIndexStatus
9
+ export { CnSearchIndexStatus }
10
+ export { lastRunTime, SEARCH_INDEX_COMMAND } from './searchIndexStatus.js'
@@ -0,0 +1,56 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+
6
+ import axios from '@nextcloud/axios'
7
+ import { generateUrl } from '@nextcloud/router'
8
+
9
+ /** The occ command that rebuilds, snapshots, restores and reports the indexes. */
10
+ export const SEARCH_INDEX_COMMAND = 'occ openregister:tables:search-index'
11
+
12
+ /** Where OpenRegister reports its search indexes. */
13
+ export const SEARCH_INDEX_STATUS_URL = '/apps/openregister/api/settings/search-index'
14
+
15
+ /**
16
+ * When the last stored run finished, or started, or null when none ran.
17
+ *
18
+ * OpenRegister answers `lastRun` as the report of that run (`startedAt`,
19
+ * `finishedAt`) and as `[]` when nothing ran.
20
+ *
21
+ * @param {object|Array|string|null|undefined} report What OpenRegister answered as `lastRun`.
22
+ * @return {string|null} An ISO 8601 time, or null when no run is on record.
23
+ */
24
+ export function lastRunTime(report) {
25
+ if (typeof report === 'string') {
26
+ return report === '' ? null : report
27
+ }
28
+ if (report === null || typeof report !== 'object' || Array.isArray(report)) {
29
+ return null
30
+ }
31
+ const time = report.finishedAt || report.startedAt || ''
32
+ return typeof time === 'string' && time !== '' ? time : null
33
+ }
34
+
35
+ /**
36
+ * What OpenRegister says about the indexes behind object search. A failed
37
+ * read throws: an outage and an instance with no indexes look the same from
38
+ * the browser, and only one of them is somebody's problem.
39
+ *
40
+ * @param {string} [url] The app path to read.
41
+ * @return {Promise<{concurrentRebuildSupported: boolean, tables: object, tableCount: number, indexCount: number, lastRun: (string|null)}>}
42
+ * @throws {Error} When it cannot be read.
43
+ */
44
+ export async function fetchSearchIndexStatus(url = SEARCH_INDEX_STATUS_URL) {
45
+ const { data } = await axios.get(generateUrl(url))
46
+ if (data === null || typeof data !== 'object' || data.error !== undefined) {
47
+ throw new Error(String((data && data.error) ?? 'The search index status could not be read.'))
48
+ }
49
+ return {
50
+ concurrentRebuildSupported: data.concurrentRebuildSupported === true,
51
+ tables: data.tables && typeof data.tables === 'object' ? data.tables : {},
52
+ tableCount: Number(data.tableCount ?? 0),
53
+ indexCount: Number(data.indexCount ?? 0),
54
+ lastRun: lastRunTime(data.lastRun),
55
+ }
56
+ }
@@ -0,0 +1,328 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <div class="cn-shipped-configuration" data-testid="cn-shipped-configuration">
7
+ <p v-if="description" class="cn-shipped-configuration__intro">
8
+ {{ text(description) }}
9
+ </p>
10
+
11
+ <NcSelect v-if="schemas.length > 1"
12
+ v-model="schemaOption"
13
+ class="cn-shipped-configuration__picker"
14
+ :options="schemaOptions"
15
+ :inputLabel="t('nextcloud-vue', 'Show')"
16
+ :clearable="false"
17
+ data-testid="cn-shipped-schema" />
18
+
19
+ <table class="cn-shipped-configuration__table" data-testid="cn-shipped-configuration-table">
20
+ <thead>
21
+ <tr>
22
+ <th scope="col">
23
+ {{ t('nextcloud-vue', 'Name') }}
24
+ </th>
25
+ <th scope="col">
26
+ {{ t('nextcloud-vue', 'State') }}
27
+ </th>
28
+ <th scope="col">
29
+ {{ t('nextcloud-vue', 'Shipped set') }}
30
+ </th>
31
+ <th scope="col">
32
+ {{ t('nextcloud-vue', 'Version') }}
33
+ </th>
34
+ <th scope="col">
35
+ {{ t('nextcloud-vue', 'Newer version') }}
36
+ </th>
37
+ </tr>
38
+ </thead>
39
+ <tbody>
40
+ <tr v-for="row in rows" :key="row.targetObject">
41
+ <td>{{ row.title || row.targetObject }}</td>
42
+ <td :data-testid="'cn-shipped-state-' + row.targetObject">
43
+ {{ stateLabel(row) }}
44
+ </td>
45
+ <td>{{ row.set }}</td>
46
+ <td>{{ row.setVersion }}</td>
47
+ <td>
48
+ <NcButton v-if="updatable(row)"
49
+ :data-testid="'cn-shipped-adopt-' + row.targetObject"
50
+ :disabled="adopting === row.targetObject"
51
+ @click="adopt(row)">
52
+ {{ adoptLabel(row) }}
53
+ </NcButton>
54
+ <span v-else class="cn-shipped-configuration__muted">
55
+ {{ t('nextcloud-vue', 'Up to date') }}
56
+ </span>
57
+ </td>
58
+ </tr>
59
+ <tr v-if="!rows.length">
60
+ <td colspan="5"
61
+ class="cn-shipped-configuration__muted"
62
+ data-testid="cn-shipped-configuration-empty">
63
+ {{ emptyLine }}
64
+ </td>
65
+ </tr>
66
+ </tbody>
67
+ </table>
68
+
69
+ <NcNoteCard v-if="loadError" type="error" data-testid="cn-shipped-configuration-error">
70
+ {{ loadError }}
71
+ </NcNoteCard>
72
+ <NcNoteCard v-if="adoptError" type="error" data-testid="cn-shipped-configuration-adopt-error">
73
+ {{ adoptError }}
74
+ </NcNoteCard>
75
+ </div>
76
+ </template>
77
+
78
+ <script>
79
+ import axios from '@nextcloud/axios'
80
+ import { showSuccess } from '@nextcloud/dialogs'
81
+ import { translate as t } from '@nextcloud/l10n'
82
+ import { generateUrl } from '@nextcloud/router'
83
+ import { NcButton, NcNoteCard, NcSelect } from '@nextcloud/vue'
84
+
85
+ /**
86
+ * The states a seeded object can be in.
87
+ *
88
+ * @type {Readonly<Record<string, string>>}
89
+ */
90
+ export const SHIPPED_STATES = Object.freeze({
91
+ shipped: 'shipped',
92
+ changed: 'changed',
93
+ local: 'local',
94
+ removed: 'removed',
95
+ })
96
+
97
+ /**
98
+ * CnShippedConfigurationSection: what an app shipped, what this instance
99
+ * changed, and what it authored. One row per seeded object with the set and
100
+ * version it came from, its state, and the newer version offered per object.
101
+ * Taking a newer version over a local change says so on the button, because
102
+ * the change goes.
103
+ *
104
+ * The app's endpoint answers `GET {endpoint}/{schema}` with `{ items: [{
105
+ * targetObject, title, state, set, setVersion, latestVersion, updateAvailable
106
+ * }] }` and takes `POST {endpoint}/{schema}/{targetObject}/adopt` with `{ set,
107
+ * acceptLocalChangeLoss }`, refusing with `{ reason }`. An empty ledger beside
108
+ * a register that holds objects is said as such (`countUrl`), not as "nothing
109
+ * seeded".
110
+ *
111
+ * @spec openspec/changes/shipped-configuration-section/specs/shipped-configuration-section/spec.md
112
+ */
113
+ export default {
114
+ name: 'CnShippedConfigurationSection',
115
+
116
+ components: { NcButton, NcNoteCard, NcSelect },
117
+
118
+ inject: {
119
+ /** The host app's translate, for the texts the page declares. */
120
+ cnTranslate: { default: () => (key) => key },
121
+ },
122
+
123
+ props: {
124
+ /** The app path of the shipped ledger (`/apps/<app>/api/starter/shipped`). */
125
+ endpoint: { type: String, required: true },
126
+ /** The schemas to show, `[{ slug, label }]`; the first is shown first. */
127
+ schemas: { type: Array, default: () => [] },
128
+ /**
129
+ * An app path that counts a schema's objects, `{schema}` filled in
130
+ * (`/apps/openregister/api/objects/<register>/{schema}`), read with
131
+ * `_limit=1` for its `total`.
132
+ */
133
+ countUrl: { type: String, default: '' },
134
+ /** A line above the table. */
135
+ description: { type: String, default: '' },
136
+ },
137
+
138
+ data() {
139
+ return {
140
+ schema: this.schemas.length > 0 ? String(this.schemas[0].slug) : '',
141
+ rows: [],
142
+ present: null,
143
+ loadError: '',
144
+ adoptError: '',
145
+ adopting: '',
146
+ }
147
+ },
148
+
149
+ computed: {
150
+ /** @return {Array<{id: string, label: string}>} The picker's options. */
151
+ schemaOptions() {
152
+ return this.schemas.map((option) => ({ id: String(option.slug), label: this.text(option.label || option.slug) }))
153
+ },
154
+
155
+ schemaOption: {
156
+ /** @return {object|null} The chosen schema. */
157
+ get() {
158
+ return this.schemaOptions.find((option) => option.id === this.schema) || null
159
+ },
160
+
161
+ /** @param {object|null} option The chosen schema. */
162
+ set(option) {
163
+ if (option) {
164
+ this.schema = option.id
165
+ this.reload()
166
+ }
167
+ },
168
+ },
169
+
170
+ /** @return {string} What the empty table says, which depends on what the register holds. */
171
+ emptyLine() {
172
+ if (typeof this.present === 'number' && this.present > 0) {
173
+ return t('nextcloud-vue', 'The register holds {count} of these. None came from a shipped set, so there is no shipped version to compare with.', { count: this.present })
174
+ }
175
+ return t('nextcloud-vue', 'Nothing has been seeded yet.')
176
+ },
177
+ },
178
+
179
+ mounted() {
180
+ this.reload()
181
+ },
182
+
183
+ methods: {
184
+ t,
185
+
186
+ /**
187
+ * @param {string} value A declared text.
188
+ * @return {string} It, translated by the host.
189
+ */
190
+ text(value) {
191
+ return value ? String(this.cnTranslate(String(value))) : ''
192
+ },
193
+
194
+ /**
195
+ * Read what shipped for the shown schema. A failed read empties the
196
+ * table: rows left beside an error would read as the current state.
197
+ *
198
+ * @return {Promise<void>}
199
+ */
200
+ async reload() {
201
+ if (!this.schema) {
202
+ return
203
+ }
204
+ try {
205
+ const { data } = await axios.get(generateUrl(`${this.endpoint}/${encodeURIComponent(this.schema)}`))
206
+ this.rows = Array.isArray(data && data.items) ? data.items : []
207
+ this.loadError = ''
208
+ this.present = null
209
+ if (this.rows.length === 0) {
210
+ this.present = await this.count()
211
+ }
212
+ } catch {
213
+ this.rows = []
214
+ this.present = null
215
+ this.loadError = t('nextcloud-vue', 'Could not read the shipped configuration.')
216
+ }
217
+ },
218
+
219
+ /**
220
+ * How many objects of the shown schema the register holds, whatever
221
+ * brought them.
222
+ *
223
+ * @return {Promise<number|null>} The count, or null when it cannot be read.
224
+ */
225
+ async count() {
226
+ if (!this.countUrl) {
227
+ return null
228
+ }
229
+ try {
230
+ const { data } = await axios.get(generateUrl(this.countUrl.replace('{schema}', encodeURIComponent(this.schema))), { params: { _limit: 1 } })
231
+ const total = Number(data && data.total)
232
+ return Number.isFinite(total) ? total : null
233
+ } catch {
234
+ return null
235
+ }
236
+ },
237
+
238
+ /**
239
+ * @param {object} row A ledger row.
240
+ * @return {string} What an administrator reads beside the object.
241
+ */
242
+ stateLabel(row) {
243
+ const state = (row && row.state) || SHIPPED_STATES.local
244
+ return {
245
+ [SHIPPED_STATES.shipped]: t('nextcloud-vue', 'Shipped, unchanged'),
246
+ [SHIPPED_STATES.changed]: t('nextcloud-vue', 'Shipped, changed here'),
247
+ [SHIPPED_STATES.removed]: t('nextcloud-vue', 'Shipped, removed here'),
248
+ }[state] || t('nextcloud-vue', 'Yours')
249
+ },
250
+
251
+ /**
252
+ * @param {object} row A ledger row.
253
+ * @return {boolean} Whether a newer shipped version can be taken.
254
+ */
255
+ updatable(row) {
256
+ return Boolean(row && row.updateAvailable && row.state !== SHIPPED_STATES.removed)
257
+ },
258
+
259
+ /**
260
+ * @param {object} row A ledger row.
261
+ * @return {boolean} Whether taking the newer version overwrites a local change.
262
+ */
263
+ losesLocalChange(row) {
264
+ return Boolean(row && row.state === SHIPPED_STATES.changed)
265
+ },
266
+
267
+ /**
268
+ * @param {object} row A ledger row.
269
+ * @return {string} The button's words, which say when a local change goes.
270
+ */
271
+ adoptLabel(row) {
272
+ return this.losesLocalChange(row)
273
+ ? t('nextcloud-vue', 'Take {version} and lose your change', { version: row.latestVersion })
274
+ : t('nextcloud-vue', 'Take {version}', { version: row.latestVersion })
275
+ },
276
+
277
+ /**
278
+ * Take the newer shipped version of one object, then read the ledger again.
279
+ *
280
+ * @param {object} row A ledger row.
281
+ * @return {Promise<void>}
282
+ */
283
+ async adopt(row) {
284
+ this.adopting = row.targetObject
285
+ this.adoptError = ''
286
+ try {
287
+ await axios.post(
288
+ generateUrl(`${this.endpoint}/${encodeURIComponent(this.schema)}/${encodeURIComponent(row.targetObject)}/adopt`),
289
+ { set: row.set, acceptLocalChangeLoss: this.losesLocalChange(row) },
290
+ )
291
+ showSuccess(t('nextcloud-vue', 'The newer version was taken.'))
292
+ await this.reload()
293
+ } catch (error) {
294
+ const data = error && error.response && error.response.data
295
+ this.adoptError = String((data && (data.reason || data.message)) || t('nextcloud-vue', 'The newer version was not taken.'))
296
+ } finally {
297
+ this.adopting = ''
298
+ }
299
+ },
300
+ },
301
+ }
302
+ </script>
303
+
304
+ <style scoped>
305
+ .cn-shipped-configuration__intro,
306
+ .cn-shipped-configuration__muted {
307
+ color: var(--color-text-maxcontrast);
308
+ }
309
+
310
+ .cn-shipped-configuration__picker {
311
+ max-width: 320px;
312
+ }
313
+
314
+ .cn-shipped-configuration__table {
315
+ width: 100%;
316
+ border-collapse: collapse;
317
+ margin-top: 12px;
318
+ /* Nextcloud sets `table { white-space: nowrap }`; a cell here can be a sentence. */
319
+ white-space: normal;
320
+ }
321
+
322
+ .cn-shipped-configuration__table th,
323
+ .cn-shipped-configuration__table td {
324
+ padding: 6px 8px;
325
+ border-bottom: 1px solid var(--color-border);
326
+ text-align: start;
327
+ }
328
+ </style>
@@ -0,0 +1,9 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+
6
+ import CnShippedConfigurationSection from './CnShippedConfigurationSection.vue'
7
+
8
+ export default CnShippedConfigurationSection
9
+ export { CnShippedConfigurationSection }