@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,46 @@
|
|
|
1
|
+
/* ========================================
|
|
2
|
+
@conduction/nextcloud-vue — Utilities
|
|
3
|
+
======================================== */
|
|
4
|
+
|
|
5
|
+
/* Text Utilities */
|
|
6
|
+
.cn-text-ellipsis {
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
text-overflow: ellipsis;
|
|
9
|
+
white-space: nowrap;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.cn-text-description {
|
|
13
|
+
font-size: 0.9em;
|
|
14
|
+
color: var(--color-text-maxcontrast);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.cn-text-success { color: var(--color-success); }
|
|
18
|
+
.cn-text-error { color: var(--color-error); }
|
|
19
|
+
.cn-text-warning { color: var(--color-warning); }
|
|
20
|
+
.cn-text-muted { color: var(--color-text-maxcontrast); }
|
|
21
|
+
|
|
22
|
+
/* Vue Transition Animations */
|
|
23
|
+
.cn-slide-fade-enter-active {
|
|
24
|
+
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.cn-slide-fade-leave-active {
|
|
28
|
+
transition: all 0.3s cubic-bezier(0.4, 0, 1, 1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.cn-slide-fade-enter,
|
|
32
|
+
.cn-slide-fade-enter-from {
|
|
33
|
+
transform: translateY(-10px);
|
|
34
|
+
opacity: 0;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.cn-slide-fade-leave-to {
|
|
38
|
+
transform: translateY(-5px);
|
|
39
|
+
opacity: 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Backwards compatibility aliases */
|
|
43
|
+
.textEllipsis { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
44
|
+
.textDescription { font-size: 0.9em; color: var(--color-text-maxcontrast); }
|
|
45
|
+
.successMessage { color: var(--color-success); }
|
|
46
|
+
.errorMessage { color: var(--color-error); }
|
package/src/index.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Components
|
|
2
|
+
export {
|
|
3
|
+
CnDataTable,
|
|
4
|
+
CnFilterBar,
|
|
5
|
+
CnListViewLayout,
|
|
6
|
+
CnDetailViewLayout,
|
|
7
|
+
CnStatusBadge,
|
|
8
|
+
CnEmptyState,
|
|
9
|
+
CnPagination,
|
|
10
|
+
CnSettingsCard,
|
|
11
|
+
CnSettingsSection,
|
|
12
|
+
CnStatsBlock,
|
|
13
|
+
CnConfigurationCard,
|
|
14
|
+
CnVersionInfoCard,
|
|
15
|
+
CnCellRenderer,
|
|
16
|
+
CnObjectCard,
|
|
17
|
+
CnCardGrid,
|
|
18
|
+
CnFacetSidebar,
|
|
19
|
+
CnViewModeToggle,
|
|
20
|
+
CnRowActions,
|
|
21
|
+
CnIndexPage,
|
|
22
|
+
CnMassActionBar,
|
|
23
|
+
CnMassDeleteDialog,
|
|
24
|
+
CnMassCopyDialog,
|
|
25
|
+
CnKpiGrid,
|
|
26
|
+
CnMassExportDialog,
|
|
27
|
+
CnMassImportDialog,
|
|
28
|
+
} from './components/index.js'
|
|
29
|
+
|
|
30
|
+
// Store
|
|
31
|
+
export { useObjectStore, createObjectStore } from './store/index.js'
|
|
32
|
+
export { createSubResourcePlugin, emptyPaginated } from './store/index.js'
|
|
33
|
+
|
|
34
|
+
// Store plugins
|
|
35
|
+
export {
|
|
36
|
+
auditTrailsPlugin,
|
|
37
|
+
relationsPlugin,
|
|
38
|
+
filesPlugin,
|
|
39
|
+
lifecyclePlugin,
|
|
40
|
+
} from './store/plugins/index.js'
|
|
41
|
+
|
|
42
|
+
// Composables
|
|
43
|
+
export { useListView, useDetailView, useSubResource } from './composables/index.js'
|
|
44
|
+
|
|
45
|
+
// Utilities
|
|
46
|
+
export { buildHeaders, buildQueryString, parseResponseError, networkError, genericError } from './utils/index.js'
|
|
47
|
+
export { columnsFromSchema, formatValue, filtersFromSchema } from './utils/index.js'
|
|
48
|
+
|
|
49
|
+
// CSS (consumers should import separately)
|
|
50
|
+
// import '@conduction/nextcloud-vue/src/css/index.css'
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { buildHeaders, buildQueryString } from '../utils/headers.js'
|
|
2
|
+
import { parseResponseError, networkError } from '../utils/errors.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Standard empty paginated response shape used by all sub-resource plugins.
|
|
6
|
+
*
|
|
7
|
+
* @param {number} [limit=20] Default page size
|
|
8
|
+
* @return {object} Empty paginated state
|
|
9
|
+
*/
|
|
10
|
+
export function emptyPaginated(limit = 20) {
|
|
11
|
+
return { results: [], total: 0, page: 1, pages: 0, limit, offset: 0 }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Capitalize the first letter of a string.
|
|
16
|
+
*
|
|
17
|
+
* @param {string} str Input string
|
|
18
|
+
* @return {string} Capitalized string
|
|
19
|
+
*/
|
|
20
|
+
function capitalize(str) {
|
|
21
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a sub-resource plugin for the object store.
|
|
26
|
+
*
|
|
27
|
+
* Generates state, getters, and actions for a standard OpenRegister sub-resource
|
|
28
|
+
* endpoint that returns paginated results. The generated plugin follows the
|
|
29
|
+
* naming convention: fetch{Name}, clear{Name}, get{Name}, is{Name}Loading, etc.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} name Camel-case name for the sub-resource (e.g. 'auditTrails')
|
|
32
|
+
* @param {string} endpoint URL path segment appended to the object URL (e.g. 'audit-trails')
|
|
33
|
+
* @param {object} [options={}] Plugin options
|
|
34
|
+
* @param {number} [options.limit=20] Default page size
|
|
35
|
+
* @return {Function} Plugin factory that returns the plugin definition
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // Simple read-only sub-resource
|
|
39
|
+
* export const auditTrailsPlugin = createSubResourcePlugin('auditTrails', 'audit-trails')
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* // With custom limit
|
|
43
|
+
* export const contractsPlugin = createSubResourcePlugin('contracts', 'contracts', { limit: 50 })
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // Usage in store creation
|
|
47
|
+
* const useStore = createObjectStore('object', {
|
|
48
|
+
* plugins: [auditTrailsPlugin()],
|
|
49
|
+
* })
|
|
50
|
+
* const store = useStore()
|
|
51
|
+
* await store.fetchAuditTrails('case', caseId)
|
|
52
|
+
* console.log(store.auditTrails.results)
|
|
53
|
+
*/
|
|
54
|
+
export function createSubResourcePlugin(name, endpoint, options = {}) {
|
|
55
|
+
const cap = capitalize(name)
|
|
56
|
+
const limit = options.limit || 20
|
|
57
|
+
|
|
58
|
+
return () => ({
|
|
59
|
+
name,
|
|
60
|
+
|
|
61
|
+
state: () => ({
|
|
62
|
+
[name]: emptyPaginated(limit),
|
|
63
|
+
[`${name}Loading`]: false,
|
|
64
|
+
[`${name}Error`]: null,
|
|
65
|
+
}),
|
|
66
|
+
|
|
67
|
+
getters: {
|
|
68
|
+
[`get${cap}`]: (state) => state[name],
|
|
69
|
+
[`is${cap}Loading`]: (state) => state[`${name}Loading`],
|
|
70
|
+
[`get${cap}Error`]: (state) => state[`${name}Error`],
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
actions: {
|
|
74
|
+
/**
|
|
75
|
+
* Fetch the sub-resource collection for an object.
|
|
76
|
+
*
|
|
77
|
+
* @param {string} type The registered object type slug
|
|
78
|
+
* @param {string} objectId The parent object ID
|
|
79
|
+
* @param {object} [params={}] Query parameters (_search, _limit, _page)
|
|
80
|
+
* @return {Promise<Array>} The fetched results
|
|
81
|
+
*/
|
|
82
|
+
async [`fetch${cap}`](type, objectId, params = {}) {
|
|
83
|
+
this[`${name}Loading`] = true
|
|
84
|
+
this[`${name}Error`] = null
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
const url = this._buildUrl(type, objectId)
|
|
88
|
+
+ '/' + endpoint
|
|
89
|
+
+ buildQueryString(params)
|
|
90
|
+
|
|
91
|
+
const response = await fetch(url, {
|
|
92
|
+
method: 'GET',
|
|
93
|
+
headers: buildHeaders(),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if (!response.ok) {
|
|
97
|
+
this[`${name}Error`] = await parseResponseError(response, name)
|
|
98
|
+
console.error(`Error fetching ${name} for ${type}/${objectId}:`, this[`${name}Error`])
|
|
99
|
+
return []
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const data = await response.json()
|
|
103
|
+
|
|
104
|
+
this[name] = {
|
|
105
|
+
results: data.results || data,
|
|
106
|
+
total: data.total || (data.results || data).length,
|
|
107
|
+
page: data.page || 1,
|
|
108
|
+
pages: data.pages || 0,
|
|
109
|
+
limit: params._limit || limit,
|
|
110
|
+
offset: data.offset || 0,
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return this[name].results
|
|
114
|
+
} catch (error) {
|
|
115
|
+
this[`${name}Error`] = error.name === 'TypeError'
|
|
116
|
+
? networkError(error)
|
|
117
|
+
: { status: null, message: error.message, details: null, isValidation: false, fields: null, toString() { return this.message } }
|
|
118
|
+
console.error(`Error fetching ${name} for ${type}/${objectId}:`, error)
|
|
119
|
+
return []
|
|
120
|
+
} finally {
|
|
121
|
+
this[`${name}Loading`] = false
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Clear all sub-resource state back to empty defaults.
|
|
127
|
+
*/
|
|
128
|
+
[`clear${cap}`]() {
|
|
129
|
+
this[name] = emptyPaginated(limit)
|
|
130
|
+
this[`${name}Loading`] = false
|
|
131
|
+
this[`${name}Error`] = null
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
})
|
|
135
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createSubResourcePlugin } from '../createSubResourcePlugin.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Audit trails plugin for the object store.
|
|
5
|
+
*
|
|
6
|
+
* Adds state, getters, and actions for fetching audit trail records
|
|
7
|
+
* for an object. Read-only — no create/update/delete.
|
|
8
|
+
*
|
|
9
|
+
* State: auditTrails, auditTrailsLoading, auditTrailsError
|
|
10
|
+
* Actions: fetchAuditTrails(type, objectId, params), clearAuditTrails()
|
|
11
|
+
* Getters: getAuditTrails, isAuditTrailsLoading, getAuditTrailsError
|
|
12
|
+
*
|
|
13
|
+
* @param {object} [options={}] Plugin options
|
|
14
|
+
* @param {number} [options.limit=20] Default page size
|
|
15
|
+
* @return {Function} Plugin factory
|
|
16
|
+
*/
|
|
17
|
+
export const auditTrailsPlugin = createSubResourcePlugin('auditTrails', 'audit-trails')
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { createSubResourcePlugin } from '../createSubResourcePlugin.js'
|
|
2
|
+
import { buildHeaders } from '../../utils/headers.js'
|
|
3
|
+
import { parseResponseError, networkError } from '../../utils/errors.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Files plugin for the object store.
|
|
7
|
+
*
|
|
8
|
+
* Extends the generic sub-resource with file-specific actions:
|
|
9
|
+
* upload (multipart), publish, unpublish, and delete.
|
|
10
|
+
*
|
|
11
|
+
* State: files, filesLoading, filesError
|
|
12
|
+
* Actions: fetchFiles, uploadFiles, publishFile, unpublishFile, deleteFile, clearFiles
|
|
13
|
+
* Getters: getFiles, isFilesLoading, getFilesError
|
|
14
|
+
*
|
|
15
|
+
* @param {object} [options={}] Plugin options
|
|
16
|
+
* @param {number} [options.limit=20] Default page size
|
|
17
|
+
* @return {Function} Plugin factory
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* const useStore = createObjectStore('object', {
|
|
21
|
+
* plugins: [filesPlugin()],
|
|
22
|
+
* })
|
|
23
|
+
* const store = useStore()
|
|
24
|
+
* await store.fetchFiles('case', caseId)
|
|
25
|
+
* await store.uploadFiles('case', caseId, formData)
|
|
26
|
+
* await store.publishFile('case', caseId, fileId)
|
|
27
|
+
*/
|
|
28
|
+
export function filesPlugin(options = {}) {
|
|
29
|
+
const base = createSubResourcePlugin('files', 'files', options)()
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
...base,
|
|
33
|
+
|
|
34
|
+
actions: {
|
|
35
|
+
...base.actions,
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Upload files to an object via multipart form data.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} type The registered object type slug
|
|
41
|
+
* @param {string} objectId The parent object ID
|
|
42
|
+
* @param {FormData} formData FormData with files[] and optional tags/share fields
|
|
43
|
+
* @return {Promise<object|null>} Upload response or null on error
|
|
44
|
+
*/
|
|
45
|
+
async uploadFiles(type, objectId, formData) {
|
|
46
|
+
this.filesLoading = true
|
|
47
|
+
this.filesError = null
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const url = this._buildUrl(type, objectId) + '/filesMultipart'
|
|
51
|
+
|
|
52
|
+
const response = await fetch(url, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: buildHeaders(null),
|
|
55
|
+
body: formData,
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
if (!response.ok) {
|
|
59
|
+
this.filesError = await parseResponseError(response, 'files')
|
|
60
|
+
console.error(`Error uploading files for ${type}/${objectId}:`, this.filesError)
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const data = await response.json()
|
|
65
|
+
|
|
66
|
+
await this.fetchFiles(type, objectId)
|
|
67
|
+
|
|
68
|
+
return data
|
|
69
|
+
} catch (error) {
|
|
70
|
+
this.filesError = error.name === 'TypeError'
|
|
71
|
+
? networkError(error)
|
|
72
|
+
: { status: null, message: error.message, details: null, isValidation: false, fields: null, toString() { return this.message } }
|
|
73
|
+
console.error(`Error uploading files for ${type}/${objectId}:`, error)
|
|
74
|
+
return null
|
|
75
|
+
} finally {
|
|
76
|
+
this.filesLoading = false
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Publish a file (make it publicly accessible).
|
|
82
|
+
*
|
|
83
|
+
* @param {string} type The registered object type slug
|
|
84
|
+
* @param {string} objectId The parent object ID
|
|
85
|
+
* @param {string} fileId The file ID to publish
|
|
86
|
+
* @return {Promise<boolean>} True if published successfully
|
|
87
|
+
*/
|
|
88
|
+
async publishFile(type, objectId, fileId) {
|
|
89
|
+
this.filesLoading = true
|
|
90
|
+
this.filesError = null
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const url = this._buildUrl(type, objectId) + `/files/${fileId}/publish`
|
|
94
|
+
|
|
95
|
+
const response = await fetch(url, {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: buildHeaders(),
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
this.filesError = await parseResponseError(response, 'files')
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
await this.fetchFiles(type, objectId)
|
|
106
|
+
return true
|
|
107
|
+
} catch (error) {
|
|
108
|
+
this.filesError = networkError(error)
|
|
109
|
+
return false
|
|
110
|
+
} finally {
|
|
111
|
+
this.filesLoading = false
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Unpublish a file (revoke public access).
|
|
117
|
+
*
|
|
118
|
+
* @param {string} type The registered object type slug
|
|
119
|
+
* @param {string} objectId The parent object ID
|
|
120
|
+
* @param {string} fileId The file ID to unpublish
|
|
121
|
+
* @return {Promise<boolean>} True if unpublished successfully
|
|
122
|
+
*/
|
|
123
|
+
async unpublishFile(type, objectId, fileId) {
|
|
124
|
+
this.filesLoading = true
|
|
125
|
+
this.filesError = null
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const url = this._buildUrl(type, objectId) + `/files/${fileId}/depublish`
|
|
129
|
+
|
|
130
|
+
const response = await fetch(url, {
|
|
131
|
+
method: 'POST',
|
|
132
|
+
headers: buildHeaders(),
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
if (!response.ok) {
|
|
136
|
+
this.filesError = await parseResponseError(response, 'files')
|
|
137
|
+
return false
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
await this.fetchFiles(type, objectId)
|
|
141
|
+
return true
|
|
142
|
+
} catch (error) {
|
|
143
|
+
this.filesError = networkError(error)
|
|
144
|
+
return false
|
|
145
|
+
} finally {
|
|
146
|
+
this.filesLoading = false
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Delete a file from an object.
|
|
152
|
+
*
|
|
153
|
+
* @param {string} type The registered object type slug
|
|
154
|
+
* @param {string} objectId The parent object ID
|
|
155
|
+
* @param {string} fileId The file ID to delete
|
|
156
|
+
* @return {Promise<boolean>} True if deleted successfully
|
|
157
|
+
*/
|
|
158
|
+
async deleteFile(type, objectId, fileId) {
|
|
159
|
+
this.filesLoading = true
|
|
160
|
+
this.filesError = null
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const url = this._buildUrl(type, objectId) + `/files/${fileId}`
|
|
164
|
+
|
|
165
|
+
const response = await fetch(url, {
|
|
166
|
+
method: 'DELETE',
|
|
167
|
+
headers: buildHeaders(),
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
if (!response.ok) {
|
|
171
|
+
this.filesError = await parseResponseError(response, 'files')
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
await this.fetchFiles(type, objectId)
|
|
176
|
+
return true
|
|
177
|
+
} catch (error) {
|
|
178
|
+
this.filesError = networkError(error)
|
|
179
|
+
return false
|
|
180
|
+
} finally {
|
|
181
|
+
this.filesLoading = false
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { buildHeaders } from '../../utils/headers.js'
|
|
2
|
+
import { parseResponseError, networkError } from '../../utils/errors.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lifecycle plugin for the object store.
|
|
6
|
+
*
|
|
7
|
+
* Adds object lifecycle actions: lock, unlock, publish, depublish, revert, merge.
|
|
8
|
+
* These operate on the object itself (not sub-resources) but share the same
|
|
9
|
+
* URL pattern: POST /{register}/{schema}/{objectId}/{action}
|
|
10
|
+
*
|
|
11
|
+
* State: lifecycleLoading, lifecycleError
|
|
12
|
+
* Actions: lockObject, unlockObject, publishObject, depublishObject, revertObject, mergeObjects
|
|
13
|
+
*
|
|
14
|
+
* @return {Function} Plugin factory
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const useStore = createObjectStore('object', {
|
|
18
|
+
* plugins: [lifecyclePlugin()],
|
|
19
|
+
* })
|
|
20
|
+
* const store = useStore()
|
|
21
|
+
* await store.lockObject('case', caseId, { process: 'review', duration: 3600 })
|
|
22
|
+
* await store.publishObject('case', caseId, { date: '2025-01-01' })
|
|
23
|
+
*/
|
|
24
|
+
export function lifecyclePlugin() {
|
|
25
|
+
return {
|
|
26
|
+
name: 'Lifecycle',
|
|
27
|
+
|
|
28
|
+
state: () => ({
|
|
29
|
+
lifecycleLoading: false,
|
|
30
|
+
lifecycleError: null,
|
|
31
|
+
}),
|
|
32
|
+
|
|
33
|
+
getters: {
|
|
34
|
+
isLifecycleLoading: (state) => state.lifecycleLoading,
|
|
35
|
+
getLifecycleError: (state) => state.lifecycleError,
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
actions: {
|
|
39
|
+
/**
|
|
40
|
+
* Perform a lifecycle action on an object.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} type The registered object type slug
|
|
43
|
+
* @param {string} objectId The object ID
|
|
44
|
+
* @param {string} action The lifecycle action endpoint (e.g. 'lock', 'publish')
|
|
45
|
+
* @param {object} [body=null] Optional request body
|
|
46
|
+
* @return {Promise<object|null>} Response data or null on error
|
|
47
|
+
*/
|
|
48
|
+
async _lifecycleAction(type, objectId, action, body = null) {
|
|
49
|
+
this.lifecycleLoading = true
|
|
50
|
+
this.lifecycleError = null
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const url = this._buildUrl(type, objectId) + '/' + action
|
|
54
|
+
const options = {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: buildHeaders(),
|
|
57
|
+
}
|
|
58
|
+
if (body) {
|
|
59
|
+
options.body = JSON.stringify(body)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const response = await fetch(url, options)
|
|
63
|
+
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
this.lifecycleError = await parseResponseError(response, action)
|
|
66
|
+
console.error(`Error performing ${action} on ${type}/${objectId}:`, this.lifecycleError)
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const data = await response.json()
|
|
71
|
+
|
|
72
|
+
if (this.objects[type] && data.id) {
|
|
73
|
+
this.objects[type][data.id] = data
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return data
|
|
77
|
+
} catch (error) {
|
|
78
|
+
this.lifecycleError = error.name === 'TypeError'
|
|
79
|
+
? networkError(error)
|
|
80
|
+
: { status: null, message: error.message, details: null, isValidation: false, fields: null, toString() { return this.message } }
|
|
81
|
+
console.error(`Error performing ${action} on ${type}/${objectId}:`, error)
|
|
82
|
+
return null
|
|
83
|
+
} finally {
|
|
84
|
+
this.lifecycleLoading = false
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Lock an object to prevent concurrent edits.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} type The registered object type slug
|
|
92
|
+
* @param {string} objectId The object ID
|
|
93
|
+
* @param {object} options Lock options
|
|
94
|
+
* @param {string} [options.process] Lock reason/process name
|
|
95
|
+
* @param {number} [options.duration] Lock duration in seconds
|
|
96
|
+
* @return {Promise<object|null>} Updated object or null on error
|
|
97
|
+
*/
|
|
98
|
+
async lockObject(type, objectId, options = {}) {
|
|
99
|
+
return this._lifecycleAction(type, objectId, 'lock', options)
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Unlock an object.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} type The registered object type slug
|
|
106
|
+
* @param {string} objectId The object ID
|
|
107
|
+
* @return {Promise<object|null>} Updated object or null on error
|
|
108
|
+
*/
|
|
109
|
+
async unlockObject(type, objectId) {
|
|
110
|
+
return this._lifecycleAction(type, objectId, 'unlock')
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Publish an object (make it publicly accessible).
|
|
115
|
+
*
|
|
116
|
+
* @param {string} type The registered object type slug
|
|
117
|
+
* @param {string} objectId The object ID
|
|
118
|
+
* @param {object} [options={}] Publish options
|
|
119
|
+
* @param {string} [options.date] Publish date (ISO 8601)
|
|
120
|
+
* @return {Promise<object|null>} Updated object or null on error
|
|
121
|
+
*/
|
|
122
|
+
async publishObject(type, objectId, options = {}) {
|
|
123
|
+
return this._lifecycleAction(type, objectId, 'publish', options)
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Depublish an object (revoke public access).
|
|
128
|
+
*
|
|
129
|
+
* @param {string} type The registered object type slug
|
|
130
|
+
* @param {string} objectId The object ID
|
|
131
|
+
* @param {object} [options={}] Depublish options
|
|
132
|
+
* @param {string} [options.date] Depublish date (ISO 8601)
|
|
133
|
+
* @return {Promise<object|null>} Updated object or null on error
|
|
134
|
+
*/
|
|
135
|
+
async depublishObject(type, objectId, options = {}) {
|
|
136
|
+
return this._lifecycleAction(type, objectId, 'depublish', options)
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Revert an object to a previous version.
|
|
141
|
+
*
|
|
142
|
+
* @param {string} type The registered object type slug
|
|
143
|
+
* @param {string} objectId The object ID
|
|
144
|
+
* @param {object} options Revert options
|
|
145
|
+
* @param {string} [options.datetime] Target datetime
|
|
146
|
+
* @param {string} [options.auditTrailId] Audit trail entry ID to revert to
|
|
147
|
+
* @param {number} [options.version] Target version number
|
|
148
|
+
* @param {boolean} [options.overwriteVersion] Overwrite current version
|
|
149
|
+
* @return {Promise<object|null>} Updated object or null on error
|
|
150
|
+
*/
|
|
151
|
+
async revertObject(type, objectId, options = {}) {
|
|
152
|
+
return this._lifecycleAction(type, objectId, 'revert', options)
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Merge two objects together.
|
|
157
|
+
*
|
|
158
|
+
* @param {string} type The registered object type slug
|
|
159
|
+
* @param {string} sourceId The source object ID (will be merged into target)
|
|
160
|
+
* @param {object} options Merge options
|
|
161
|
+
* @param {string} options.target Target object ID
|
|
162
|
+
* @param {string} [options.fileAction] How to handle files ('move'|'copy'|'skip')
|
|
163
|
+
* @param {string} [options.relationAction] How to handle relations
|
|
164
|
+
* @param {string} [options.referenceAction] How to handle references
|
|
165
|
+
* @return {Promise<object|null>} Merge result or null on error
|
|
166
|
+
*/
|
|
167
|
+
async mergeObjects(type, sourceId, options = {}) {
|
|
168
|
+
return this._lifecycleAction(type, sourceId, 'merge', options)
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Clear lifecycle state.
|
|
173
|
+
*/
|
|
174
|
+
clearLifecycle() {
|
|
175
|
+
this.lifecycleLoading = false
|
|
176
|
+
this.lifecycleError = null
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createSubResourcePlugin } from '../createSubResourcePlugin.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Relations plugin for the object store.
|
|
5
|
+
*
|
|
6
|
+
* Adds three sub-resources for object relations:
|
|
7
|
+
* - contracts: contractual relations between objects
|
|
8
|
+
* - uses: outgoing references (this object uses other objects)
|
|
9
|
+
* - used: incoming references (other objects use this object)
|
|
10
|
+
*
|
|
11
|
+
* Each sub-resource gets its own state, loading, error, fetch, and clear.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const contractsBase = createSubResourcePlugin('contracts', 'contracts')
|
|
15
|
+
const usesBase = createSubResourcePlugin('uses', 'uses')
|
|
16
|
+
const usedBase = createSubResourcePlugin('used', 'used')
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Combined relations plugin that registers contracts, uses, and used sub-resources.
|
|
20
|
+
*
|
|
21
|
+
* @param {object} [options={}] Plugin options
|
|
22
|
+
* @return {Function} Plugin factory
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* const useStore = createObjectStore('object', {
|
|
26
|
+
* plugins: [relationsPlugin()],
|
|
27
|
+
* })
|
|
28
|
+
* const store = useStore()
|
|
29
|
+
* await store.fetchContracts('case', caseId)
|
|
30
|
+
* await store.fetchUses('case', caseId)
|
|
31
|
+
* await store.fetchUsed('case', caseId)
|
|
32
|
+
*/
|
|
33
|
+
export function relationsPlugin(options = {}) {
|
|
34
|
+
const contracts = contractsBase(options)
|
|
35
|
+
const uses = usesBase(options)
|
|
36
|
+
const used = usedBase(options)
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
name: 'Relations',
|
|
40
|
+
|
|
41
|
+
state: () => ({
|
|
42
|
+
...contracts.state(),
|
|
43
|
+
...uses.state(),
|
|
44
|
+
...used.state(),
|
|
45
|
+
}),
|
|
46
|
+
|
|
47
|
+
getters: {
|
|
48
|
+
...contracts.getters,
|
|
49
|
+
...uses.getters,
|
|
50
|
+
...used.getters,
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
actions: {
|
|
54
|
+
...contracts.actions,
|
|
55
|
+
...uses.actions,
|
|
56
|
+
...used.actions,
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Clear all relation sub-resources.
|
|
60
|
+
*/
|
|
61
|
+
clearRelations() {
|
|
62
|
+
this.clearContracts()
|
|
63
|
+
this.clearUses()
|
|
64
|
+
this.clearUsed()
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
}
|