@conduction/nextcloud-vue 2.119.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 (179) 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/CnIndexPage/CnIndexPage.vue2.js +53 -1
  16. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  17. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +546 -523
  18. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  19. package/dist/esm/components/CnIndexPage/useNamedSource.js +9 -0
  20. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  21. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js +9 -0
  22. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue.js.map +1 -0
  23. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js +257 -0
  24. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue2.js.map +1 -0
  25. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js +266 -0
  26. package/dist/esm/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue3.js.map +1 -0
  27. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue.js +1 -1
  28. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js +14 -7
  29. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue2.js.map +1 -1
  30. package/dist/esm/components/CnObjectSidebar/CnObjectSidebar.vue3.js.map +1 -1
  31. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js +9 -0
  32. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue.js.map +1 -0
  33. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js +278 -0
  34. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue2.js.map +1 -0
  35. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js +128 -0
  36. package/dist/esm/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue3.js.map +1 -0
  37. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js +9 -0
  38. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue.js.map +1 -0
  39. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js +214 -0
  40. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue2.js.map +1 -0
  41. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js +125 -0
  42. package/dist/esm/components/CnReadinessChecklist/CnReadinessChecklist.vue3.js.map +1 -0
  43. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js +9 -0
  44. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue.js.map +1 -0
  45. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js +163 -0
  46. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue2.js.map +1 -0
  47. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js +131 -0
  48. package/dist/esm/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue3.js.map +1 -0
  49. package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js +127 -0
  50. package/dist/esm/components/CnSchemaRulesWidget/schemaRules.js.map +1 -0
  51. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js +9 -0
  52. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue.js.map +1 -0
  53. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js +82 -0
  54. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue2.js.map +1 -0
  55. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js +61 -0
  56. package/dist/esm/components/CnSearchIndexStatus/CnSearchIndexStatus.vue3.js.map +1 -0
  57. package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js +60 -0
  58. package/dist/esm/components/CnSearchIndexStatus/searchIndexStatus.js.map +1 -0
  59. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js +10 -0
  60. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue.js.map +1 -0
  61. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js +226 -0
  62. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue2.js.map +1 -0
  63. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js +126 -0
  64. package/dist/esm/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue3.js.map +1 -0
  65. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js +10 -0
  66. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue.js.map +1 -0
  67. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js +944 -0
  68. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue2.js.map +1 -0
  69. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js +276 -0
  70. package/dist/esm/components/CnStateMachineEditor/CnStateMachineEditor.vue3.js.map +1 -0
  71. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js +9 -0
  72. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue.js.map +1 -0
  73. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js +354 -0
  74. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue2.js.map +1 -0
  75. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js +392 -0
  76. package/dist/esm/components/CnStateMachineEditor/CnStateMachineFields.vue3.js.map +1 -0
  77. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js +9 -0
  78. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue.js.map +1 -0
  79. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js +180 -0
  80. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue2.js.map +1 -0
  81. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js +208 -0
  82. package/dist/esm/components/CnStateMachineEditor/CnStateMachineNode.vue3.js.map +1 -0
  83. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js +9 -0
  84. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue.js.map +1 -0
  85. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js +244 -0
  86. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue2.js.map +1 -0
  87. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js +76 -0
  88. package/dist/esm/components/CnStateMachineEditor/CnStateMachinePanel.vue3.js.map +1 -0
  89. package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js +290 -0
  90. package/dist/esm/components/CnStateMachineEditor/stateMachineFields.js.map +1 -0
  91. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +9 -0
  92. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js.map +1 -0
  93. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +448 -0
  94. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -0
  95. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +303 -0
  96. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -0
  97. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +1 -1
  98. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +19 -2
  99. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
  100. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +73 -62
  101. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
  102. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +6 -0
  103. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  104. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +42 -32
  105. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  106. package/dist/esm/composables/indexSources.js +116 -16
  107. package/dist/esm/composables/indexSources.js.map +1 -1
  108. package/dist/esm/index.js +9 -0
  109. package/dist/esm/index.js.map +1 -1
  110. package/dist/esm/l10n/en.json.js +207 -1
  111. package/dist/esm/l10n/en.json.js.map +1 -1
  112. package/dist/esm/l10n/nl.json.js +207 -1
  113. package/dist/esm/l10n/nl.json.js.map +1 -1
  114. package/dist/esm/library-style.js +1 -1
  115. package/dist/esm/nextcloud-vue.css +951 -16
  116. package/dist/esm/utils/aiFeatures.js +245 -0
  117. package/dist/esm/utils/aiFeatures.js.map +1 -0
  118. package/dist/esm/utils/aiSuggestionTracker.js +182 -0
  119. package/dist/esm/utils/aiSuggestionTracker.js.map +1 -0
  120. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  121. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  122. package/dist/esm/utils/objectPermissions.js +552 -0
  123. package/dist/esm/utils/objectPermissions.js.map +1 -0
  124. package/dist/esm/utils/objectTranslations.js +180 -0
  125. package/dist/esm/utils/objectTranslations.js.map +1 -0
  126. package/dist/esm/utils/stateMachineValidation.js +254 -0
  127. package/dist/esm/utils/stateMachineValidation.js.map +1 -0
  128. package/dist/esm/utils/widgetDispatch.js +3 -1
  129. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  130. package/dist/nextcloud-vue.cjs.js +23038 -14567
  131. package/dist/nextcloud-vue.cjs.js.map +2 -2
  132. package/dist/nextcloud-vue.css +951 -16
  133. package/l10n/en.json +207 -1
  134. package/l10n/nl.json +207 -1
  135. package/package.json +1 -1
  136. package/src/components/CnAiFeaturesWidget/CnAiFeaturesWidget.vue +416 -0
  137. package/src/components/CnAiFeaturesWidget/CnAiSuggestionDecision.vue +136 -0
  138. package/src/components/CnAiFeaturesWidget/__tests__/CnAiFeaturesWidget.spec.js +213 -0
  139. package/src/components/CnAiFeaturesWidget/index.js +14 -0
  140. package/src/components/CnIndexPage/CnIndexPage.vue +74 -3
  141. package/src/components/CnIndexPage/useNamedSource.js +9 -0
  142. package/src/components/CnObjectPermissionsTab/CnObjectPermissionsTab.vue +499 -0
  143. package/src/components/CnObjectPermissionsTab/__tests__/CnObjectPermissionsTab.spec.js +186 -0
  144. package/src/components/CnObjectPermissionsTab/index.js +13 -0
  145. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +8 -1
  146. package/src/components/CnObjectTranslationsWidget/CnObjectTranslationsWidget.vue +424 -0
  147. package/src/components/CnObjectTranslationsWidget/__tests__/CnObjectTranslationsWidget.spec.js +189 -0
  148. package/src/components/CnObjectTranslationsWidget/index.js +13 -0
  149. package/src/components/CnReadinessChecklist/CnReadinessChecklist.vue +335 -0
  150. package/src/components/CnReadinessChecklist/index.js +9 -0
  151. package/src/components/CnSchemaRulesWidget/CnSchemaRulesWidget.vue +300 -0
  152. package/src/components/CnSchemaRulesWidget/index.js +10 -0
  153. package/src/components/CnSchemaRulesWidget/schemaRules.js +123 -0
  154. package/src/components/CnSearchIndexStatus/CnSearchIndexStatus.vue +160 -0
  155. package/src/components/CnSearchIndexStatus/index.js +10 -0
  156. package/src/components/CnSearchIndexStatus/searchIndexStatus.js +56 -0
  157. package/src/components/CnShippedConfigurationSection/CnShippedConfigurationSection.vue +328 -0
  158. package/src/components/CnShippedConfigurationSection/index.js +9 -0
  159. package/src/components/CnStateMachineEditor/CnStateMachineEditor.vue +1332 -0
  160. package/src/components/CnStateMachineEditor/CnStateMachineFields.vue +791 -0
  161. package/src/components/CnStateMachineEditor/CnStateMachineNode.vue +442 -0
  162. package/src/components/CnStateMachineEditor/CnStateMachinePanel.vue +326 -0
  163. package/src/components/CnStateMachineEditor/index.js +8 -0
  164. package/src/components/CnStateMachineEditor/stateMachineFields.js +287 -0
  165. package/src/components/CnTimelineWidget/CnTimelineManage.vue +827 -0
  166. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +29 -3
  167. package/src/components/CnWidgetGrid/builtInWidgets.js +6 -0
  168. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +10 -0
  169. package/src/components/index.js +8 -0
  170. package/src/composables/indexSources.js +116 -16
  171. package/src/css/index-page.css +7 -0
  172. package/src/index.js +9 -0
  173. package/src/utils/aiFeatures.js +241 -0
  174. package/src/utils/aiSuggestionTracker.js +178 -0
  175. package/src/utils/libraryWidgetKeys.js +2 -0
  176. package/src/utils/objectPermissions.js +548 -0
  177. package/src/utils/objectTranslations.js +176 -0
  178. package/src/utils/stateMachineValidation.js +250 -0
  179. package/src/utils/widgetDispatch.js +3 -1
