@conduction/nextcloud-vue 0.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/nextcloud-vue.cjs.js +10710 -0
- package/dist/nextcloud-vue.cjs.js.map +1 -0
- package/dist/nextcloud-vue.css +803 -0
- package/dist/nextcloud-vue.esm.js +10665 -0
- package/dist/nextcloud-vue.esm.js.map +1 -0
- package/package.json +63 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +152 -0
- package/src/components/CnCardGrid/index.js +1 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -0
- package/src/components/CnCellRenderer/index.js +1 -0
- package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -0
- package/src/components/CnConfigurationCard/index.js +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +354 -0
- package/src/components/CnDataTable/index.js +1 -0
- package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +88 -0
- package/src/components/CnDetailViewLayout/index.js +1 -0
- package/src/components/CnEmptyState/CnEmptyState.vue +78 -0
- package/src/components/CnEmptyState/index.js +1 -0
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +223 -0
- package/src/components/CnFacetSidebar/index.js +1 -0
- package/src/components/CnFilterBar/CnFilterBar.vue +152 -0
- package/src/components/CnFilterBar/index.js +1 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +682 -0
- package/src/components/CnIndexPage/index.js +1 -0
- package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -0
- package/src/components/CnKpiGrid/index.js +1 -0
- package/src/components/CnListViewLayout/CnListViewLayout.vue +80 -0
- package/src/components/CnListViewLayout/index.js +1 -0
- package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -0
- package/src/components/CnMassActionBar/index.js +1 -0
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -0
- package/src/components/CnMassCopyDialog/index.js +1 -0
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -0
- package/src/components/CnMassDeleteDialog/index.js +1 -0
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -0
- package/src/components/CnMassExportDialog/index.js +1 -0
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -0
- package/src/components/CnMassImportDialog/index.js +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +292 -0
- package/src/components/CnObjectCard/index.js +1 -0
- package/src/components/CnPagination/CnPagination.vue +252 -0
- package/src/components/CnPagination/index.js +1 -0
- package/src/components/CnRowActions/CnRowActions.vue +73 -0
- package/src/components/CnRowActions/index.js +1 -0
- package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -0
- package/src/components/CnSettingsCard/index.js +1 -0
- package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -0
- package/src/components/CnSettingsSection/index.js +1 -0
- package/src/components/CnStatsBlock/CnStatsBlock.vue +366 -0
- package/src/components/CnStatsBlock/index.js +1 -0
- package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -0
- package/src/components/CnStatusBadge/index.js +1 -0
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -0
- package/src/components/CnVersionInfoCard/index.js +1 -0
- package/src/components/CnViewModeToggle/CnViewModeToggle.vue +77 -0
- package/src/components/CnViewModeToggle/index.js +1 -0
- package/src/components/index.js +25 -0
- package/src/composables/index.js +3 -0
- package/src/composables/useDetailView.js +132 -0
- package/src/composables/useListView.js +153 -0
- package/src/composables/useSubResource.js +142 -0
- package/src/css/badge.css +51 -0
- package/src/css/card.css +128 -0
- package/src/css/detail.css +68 -0
- package/src/css/index.css +8 -0
- package/src/css/layout.css +90 -0
- package/src/css/pagination.css +72 -0
- package/src/css/table.css +143 -0
- package/src/css/utilities.css +46 -0
- package/src/index.js +50 -0
- package/src/store/createSubResourcePlugin.js +135 -0
- package/src/store/index.js +3 -0
- package/src/store/plugins/auditTrails.js +17 -0
- package/src/store/plugins/files.js +186 -0
- package/src/store/plugins/index.js +4 -0
- package/src/store/plugins/lifecycle.js +180 -0
- package/src/store/plugins/relations.js +68 -0
- package/src/store/useObjectStore.js +625 -0
- package/src/types/auditTrail.d.ts +32 -0
- package/src/types/file.d.ts +23 -0
- package/src/types/index.d.ts +35 -0
- package/src/types/notification.d.ts +36 -0
- package/src/types/object.d.ts +40 -0
- package/src/types/organisation.d.ts +41 -0
- package/src/types/register.d.ts +25 -0
- package/src/types/schema.d.ts +39 -0
- package/src/types/shared.d.ts +79 -0
- package/src/types/source.d.ts +14 -0
- package/src/types/task.d.ts +31 -0
- package/src/utils/errors.js +96 -0
- package/src/utils/headers.js +44 -0
- package/src/utils/index.js +3 -0
- package/src/utils/schema.js +287 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { buildHeaders, buildQueryString } from '../utils/headers.js'
|
|
3
|
+
import { parseResponseError, networkError, genericError } from '../utils/errors.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generic Pinia store for OpenRegister object CRUD operations.
|
|
7
|
+
*
|
|
8
|
+
* Provides a unified interface for managing objects across registers and schemas.
|
|
9
|
+
* Apps register their object types with schema/register IDs, then use type slugs
|
|
10
|
+
* for all operations. Supports plugins for sub-resources (files, audit trails, etc.).
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Basic usage (CRUD only)
|
|
14
|
+
* import { useObjectStore } from '@conduction/nextcloud-vue'
|
|
15
|
+
* const store = useObjectStore()
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* // With plugins
|
|
19
|
+
* import { createObjectStore, filesPlugin, auditTrailsPlugin } from '@conduction/nextcloud-vue'
|
|
20
|
+
* const useMyStore = createObjectStore('object', {
|
|
21
|
+
* plugins: [filesPlugin(), auditTrailsPlugin()],
|
|
22
|
+
* })
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const DEFAULT_STORE_ID = 'conduction-objects'
|
|
26
|
+
const DEFAULT_BASE_URL = '/apps/openregister/api/objects'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Capitalize the first letter of a string.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} str Input string
|
|
32
|
+
* @return {string} Capitalized string
|
|
33
|
+
*/
|
|
34
|
+
function capitalize(str) {
|
|
35
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Merge plugin state factories into a single state object.
|
|
40
|
+
*
|
|
41
|
+
* @param {Array} plugins Array of plugin definitions
|
|
42
|
+
* @return {object} Merged state object
|
|
43
|
+
*/
|
|
44
|
+
function mergePluginState(plugins) {
|
|
45
|
+
const merged = {}
|
|
46
|
+
for (const plugin of plugins) {
|
|
47
|
+
if (plugin.state) {
|
|
48
|
+
Object.assign(merged, plugin.state())
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return merged
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Merge plugin getters into a single getters object.
|
|
56
|
+
*
|
|
57
|
+
* @param {Array} plugins Array of plugin definitions
|
|
58
|
+
* @return {object} Merged getters object
|
|
59
|
+
*/
|
|
60
|
+
function mergePluginGetters(plugins) {
|
|
61
|
+
const merged = {}
|
|
62
|
+
for (const plugin of plugins) {
|
|
63
|
+
if (plugin.getters) {
|
|
64
|
+
Object.assign(merged, plugin.getters)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return merged
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Merge plugin actions into a single actions object.
|
|
72
|
+
*
|
|
73
|
+
* @param {Array} plugins Array of plugin definitions
|
|
74
|
+
* @return {object} Merged actions object
|
|
75
|
+
*/
|
|
76
|
+
function mergePluginActions(plugins) {
|
|
77
|
+
const merged = {}
|
|
78
|
+
for (const plugin of plugins) {
|
|
79
|
+
if (plugin.actions) {
|
|
80
|
+
Object.assign(merged, plugin.actions)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return merged
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Base state ──────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
function baseState() {
|
|
89
|
+
return {
|
|
90
|
+
/** @type {Object<string, {schema: string, register: string}>} */
|
|
91
|
+
objectTypeRegistry: {},
|
|
92
|
+
/** @type {Object<string, Array>} */
|
|
93
|
+
collections: {},
|
|
94
|
+
/** @type {Object<string, Object<string, object>>} */
|
|
95
|
+
objects: {},
|
|
96
|
+
/** @type {Object<string, boolean>} */
|
|
97
|
+
loading: {},
|
|
98
|
+
/** @type {Object<string, import('../utils/errors.js').ApiError|null>} */
|
|
99
|
+
errors: {},
|
|
100
|
+
/** @type {Object<string, {total: number, page: number, pages: number, limit: number}>} */
|
|
101
|
+
pagination: {},
|
|
102
|
+
/** @type {Object<string, string>} */
|
|
103
|
+
searchTerms: {},
|
|
104
|
+
/** @type {Object<string, object|null>} */
|
|
105
|
+
schemas: {},
|
|
106
|
+
/** @type {{baseUrl: string}} */
|
|
107
|
+
_options: {
|
|
108
|
+
baseUrl: DEFAULT_BASE_URL,
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Base getters ────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
const baseGetters = {
|
|
116
|
+
/**
|
|
117
|
+
* Get all registered object type slugs.
|
|
118
|
+
* @return {string[]}
|
|
119
|
+
*/
|
|
120
|
+
objectTypes: (state) => Object.keys(state.objectTypeRegistry),
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get the collection array for a type.
|
|
124
|
+
* @return {Function} (type: string) => Array
|
|
125
|
+
*/
|
|
126
|
+
getCollection: (state) => (type) => state.collections[type] || [],
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get a single cached object by type and ID.
|
|
130
|
+
* @return {Function} (type: string, id: string) => object|null
|
|
131
|
+
*/
|
|
132
|
+
getObject: (state) => (type, id) => state.objects[type]?.[id] || null,
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Alias for getObject — check cache without fetching.
|
|
136
|
+
* @return {Function} (type: string, id: string) => object|null
|
|
137
|
+
*/
|
|
138
|
+
getCachedObject: (state) => (type, id) => state.objects[type]?.[id] || null,
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if a type is currently loading.
|
|
142
|
+
* @return {Function} (type: string) => boolean
|
|
143
|
+
*/
|
|
144
|
+
isLoading: (state) => (type) => state.loading[type] || false,
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get the current error for a type.
|
|
148
|
+
* @return {Function} (type: string) => ApiError|null
|
|
149
|
+
*/
|
|
150
|
+
getError: (state) => (type) => state.errors[type] || null,
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get pagination state for a type.
|
|
154
|
+
* @return {Function} (type: string) => {total, page, pages, limit}
|
|
155
|
+
*/
|
|
156
|
+
getPagination: (state) => (type) =>
|
|
157
|
+
state.pagination[type] || { total: 0, page: 1, pages: 1, limit: 20 },
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get the current search term for a type.
|
|
161
|
+
* @return {Function} (type: string) => string
|
|
162
|
+
*/
|
|
163
|
+
getSearchTerm: (state) => (type) => state.searchTerms[type] || '',
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get a cached schema for a type.
|
|
167
|
+
* @return {Function} (type: string) => object|null
|
|
168
|
+
*/
|
|
169
|
+
getSchema: (state) => (type) => state.schemas[type] || null,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ── Base actions ────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
const baseActions = {
|
|
175
|
+
/**
|
|
176
|
+
* Configure the store with custom options.
|
|
177
|
+
* Call once before using the store if you need a custom base URL.
|
|
178
|
+
*
|
|
179
|
+
* @param {object} options Configuration options
|
|
180
|
+
* @param {string} [options.baseUrl] Custom base URL for API calls
|
|
181
|
+
*/
|
|
182
|
+
configure(options) {
|
|
183
|
+
Object.assign(this._options, options)
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Register an object type for CRUD operations.
|
|
188
|
+
*
|
|
189
|
+
* @param {string} slug Short name for the type (e.g. 'client', 'case')
|
|
190
|
+
* @param {string} schemaId OpenRegister schema ID
|
|
191
|
+
* @param {string} registerId OpenRegister register ID
|
|
192
|
+
*/
|
|
193
|
+
registerObjectType(slug, schemaId, registerId) {
|
|
194
|
+
this.objectTypeRegistry[slug] = { schema: schemaId, register: registerId }
|
|
195
|
+
this.collections[slug] = []
|
|
196
|
+
this.objects[slug] = {}
|
|
197
|
+
this.loading[slug] = false
|
|
198
|
+
this.errors[slug] = null
|
|
199
|
+
this.pagination[slug] = { total: 0, page: 1, pages: 1, limit: 20 }
|
|
200
|
+
this.searchTerms[slug] = ''
|
|
201
|
+
this.schemas[slug] = null
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Unregister an object type and clean up all its state.
|
|
206
|
+
*
|
|
207
|
+
* @param {string} slug The type slug to unregister
|
|
208
|
+
*/
|
|
209
|
+
unregisterObjectType(slug) {
|
|
210
|
+
delete this.objectTypeRegistry[slug]
|
|
211
|
+
delete this.collections[slug]
|
|
212
|
+
delete this.objects[slug]
|
|
213
|
+
delete this.loading[slug]
|
|
214
|
+
delete this.errors[slug]
|
|
215
|
+
delete this.pagination[slug]
|
|
216
|
+
delete this.searchTerms[slug]
|
|
217
|
+
delete this.schemas[slug]
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Get the type config or throw if not registered.
|
|
222
|
+
*
|
|
223
|
+
* @param {string} type The type slug
|
|
224
|
+
* @return {{schema: string, register: string}} Type configuration
|
|
225
|
+
* @throws {Error} If the type is not registered
|
|
226
|
+
*/
|
|
227
|
+
_getTypeConfig(type) {
|
|
228
|
+
const config = this.objectTypeRegistry[type]
|
|
229
|
+
if (!config) {
|
|
230
|
+
throw new Error(`Object type "${type}" is not registered in the store. Call registerObjectType('${type}', schemaId, registerId) first.`)
|
|
231
|
+
}
|
|
232
|
+
return config
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Build the API URL for a type and optional object ID.
|
|
237
|
+
*
|
|
238
|
+
* @param {string} type The type slug
|
|
239
|
+
* @param {string|null} [id=null] Optional object ID
|
|
240
|
+
* @return {string} Full API URL path
|
|
241
|
+
*/
|
|
242
|
+
_buildUrl(type, id = null) {
|
|
243
|
+
const config = this._getTypeConfig(type)
|
|
244
|
+
let url = `${this._options.baseUrl}/${config.register}/${config.schema}`
|
|
245
|
+
if (id) {
|
|
246
|
+
url += `/${id}`
|
|
247
|
+
}
|
|
248
|
+
return url
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Clear the error state for a type.
|
|
253
|
+
*
|
|
254
|
+
* @param {string} type The type slug
|
|
255
|
+
*/
|
|
256
|
+
clearError(type) {
|
|
257
|
+
this.errors[type] = null
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Set the search term for a type.
|
|
262
|
+
*
|
|
263
|
+
* @param {string} type The type slug
|
|
264
|
+
* @param {string} term The search term
|
|
265
|
+
*/
|
|
266
|
+
setSearchTerm(type, term) {
|
|
267
|
+
this.searchTerms[type] = term
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Clear the search term for a type.
|
|
272
|
+
*
|
|
273
|
+
* @param {string} type The type slug
|
|
274
|
+
*/
|
|
275
|
+
clearSearchTerm(type) {
|
|
276
|
+
this.searchTerms[type] = ''
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Fetch the schema definition for a registered type.
|
|
281
|
+
* Uses cache — only fetches once per type per session.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} type The registered type slug
|
|
284
|
+
* @return {Promise<object|null>} The schema object or null on error
|
|
285
|
+
*/
|
|
286
|
+
async fetchSchema(type) {
|
|
287
|
+
const config = this._getTypeConfig(type)
|
|
288
|
+
|
|
289
|
+
if (this.schemas[type]) {
|
|
290
|
+
return this.schemas[type]
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const response = await fetch(
|
|
295
|
+
`/apps/openregister/api/schemas/${config.schema}`,
|
|
296
|
+
{ method: 'GET', headers: buildHeaders() },
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
if (!response.ok) return null
|
|
300
|
+
|
|
301
|
+
const schema = await response.json()
|
|
302
|
+
this.schemas[type] = schema
|
|
303
|
+
return schema
|
|
304
|
+
} catch {
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
},
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Fetch a collection of objects for a registered type.
|
|
311
|
+
*
|
|
312
|
+
* @param {string} type The registered type slug
|
|
313
|
+
* @param {object} [params={}] Query parameters (_limit, _page, _search, _order, filters)
|
|
314
|
+
* @return {Promise<Array>} The fetched collection (also stored in state)
|
|
315
|
+
*/
|
|
316
|
+
async fetchCollection(type, params = {}) {
|
|
317
|
+
this.loading[type] = true
|
|
318
|
+
this.errors[type] = null
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
const url = this._buildUrl(type) + buildQueryString(params)
|
|
322
|
+
|
|
323
|
+
const response = await fetch(url, {
|
|
324
|
+
method: 'GET',
|
|
325
|
+
headers: buildHeaders(),
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
if (!response.ok) {
|
|
329
|
+
this.errors[type] = await parseResponseError(response, type)
|
|
330
|
+
console.error(`Error fetching ${type} collection:`, this.errors[type])
|
|
331
|
+
return []
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const data = await response.json()
|
|
335
|
+
|
|
336
|
+
this.collections[type] = data.results || data
|
|
337
|
+
this.pagination[type] = {
|
|
338
|
+
total: data.total || (data.results || data).length,
|
|
339
|
+
page: data.page || 1,
|
|
340
|
+
pages: data.pages || 1,
|
|
341
|
+
limit: params._limit || 20,
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return this.collections[type]
|
|
345
|
+
} catch (error) {
|
|
346
|
+
this.errors[type] = error.name === 'TypeError'
|
|
347
|
+
? networkError(error)
|
|
348
|
+
: genericError(error)
|
|
349
|
+
console.error(`Error fetching ${type} collection:`, error)
|
|
350
|
+
return []
|
|
351
|
+
} finally {
|
|
352
|
+
this.loading[type] = false
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Fetch a single object by type and ID.
|
|
358
|
+
*
|
|
359
|
+
* @param {string} type The registered type slug
|
|
360
|
+
* @param {string} id The object ID or UUID
|
|
361
|
+
* @return {Promise<object|null>} The fetched object (also cached in state)
|
|
362
|
+
*/
|
|
363
|
+
async fetchObject(type, id) {
|
|
364
|
+
this.loading[type] = true
|
|
365
|
+
this.errors[type] = null
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const url = this._buildUrl(type, id)
|
|
369
|
+
|
|
370
|
+
const response = await fetch(url, {
|
|
371
|
+
method: 'GET',
|
|
372
|
+
headers: buildHeaders(),
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
if (!response.ok) {
|
|
376
|
+
this.errors[type] = await parseResponseError(response, type)
|
|
377
|
+
console.error(`Error fetching ${type}/${id}:`, this.errors[type])
|
|
378
|
+
return null
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const data = await response.json()
|
|
382
|
+
|
|
383
|
+
if (!this.objects[type]) {
|
|
384
|
+
this.objects[type] = {}
|
|
385
|
+
}
|
|
386
|
+
this.objects[type][id] = data
|
|
387
|
+
|
|
388
|
+
return data
|
|
389
|
+
} catch (error) {
|
|
390
|
+
this.errors[type] = error.name === 'TypeError'
|
|
391
|
+
? networkError(error)
|
|
392
|
+
: genericError(error)
|
|
393
|
+
console.error(`Error fetching ${type}/${id}:`, error)
|
|
394
|
+
return null
|
|
395
|
+
} finally {
|
|
396
|
+
this.loading[type] = false
|
|
397
|
+
}
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Create or update an object. Uses POST for new objects, PUT for updates.
|
|
402
|
+
*
|
|
403
|
+
* @param {string} type The registered type slug
|
|
404
|
+
* @param {object} objectData The object data (include `id` for updates)
|
|
405
|
+
* @return {Promise<object|null>} The saved object or null on error
|
|
406
|
+
*/
|
|
407
|
+
async saveObject(type, objectData) {
|
|
408
|
+
this.loading[type] = true
|
|
409
|
+
this.errors[type] = null
|
|
410
|
+
|
|
411
|
+
try {
|
|
412
|
+
const isUpdate = !!objectData.id
|
|
413
|
+
const url = isUpdate
|
|
414
|
+
? this._buildUrl(type, objectData.id)
|
|
415
|
+
: this._buildUrl(type)
|
|
416
|
+
const method = isUpdate ? 'PUT' : 'POST'
|
|
417
|
+
|
|
418
|
+
const response = await fetch(url, {
|
|
419
|
+
method,
|
|
420
|
+
headers: buildHeaders(),
|
|
421
|
+
body: JSON.stringify(objectData),
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
if (!response.ok) {
|
|
425
|
+
this.errors[type] = await parseResponseError(response, type)
|
|
426
|
+
console.error(`Error saving ${type}:`, this.errors[type])
|
|
427
|
+
return null
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const data = await response.json()
|
|
431
|
+
|
|
432
|
+
if (!this.objects[type]) {
|
|
433
|
+
this.objects[type] = {}
|
|
434
|
+
}
|
|
435
|
+
const savedId = data.id || objectData.id
|
|
436
|
+
this.objects[type][savedId] = data
|
|
437
|
+
|
|
438
|
+
return data
|
|
439
|
+
} catch (error) {
|
|
440
|
+
this.errors[type] = error.name === 'TypeError'
|
|
441
|
+
? networkError(error)
|
|
442
|
+
: genericError(error)
|
|
443
|
+
console.error(`Error saving ${type}:`, error)
|
|
444
|
+
return null
|
|
445
|
+
} finally {
|
|
446
|
+
this.loading[type] = false
|
|
447
|
+
}
|
|
448
|
+
},
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Delete an object by type and ID.
|
|
452
|
+
*
|
|
453
|
+
* @param {string} type The registered type slug
|
|
454
|
+
* @param {string} id The object ID
|
|
455
|
+
* @return {Promise<boolean>} True if deleted successfully
|
|
456
|
+
*/
|
|
457
|
+
async deleteObject(type, id) {
|
|
458
|
+
this.loading[type] = true
|
|
459
|
+
this.errors[type] = null
|
|
460
|
+
|
|
461
|
+
try {
|
|
462
|
+
const url = this._buildUrl(type, id)
|
|
463
|
+
|
|
464
|
+
const response = await fetch(url, {
|
|
465
|
+
method: 'DELETE',
|
|
466
|
+
headers: buildHeaders(),
|
|
467
|
+
})
|
|
468
|
+
|
|
469
|
+
if (!response.ok) {
|
|
470
|
+
this.errors[type] = await parseResponseError(response, type)
|
|
471
|
+
console.error(`Error deleting ${type}/${id}:`, this.errors[type])
|
|
472
|
+
return false
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (this.objects[type]) {
|
|
476
|
+
delete this.objects[type][id]
|
|
477
|
+
}
|
|
478
|
+
if (this.collections[type]) {
|
|
479
|
+
this.collections[type] = this.collections[type].filter(
|
|
480
|
+
(obj) => obj.id !== id,
|
|
481
|
+
)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return true
|
|
485
|
+
} catch (error) {
|
|
486
|
+
this.errors[type] = error.name === 'TypeError'
|
|
487
|
+
? networkError(error)
|
|
488
|
+
: genericError(error)
|
|
489
|
+
console.error(`Error deleting ${type}/${id}:`, error)
|
|
490
|
+
return false
|
|
491
|
+
} finally {
|
|
492
|
+
this.loading[type] = false
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Batch-resolve references by fetching multiple objects by their IDs.
|
|
498
|
+
* Uses the cache first, only fetches uncached objects.
|
|
499
|
+
*
|
|
500
|
+
* @param {string} type The registered type slug
|
|
501
|
+
* @param {string[]} ids Array of object IDs to resolve
|
|
502
|
+
* @return {Promise<Object<string, object>>} Map of id -> object
|
|
503
|
+
*/
|
|
504
|
+
async resolveReferences(type, ids) {
|
|
505
|
+
if (!ids || ids.length === 0) return {}
|
|
506
|
+
|
|
507
|
+
const uniqueIds = [...new Set(ids.filter(Boolean))]
|
|
508
|
+
const result = {}
|
|
509
|
+
const toFetch = []
|
|
510
|
+
|
|
511
|
+
for (const id of uniqueIds) {
|
|
512
|
+
const cached = this.objects[type]?.[id]
|
|
513
|
+
if (cached) {
|
|
514
|
+
result[id] = cached
|
|
515
|
+
} else {
|
|
516
|
+
toFetch.push(id)
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (toFetch.length > 0) {
|
|
521
|
+
const fetches = toFetch.map(async (id) => {
|
|
522
|
+
try {
|
|
523
|
+
const url = this._buildUrl(type, id)
|
|
524
|
+
const response = await fetch(url, {
|
|
525
|
+
method: 'GET',
|
|
526
|
+
headers: buildHeaders(),
|
|
527
|
+
})
|
|
528
|
+
if (response.ok) {
|
|
529
|
+
const data = await response.json()
|
|
530
|
+
if (!this.objects[type]) this.objects[type] = {}
|
|
531
|
+
this.objects[type][id] = data
|
|
532
|
+
result[id] = data
|
|
533
|
+
}
|
|
534
|
+
} catch {
|
|
535
|
+
// Non-blocking — leave unresolved
|
|
536
|
+
}
|
|
537
|
+
})
|
|
538
|
+
await Promise.all(fetches)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return result
|
|
542
|
+
},
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// ── Store factory ───────────────────────────────────────────────────────
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Create the object store definition with a given store ID and optional plugins.
|
|
549
|
+
*
|
|
550
|
+
* Plugins are merged into the store at definition time. Each plugin provides
|
|
551
|
+
* additional state, getters, and actions (e.g. for sub-resources like files,
|
|
552
|
+
* audit trails, relations).
|
|
553
|
+
*
|
|
554
|
+
* @param {string} storeId Pinia store identifier
|
|
555
|
+
* @param {Array} [plugins=[]] Array of plugin definitions
|
|
556
|
+
* @return {Function} Pinia store composable
|
|
557
|
+
*/
|
|
558
|
+
function defineObjectStore(storeId, plugins = []) {
|
|
559
|
+
const pluginState = mergePluginState(plugins)
|
|
560
|
+
const pluginGetters = mergePluginGetters(plugins)
|
|
561
|
+
const pluginActions = mergePluginActions(plugins)
|
|
562
|
+
|
|
563
|
+
return defineStore(storeId, {
|
|
564
|
+
state: () => ({
|
|
565
|
+
...baseState(),
|
|
566
|
+
...pluginState,
|
|
567
|
+
}),
|
|
568
|
+
|
|
569
|
+
getters: {
|
|
570
|
+
...baseGetters,
|
|
571
|
+
...pluginGetters,
|
|
572
|
+
},
|
|
573
|
+
|
|
574
|
+
actions: {
|
|
575
|
+
...baseActions,
|
|
576
|
+
...pluginActions,
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Clear all sub-resource data from active plugins.
|
|
580
|
+
* Calls each plugin's clear method (e.g. clearFiles, clearAuditTrails).
|
|
581
|
+
*/
|
|
582
|
+
clearAllSubResources() {
|
|
583
|
+
for (const plugin of plugins) {
|
|
584
|
+
const clearFn = `clear${capitalize(plugin.name)}`
|
|
585
|
+
if (typeof this[clearFn] === 'function') {
|
|
586
|
+
this[clearFn]()
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
})
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Default object store instance with ID 'conduction-objects'.
|
|
596
|
+
*
|
|
597
|
+
* @example
|
|
598
|
+
* import { useObjectStore } from '@conduction/nextcloud-vue'
|
|
599
|
+
* const store = useObjectStore()
|
|
600
|
+
*/
|
|
601
|
+
export const useObjectStore = defineObjectStore(DEFAULT_STORE_ID)
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Factory function to create an object store with a custom Pinia store ID
|
|
605
|
+
* and optional plugins for sub-resources.
|
|
606
|
+
*
|
|
607
|
+
* @param {string} storeId Custom Pinia store identifier
|
|
608
|
+
* @param {object} [options={}] Configuration options
|
|
609
|
+
* @param {Array} [options.plugins=[]] Array of sub-resource plugins
|
|
610
|
+
* @return {Function} Pinia store composable
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* // Basic (backwards compatible)
|
|
614
|
+
* const useMyStore = createObjectStore('object')
|
|
615
|
+
*
|
|
616
|
+
* @example
|
|
617
|
+
* // With plugins
|
|
618
|
+
* import { filesPlugin, auditTrailsPlugin } from '@conduction/nextcloud-vue'
|
|
619
|
+
* const useMyStore = createObjectStore('object', {
|
|
620
|
+
* plugins: [filesPlugin(), auditTrailsPlugin()],
|
|
621
|
+
* })
|
|
622
|
+
*/
|
|
623
|
+
export function createObjectStore(storeId, options = {}) {
|
|
624
|
+
return defineObjectStore(storeId, options.plugins || [])
|
|
625
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRegister Audit Trail entity type.
|
|
3
|
+
*
|
|
4
|
+
* Records changes made to objects for compliance and history tracking.
|
|
5
|
+
*/
|
|
6
|
+
export interface TAuditTrail {
|
|
7
|
+
id: number
|
|
8
|
+
uuid: string
|
|
9
|
+
schema: number
|
|
10
|
+
register: number
|
|
11
|
+
object: number
|
|
12
|
+
objectUuid: string | null
|
|
13
|
+
registerUuid: string | null
|
|
14
|
+
schemaUuid: string | null
|
|
15
|
+
action: string
|
|
16
|
+
changed: Record<string, unknown> | unknown[]
|
|
17
|
+
user: string
|
|
18
|
+
userName: string
|
|
19
|
+
session: string
|
|
20
|
+
request: string
|
|
21
|
+
ipAddress: string
|
|
22
|
+
version: string | null
|
|
23
|
+
created: string
|
|
24
|
+
organisationId: string | null
|
|
25
|
+
organisationIdType: string | null
|
|
26
|
+
processingActivityId: string | null
|
|
27
|
+
processingActivityUrl: string | null
|
|
28
|
+
processingId: string | null
|
|
29
|
+
confidentiality: string | null
|
|
30
|
+
retentionPeriod: string | null
|
|
31
|
+
size: number
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenRegister File entity type.
|
|
3
|
+
*
|
|
4
|
+
* Represents a file attached to an object. Files are stored in Nextcloud's
|
|
5
|
+
* file system and referenced by the object's files sub-resource.
|
|
6
|
+
*/
|
|
7
|
+
export interface TFile {
|
|
8
|
+
id: string | number
|
|
9
|
+
uuid?: string
|
|
10
|
+
name: string
|
|
11
|
+
mimeType: string
|
|
12
|
+
size: number
|
|
13
|
+
path?: string
|
|
14
|
+
url?: string
|
|
15
|
+
downloadUrl?: string
|
|
16
|
+
shareUrl?: string | null
|
|
17
|
+
tags?: string[]
|
|
18
|
+
published?: string | null
|
|
19
|
+
depublished?: string | null
|
|
20
|
+
owner?: string
|
|
21
|
+
created: string
|
|
22
|
+
updated?: string
|
|
23
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript type definitions for OpenRegister entities and shared utilities.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* // Import specific types
|
|
6
|
+
* import type { TObject, TSchema, TRegister } from '@conduction/nextcloud-vue/src/types'
|
|
7
|
+
*
|
|
8
|
+
* // Import shared utilities
|
|
9
|
+
* import type { TPaginated, TApiError, TQuota } from '@conduction/nextcloud-vue/src/types'
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Shared utility types
|
|
13
|
+
export type {
|
|
14
|
+
TPaginated,
|
|
15
|
+
TEmptyPaginated,
|
|
16
|
+
TQuota,
|
|
17
|
+
TUsage,
|
|
18
|
+
TEntityStats,
|
|
19
|
+
TCrudAuthorization,
|
|
20
|
+
TApiError,
|
|
21
|
+
TObjectPath,
|
|
22
|
+
} from './shared'
|
|
23
|
+
|
|
24
|
+
// Core entity types
|
|
25
|
+
export type { TObject } from './object'
|
|
26
|
+
export type { TSchema, TSchemaConfiguration } from './schema'
|
|
27
|
+
export type { TRegister } from './register'
|
|
28
|
+
export type { TAuditTrail } from './auditTrail'
|
|
29
|
+
export type { TSource } from './source'
|
|
30
|
+
export type { TOrganisation, TOrganisationAuthorization } from './organisation'
|
|
31
|
+
|
|
32
|
+
// Sub-resource entity types
|
|
33
|
+
export type { TFile } from './file'
|
|
34
|
+
export type { TTask, TTaskPriority, TTaskStatus } from './task'
|
|
35
|
+
export type { TNotification, TNotificationType, TNotificationPriority } from './notification'
|