@@ -0,0 +1,548 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * Who holds which right on one OpenRegister object, and where each grant came
6
+ * from. The reads behind CnObjectPermissionsTab.
7
+ *
8
+ * Every function here is a read. Nothing decides who may open an object: the
9
+ * grant, its provenance, the deny and the inheritance live in OpenRegister, and
10
+ * these helpers hand on what it answered. A deny is never subtracted from a
11
+ * grant, a missing answer is never filled with a default, and nothing is
12
+ * cached: a copy of an access decision is a second decision nobody updates.
13
+ *
14
+ * The object's own permission set (`/permissions`) is preferred; an older
15
+ * OpenRegister answers 404 to it, and then five reads are the fallback:
16
+ *
17
+ * GET {apiBase}/permissions the verb catalogue + deny mode
18
+ * GET {apiBase}/objects/{register}/{schema}/{id}/shares grants written on the object
19
+ * GET {apiBase}/permissions/compare-roles?register= each role and its verbs
20
+ * GET {apiBase}/scopes?register=&schema= the caller's own verdict + provenance
21
+ * GET {apiBase}/permissions/deny-preview?register=&schema= the deny rules as written
22
+ *
23
+ * plus the history read `{...}/{id}/permissions/history?at=` for "who held a
24
+ * right on a date".
25
+ *
26
+ * A failed read answers `null`, never `[]`: "we could not ask" and "nobody
27
+ * holds this" are opposite answers that would otherwise render as the same
28
+ * empty table.
29
+ */
30
+ import axios from '@nextcloud/axios'
31
+ import { translate as t } from '@nextcloud/l10n'
32
+ import { prefixUrl } from './headers.js'
33
+
34
+ /** The OpenRegister API root a target defaults to. */
35
+ export const DEFAULT_API_BASE = '/apps/openregister/api'
36
+
37
+ /**
38
+ * Normalise a target to `{register, schema, apiBase}`.
39
+ *
40
+ * @param {object} target The object's register, schema and optional apiBase.
41
+ * @return {{register: string, schema: string, apiBase: string}} The target.
42
+ */
43
+ function normalise(target = {}) {
44
+ return {
45
+ register: String(target.register || ''),
46
+ schema: String(target.schema || ''),
47
+ apiBase: String(target.apiBase || DEFAULT_API_BASE).replace(/\/$/, ''),
48
+ }
49
+ }
50
+
51
+ /**
52
+ * GET one path, keeping the status beside the body.
53
+ *
54
+ * The status is part of the answer: a 403 on the access set means the reader
55
+ * may open the object and may not enumerate who else can, which is not a
56
+ * failed read. A transport failure has no status and reports 0. A non-JSON
57
+ * body (the SPA shell answering 200) is a failed read, not "no rules".
58
+ *
59
+ * @param {string} url App-absolute URL.
60
+ * @param {object} params Query parameters.
61
+ * @return {Promise<{status: number, data: object|null}>} The status and the body.
62
+ */
63
+ async function readResult(url, params = {}) {
64
+ try {
65
+ const response = await axios.get(prefixUrl(url), { params })
66
+ if (typeof response?.data !== 'object' || response.data === null) {
67
+ return { status: response?.status || 0, data: null }
68
+ }
69
+ return { status: response.status || 200, data: response.data }
70
+ } catch (error) {
71
+ return { status: error?.response?.status || 0, data: null }
72
+ }
73
+ }
74
+
75
+ /**
76
+ * GET one path, answering null when it cannot be read.
77
+ *
78
+ * @param {string} url App-absolute URL.
79
+ * @param {object} params Query parameters.
80
+ * @return {Promise<object|null>} The body, or null.
81
+ */
82
+ async function read(url, params = {}) {
83
+ const { data } = await readResult(url, params)
84
+ return data
85
+ }
86
+
87
+ /**
88
+ * The URL of one object, under its target.
89
+ *
90
+ * @param {object} target The target.
91
+ * @param {string} objectId The object id.
92
+ * @param {string} [schema] A schema other than the target's.
93
+ * @return {string} The URL.
94
+ */
95
+ function objectUrl(target, objectId, schema) {
96
+ const { register, apiBase } = normalise(target)
97
+ return `${apiBase}/objects/${register}/${schema || normalise(target).schema}/${objectId}`
98
+ }
99
+
100
+ /**
101
+ * The grantable verbs and the deny enforcement mode.
102
+ *
103
+ * @param {object} target The target (only `apiBase` is read).
104
+ * @return {Promise<object|null>} `{permissions, denyEnforcement}`, or null.
105
+ */
106
+ export function fetchPermissionCatalogue(target) {
107
+ return read(`${normalise(target).apiBase}/permissions`)
108
+ }
109
+
110
+ /**
111
+ * The grants written on one object.
112
+ *
113
+ * @param {object} target The target.
114
+ * @param {string} objectId The object id.
115
+ * @return {Promise<Array|null>} The grants, or null.
116
+ */
117
+ export async function fetchObjectGrants(target, objectId) {
118
+ const body = await read(`${objectUrl(target, objectId)}/shares`)
119
+ if (body === null || !Array.isArray(body.results)) {
120
+ return null
121
+ }
122
+ return body.results
123
+ }
124
+
125
+ /**
126
+ * Each role in the register and the verbs it holds.
127
+ *
128
+ * @param {object} target The target.
129
+ * @return {Promise<object|null>} `{roles}`, or null.
130
+ */
131
+ export function fetchRoleGrants(target) {
132
+ const { apiBase, register } = normalise(target)
133
+ return read(`${apiBase}/permissions/compare-roles`, { register })
134
+ }
135
+
136
+ /**
137
+ * The caller's own effective verbs on the schema, with their provenance.
138
+ *
139
+ * The endpoint answers a matrix even when narrowed, so the row is picked by
140
+ * schema name rather than by position.
141
+ *
142
+ * @param {object} target The target.
143
+ * @return {Promise<object|null>} The scope entry, or null.
144
+ */
145
+ export async function fetchCallerScope(target) {
146
+ const { apiBase, register, schema } = normalise(target)
147
+ const body = await read(`${apiBase}/scopes`, { register, schema })
148
+ if (body === null || !Array.isArray(body.scopes)) {
149
+ return null
150
+ }
151
+ const scope = body.scopes.find((entry) => entry?.schema === schema)
152
+ if (!scope) {
153
+ return null
154
+ }
155
+ return { user: body.user, isAdmin: body.isAdmin, groups: body.groups, ...scope }
156
+ }
157
+
158
+ /**
159
+ * The deny rules as written for the register and schema.
160
+ *
161
+ * @param {object} target The target.
162
+ * @return {Promise<object|null>} `{denyEnforcement, enforcing, rules}`, or null.
163
+ */
164
+ export function fetchDenyRules(target) {
165
+ const { apiBase, register, schema } = normalise(target)
166
+ return read(`${apiBase}/permissions/deny-preview`, { register, schema })
167
+ }
168
+
169
+ /**
170
+ * The permission set OpenRegister publishes for this object, with its status.
171
+ *
172
+ * @param {object} target The target.
173
+ * @param {string} objectId The object id.
174
+ * @return {Promise<{status: number, set: object|null}>} The status and the set.
175
+ */
176
+ export async function fetchObjectPermissions(target, objectId) {
177
+ const { status, data } = await readResult(`${objectUrl(target, objectId)}/permissions`)
178
+ if (data === null || Array.isArray(data.holders) === false) {
179
+ return { status, set: null }
180
+ }
181
+ return { status, set: data }
182
+ }
183
+
184
+ /**
185
+ * The set as it stood at a moment, from the object's audit trail.
186
+ *
187
+ * @param {object} target The target.
188
+ * @param {string} objectId The object id.
189
+ * @param {string} at An ISO-8601 moment.
190
+ * @return {Promise<{status: number, history: object|null}>} The status and the history.
191
+ */
192
+ export async function fetchAccessHistory(target, objectId, at) {
193
+ const { status, data } = await readResult(`${objectUrl(target, objectId)}/permissions/history`, { at })
194
+ if (data === null || Array.isArray(data.changes) === false) {
195
+ return { status, history: null }
196
+ }
197
+ return { status, history: data }
198
+ }
199
+
200
+ /**
201
+ * The end and the area written on one rule, as OpenRegister reported them.
202
+ *
203
+ * Neither is compared to anything: whether an expired grant still answers is
204
+ * resolved in OpenRegister. A role grant carries the whole holder list, so the
205
+ * entry naming this principal is picked out of it.
206
+ *
207
+ * @param {object|Array|string|null} rule The rule as written.
208
+ * @param {string} principal The holder of the row.
209
+ * @return {{until: string, scopedTo: object|null}} The end and the area.
210
+ */
211
+ export function constraintsOf(rule, principal) {
212
+ const none = { until: '', scopedTo: null }
213
+ if (rule === null || typeof rule !== 'object') {
214
+ return none
215
+ }
216
+
217
+ let entry = rule
218
+ if (Array.isArray(rule)) {
219
+ entry = rule.find((candidate) => candidate === principal
220
+ || (candidate !== null
221
+ && typeof candidate === 'object'
222
+ && [candidate.principal, candidate.group, candidate.role, candidate.user].includes(principal)))
223
+ }
224
+
225
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
226
+ return none
227
+ }
228
+
229
+ return {
230
+ until: typeof entry.until === 'string' ? entry.until : '',
231
+ scopedTo: entry.scopedTo || null,
232
+ }
233
+ }
234
+
235
+ /**
236
+ * One reported rule as a row.
237
+ *
238
+ * @param {object} rule The rule.
239
+ * @param {boolean} refusing Whether the rule takes a right away.
240
+ * @param {boolean} enforcing Whether refusals are in force.
241
+ * @return {object} The row.
242
+ */
243
+ function ruleRow(rule, refusing, enforcing) {
244
+ const principal = rule?.principal || ''
245
+ const { until, scopedTo } = constraintsOf(rule?.rule ?? null, principal)
246
+
247
+ let source = rule?.level || 'object'
248
+ if (refusing === true) {
249
+ source = enforcing === true ? 'deny' : 'staged-deny'
250
+ }
251
+
252
+ return {
253
+ holder: principal,
254
+ right: rule?.action || '',
255
+ source,
256
+ detail: rule?.role || '',
257
+ inheritedFrom: rule?.inheritedFrom || '',
258
+ level: rule?.level || '',
259
+ role: rule?.role || '',
260
+ declared: rule?.declared !== false,
261
+ conditional: rule?.conditional === true,
262
+ until,
263
+ scopedTo,
264
+ }
265
+ }
266
+
267
+ /**
268
+ * The object's own permission set as rows. A deny is its own row.
269
+ *
270
+ * @param {object|null} set The set from `fetchObjectPermissions`.
271
+ * @return {Array<object>} The rows.
272
+ */
273
+ export function objectPermissionRows(set) {
274
+ if (set === null || typeof set !== 'object') {
275
+ return []
276
+ }
277
+
278
+ const enforcing = set.denyEnforcement === 'enforcing'
279
+ const rows = []
280
+ for (const holder of set.holders || []) {
281
+ for (const rule of holder?.rules || []) {
282
+ rows.push(ruleRow(rule, false, enforcing))
283
+ }
284
+ }
285
+ for (const rule of set.denied || []) {
286
+ rows.push(ruleRow(rule, true, enforcing))
287
+ }
288
+ return rows
289
+ }
290
+
291
+ /**
292
+ * The set as it stood at the moment asked about.
293
+ *
294
+ * `asOf: null` (the trail does not reach back that far) is "unanswered", never
295
+ * an empty table: an empty table would claim nobody held a right that day.
296
+ *
297
+ * @param {object|null} history The history from `fetchAccessHistory`.
298
+ * @return {{answered: boolean, at: string, rows: Array<object>, setBy: string, changedAfterwardsBy: object|null}} The answer.
299
+ */
300
+ export function asOfRows(history) {
301
+ const unanswered = { answered: false, at: history?.at || '', rows: [], setBy: '', changedAfterwardsBy: null }
302
+
303
+ const asOf = history?.asOf || null
304
+ if (asOf === null || typeof asOf !== 'object') {
305
+ return unanswered
306
+ }
307
+
308
+ const rows = []
309
+ for (const holder of asOf.holders || []) {
310
+ for (const rule of holder?.rules || []) {
311
+ rows.push(ruleRow(rule, false, false))
312
+ }
313
+ }
314
+
315
+ return {
316
+ answered: true,
317
+ at: asOf.at || history?.at || '',
318
+ rows,
319
+ setBy: asOf.setBy || '',
320
+ changedAfterwardsBy: asOf.changedAfterwardsBy || null,
321
+ }
322
+ }
323
+
324
+ /**
325
+ * All answers as one list of rows. A renderer, not an evaluator: each row
326
+ * restates ONE reported rule, and no row is the product of two.
327
+ *
328
+ * When the object answered for itself the share, role and deny reads are not
329
+ * replayed on top (the same grant would be counted twice). The caller's own
330
+ * verdict always rides along last, because it is the one answer about this
331
+ * reader rather than a rule about everybody.
332
+ *
333
+ * @param {object} answers The reads.
334
+ * @param {object|null} answers.objectPermissions The object's own set.
335
+ * @param {Array|null} answers.objectGrants Grants on the object.
336
+ * @param {object|null} answers.roleGrants Roles and their verbs.
337
+ * @param {object|null} answers.callerScope The caller's own verdict.
338
+ * @param {object|null} answers.denyRules The deny rules.
339
+ * @return {Array<object>} The rows.
340
+ */
341
+ export function grantRows({ objectPermissions = null, objectGrants, roleGrants, callerScope, denyRules }) {
342
+ const rows = []
343
+
344
+ if (objectPermissions !== null) {
345
+ rows.push(...objectPermissionRows(objectPermissions))
346
+ }
347
+
348
+ for (const grant of (objectPermissions === null ? objectGrants : null) || []) {
349
+ for (const right of grant?.actions || []) {
350
+ rows.push({
351
+ holder: grant.principal || grant.userId || grant.groupId || '',
352
+ right,
353
+ // `inheritedFrom` is the id of the ancestor, not a source key.
354
+ source: grant.source || (grant.inheritedFrom ? 'inherited' : 'share'),
355
+ inheritedFrom: grant.inheritedFrom || '',
356
+ detail: grant.expires ? String(grant.expires) : '',
357
+ })
358
+ }
359
+ }
360
+
361
+ for (const role of (objectPermissions === null ? roleGrants?.roles : null) || []) {
362
+ for (const right of role?.actions || []) {
363
+ rows.push({ holder: role.role || '', right, source: 'role', detail: '' })
364
+ }
365
+ }
366
+
367
+ for (const rule of (objectPermissions === null ? denyRules?.rules : null) || []) {
368
+ rows.push({
369
+ holder: rule.principal || '',
370
+ right: rule.action || '',
371
+ source: denyRules.enforcing ? 'deny' : 'staged-deny',
372
+ detail: rule.level ? `${rule.level}: ${rule.subject || ''}` : '',
373
+ })
374
+ }
375
+
376
+ for (const [right, record] of Object.entries(callerScope?.provenance || {})) {
377
+ rows.push({
378
+ holder: callerScope.user || '',
379
+ right,
380
+ source: record?.source || 'unknown',
381
+ granted: record?.granted,
382
+ detail: record?.role || record?.principal || '',
383
+ stagedDeny: record?.stagedDeny || null,
384
+ })
385
+ }
386
+
387
+ return rows
388
+ }
389
+
390
+ /**
391
+ * The id a reference carries, as a uuid string or as an expanded object.
392
+ *
393
+ * @param {unknown} value The reference.
394
+ * @return {string} The id, or ''.
395
+ */
396
+ function referenceId(value) {
397
+ if (value && typeof value === 'object') {
398
+ return String(value['@self']?.id ?? value.id ?? value.uuid ?? '')
399
+ }
400
+ return String(value ?? '')
401
+ }
402
+
403
+ /**
404
+ * The object's decided field rules, and the per-role field rules its TYPE
405
+ * object declares.
406
+ *
407
+ * Two reads, because they answer different questions: `@self.fieldRules` is
408
+ * what OpenRegister decided for THIS reader, the declaration on the type
409
+ * object is the rule behind it. Neither is evaluated here.
410
+ *
411
+ * @param {object} target The target.
412
+ * @param {string} objectId The object id.
413
+ * @param {object} config Where the declaration lives.
414
+ * @param {string} config.typeField Field on the object referencing its type object.
415
+ * @param {string} config.typeSchema Schema of the type object (same register).
416
+ * @param {string} [config.declarationField] Field on the type object holding the rules.
417
+ * @return {Promise<{decided: object|null, declared: Array<object>|null}>} The answer.
418
+ */
419
+ export async function fetchFieldRoleRules(target, objectId, config) {
420
+ if (!objectId || !config?.typeField || !config?.typeSchema) {
421
+ return { decided: null, declared: null }
422
+ }
423
+
424
+ const object = await read(objectUrl(target, objectId))
425
+ if (object === null) {
426
+ return { decided: null, declared: null }
427
+ }
428
+
429
+ const decided = object['@self']?.fieldRules ?? null
430
+ const typeId = referenceId(object[config.typeField])
431
+ if (!typeId) {
432
+ return { decided, declared: null }
433
+ }
434
+
435
+ const type = await read(objectUrl(target, typeId, config.typeSchema))
436
+ if (type === null) {
437
+ return { decided, declared: null }
438
+ }
439
+
440
+ const declared = type[config.declarationField || 'fieldRoleRules']
441
+ return { decided, declared: Array.isArray(declared) ? declared : [] }
442
+ }
443
+
444
+ /**
445
+ * The two things a role rule can do to a field. The spelling is OpenRegister's
446
+ * contract (`hidden`, `readOnly`); any other rule is dropped.
447
+ */
448
+ export const ROLE_RULES = ['hidden', 'readOnly']
449
+
450
+ /**
451
+ * The reader's word for each field rule.
452
+ *
453
+ * @return {object} The labels, by rule id.
454
+ */
455
+ export function roleRuleLabels() {
456
+ return {
457
+ hidden: t('nextcloud-vue', 'Not shown'),
458
+ readOnly: t('nextcloud-vue', 'Cannot be changed'),
459
+ }
460
+ }
461
+
462
+ /**
463
+ * A declared list of names, blanks and repeats removed.
464
+ *
465
+ * @param {unknown} value The list.
466
+ * @return {Array<string>} The names.
467
+ */
468
+ function names(value) {
469
+ if (Array.isArray(value) === false) {
470
+ return []
471
+ }
472
+ const found = []
473
+ value.forEach((entry) => {
474
+ const name = String(entry ?? '').trim()
475
+ if (name !== '' && found.includes(name) === false) {
476
+ found.push(name)
477
+ }
478
+ })
479
+ return found
480
+ }
481
+
482
+ /**
483
+ * Whether OpenRegister applied this rule to the reader. False when the object
484
+ * carries no decision at all (an older OpenRegister), never invented.
485
+ *
486
+ * @param {string} field The field.
487
+ * @param {string} rule The rule.
488
+ * @param {object|null} decided The object's `@self.fieldRules`.
489
+ * @return {boolean} True when the reader met this rule.
490
+ */
491
+ function appliesToMe(field, rule, decided) {
492
+ const applied = decided?.[rule]
493
+ if (Array.isArray(applied) === false) {
494
+ return false
495
+ }
496
+ return applied.map((name) => String(name)).includes(field)
497
+ }
498
+
499
+ /**
500
+ * The declared field rules as rows. A row naming no field, or an unknown rule,
501
+ * is dropped rather than rendered blank.
502
+ *
503
+ * @param {Array<object>|null} declared The declaration.
504
+ * @param {object|null} decided The object's `@self.fieldRules`.
505
+ * @return {Array<object>} The rows.
506
+ */
507
+ export function fieldRoleRows(declared, decided) {
508
+ if (Array.isArray(declared) === false) {
509
+ return []
510
+ }
511
+
512
+ return declared
513
+ .filter((rule) => rule && ROLE_RULES.includes(rule.rule) && typeof rule.field === 'string' && rule.field.trim() !== '')
514
+ .map((rule) => {
515
+ const field = rule.field.trim()
516
+ return {
517
+ field,
518
+ rule: rule.rule,
519
+ groups: names(rule.groups),
520
+ heldBy: names(rule.heldBy),
521
+ reason: String(rule.reason ?? '').trim(),
522
+ appliesToMe: appliesToMe(field, rule.rule, decided),
523
+ }
524
+ })
525
+ }
526
+
527
+ /**
528
+ * One field-rule row as a whole sentence (whole sentences, so a translator can
529
+ * reorder the groups).
530
+ *
531
+ * @param {object} row One row from `fieldRoleRows`.
532
+ * @return {string} The sentence.
533
+ */
534
+ export function roleRuleSentence(row) {
535
+ const groups = row?.groups?.join(', ') ?? ''
536
+ const heldBy = row?.heldBy?.join(', ') ?? ''
537
+
538
+ if (row?.rule === 'hidden' && heldBy !== '') {
539
+ return t('nextcloud-vue', '{groups} do not see this field. {heldBy} do.', { groups, heldBy })
540
+ }
541
+ if (row?.rule === 'hidden') {
542
+ return t('nextcloud-vue', '{groups} do not see this field.', { groups })
543
+ }
544
+ if (heldBy !== '') {
545
+ return t('nextcloud-vue', '{groups} cannot change this field. {heldBy} can.', { groups, heldBy })
546
+ }
547
+ return t('nextcloud-vue', '{groups} cannot change this field.', { groups })
548
+ }