@conduction/nextcloud-vue 0.1.0-beta.1 → 0.1.0-beta.11
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/README.md +226 -0
- package/css/index.css +5 -0
- package/dist/nextcloud-vue.cjs.js +79416 -7715
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +3583 -504
- package/dist/nextcloud-vue.esm.js +79343 -7692
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/l10n/en.json +164 -0
- package/l10n/nl.json +164 -0
- package/package.json +104 -63
- package/src/components/CnActionsBar/CnActionsBar.vue +254 -0
- package/src/components/CnActionsBar/index.js +1 -0
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +570 -0
- package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
- package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +422 -0
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
- package/src/components/CnAdvancedFormDialog/index.js +1 -0
- package/src/components/CnCard/CnCard.vue +415 -0
- package/src/components/CnCard/index.js +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +156 -152
- package/src/components/CnCardGrid/index.js +1 -1
- package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
- package/src/components/CnCellRenderer/index.js +1 -1
- package/src/components/CnChartWidget/CnChartWidget.vue +346 -0
- package/src/components/CnChartWidget/index.js +1 -0
- package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
- package/src/components/CnConfigurationCard/index.js +1 -1
- package/src/components/CnContextMenu/CnContextMenu.vue +142 -0
- package/src/components/CnContextMenu/index.js +1 -0
- package/src/components/CnCopyDialog/CnCopyDialog.vue +266 -0
- package/src/components/CnCopyDialog/index.js +1 -0
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +229 -0
- package/src/components/CnDashboardGrid/index.js +1 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +397 -0
- package/src/components/CnDashboardPage/index.js +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +362 -354
- package/src/components/CnDataTable/index.js +1 -1
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +177 -0
- package/src/components/CnDeleteDialog/index.js +1 -0
- package/src/components/CnDetailCard/CnDetailCard.vue +225 -0
- package/src/components/CnDetailCard/index.js +1 -0
- package/src/components/CnDetailGrid/CnDetailGrid.vue +256 -0
- package/src/components/CnDetailGrid/index.js +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +432 -0
- package/src/components/CnDetailPage/index.js +1 -0
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +234 -223
- package/src/components/CnFacetSidebar/index.js +1 -1
- package/src/components/CnFilterBar/CnFilterBar.vue +153 -152
- package/src/components/CnFilterBar/index.js +1 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +1047 -0
- package/src/components/CnFormDialog/index.js +1 -0
- package/src/components/CnIcon/CnIcon.vue +89 -0
- package/src/components/CnIcon/index.js +1 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +980 -682
- package/src/components/CnIndexPage/index.js +1 -1
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +536 -0
- package/src/components/CnIndexSidebar/index.js +1 -0
- package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
- package/src/components/CnInfoWidget/index.js +1 -0
- package/src/components/CnItemCard/CnItemCard.vue +134 -0
- package/src/components/CnItemCard/index.js +1 -0
- package/src/components/CnJsonViewer/CnJsonViewer.vue +312 -0
- package/src/components/CnJsonViewer/index.js +1 -0
- package/src/components/CnKpiGrid/CnKpiGrid.vue +93 -89
- package/src/components/CnKpiGrid/index.js +1 -1
- package/src/components/CnMassActionBar/CnMassActionBar.vue +161 -160
- package/src/components/CnMassActionBar/index.js +1 -1
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +327 -320
- package/src/components/CnMassCopyDialog/index.js +1 -1
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +245 -238
- package/src/components/CnMassDeleteDialog/index.js +1 -1
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +191 -190
- package/src/components/CnMassExportDialog/index.js +1 -1
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +494 -491
- package/src/components/CnMassImportDialog/index.js +1 -1
- package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
- package/src/components/CnNoteCard/index.js +1 -0
- package/src/components/CnNotesCard/CnNotesCard.vue +416 -0
- package/src/components/CnNotesCard/index.js +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +294 -292
- package/src/components/CnObjectCard/index.js +1 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +854 -0
- package/src/components/CnObjectDataWidget/index.js +1 -0
- package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +289 -0
- package/src/components/CnObjectMetadataWidget/index.js +1 -0
- package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +369 -0
- package/src/components/CnObjectSidebar/CnFilesTab.vue +287 -0
- package/src/components/CnObjectSidebar/CnNotesTab.vue +250 -0
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +255 -0
- package/src/components/CnObjectSidebar/CnTagsTab.vue +259 -0
- package/src/components/CnObjectSidebar/CnTasksTab.vue +483 -0
- package/src/components/CnObjectSidebar/index.js +6 -0
- package/src/components/CnPageHeader/CnPageHeader.vue +61 -0
- package/src/components/CnPageHeader/index.js +1 -0
- package/src/components/CnPagination/CnPagination.vue +253 -252
- package/src/components/CnPagination/index.js +1 -1
- package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
- package/src/components/CnProgressBar/index.js +1 -0
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +793 -0
- package/src/components/CnRegisterMapping/index.js +1 -0
- package/src/components/CnRowActions/CnRowActions.vue +95 -73
- package/src/components/CnRowActions/index.js +1 -1
- package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
- package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +788 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
- package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
- package/src/components/CnSchemaFormDialog/index.js +1 -0
- package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
- package/src/components/CnSettingsCard/index.js +1 -1
- package/src/components/CnSettingsSection/CnSettingsSection.vue +267 -266
- package/src/components/CnSettingsSection/index.js +1 -1
- package/src/components/CnStatsBlock/CnStatsBlock.vue +437 -366
- package/src/components/CnStatsBlock/index.js +1 -1
- package/src/components/CnStatsPanel/CnStatsPanel.vue +321 -0
- package/src/components/CnStatsPanel/index.js +1 -0
- package/src/components/CnStatusBadge/CnStatusBadge.vue +90 -77
- package/src/components/CnStatusBadge/index.js +1 -1
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +545 -0
- package/src/components/CnTabbedFormDialog/index.js +1 -0
- package/src/components/CnTableWidget/CnTableWidget.vue +333 -0
- package/src/components/CnTableWidget/index.js +1 -0
- package/src/components/CnTasksCard/CnTasksCard.vue +374 -0
- package/src/components/CnTasksCard/index.js +1 -0
- package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
- package/src/components/CnTileWidget/index.js +1 -0
- package/src/components/CnTimelineStages/CnTimelineStages.vue +294 -0
- package/src/components/CnTimelineStages/index.js +1 -0
- package/src/components/CnUserActionMenu/CnUserActionMenu.vue +436 -0
- package/src/components/CnUserActionMenu/index.js +1 -0
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +313 -312
- package/src/components/CnVersionInfoCard/index.js +1 -1
- package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
- package/src/components/CnWidgetRenderer/index.js +1 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +248 -0
- package/src/components/CnWidgetWrapper/index.js +1 -0
- package/src/components/index.js +57 -25
- package/src/composables/index.js +5 -3
- package/src/composables/useContextMenu.js +126 -0
- package/src/composables/useDashboardView.js +286 -0
- package/src/composables/useDetailView.js +290 -132
- package/src/composables/useListView.js +364 -153
- package/src/composables/useSubResource.js +142 -142
- package/src/constants/metadata.js +30 -0
- package/src/css/CnSchemaFormDialog.css +546 -0
- package/src/css/__sample_nextcloud_tokens.css +110 -0
- package/src/css/actions-bar.css +54 -0
- package/src/css/badge.css +83 -51
- package/src/css/card.css +129 -128
- package/src/css/context-menu.css +20 -0
- package/src/css/dashboard.css +70 -0
- package/src/css/detail-page.css +235 -0
- package/src/css/detail.css +68 -68
- package/src/css/index-page.css +44 -0
- package/src/css/index-sidebar.css +193 -0
- package/src/css/index.css +17 -8
- package/src/css/layout.css +90 -90
- package/src/css/page-header.css +35 -0
- package/src/css/pagination.css +72 -72
- package/src/css/table.css +142 -143
- package/src/css/timeline-stages.css +220 -0
- package/src/css/utilities.css +46 -46
- package/src/index.js +95 -50
- package/src/l10n/index.js +12 -0
- package/src/mixins/gridLayout.js +118 -0
- package/src/store/createCrudStore.d.ts +350 -0
- package/src/store/createCrudStore.js +413 -0
- package/src/store/createSubResourcePlugin.js +125 -135
- package/src/store/index.js +4 -3
- package/src/store/pluginMerge.js +55 -0
- package/src/store/plugins/auditTrails.js +357 -17
- package/src/store/plugins/files.js +250 -186
- package/src/store/plugins/index.js +8 -4
- package/src/store/plugins/lifecycle.js +180 -180
- package/src/store/plugins/logs.d.ts +22 -0
- package/src/store/plugins/logs.js +172 -0
- package/src/store/plugins/registerMapping.js +195 -0
- package/src/store/plugins/relations.js +68 -68
- package/src/store/plugins/search.js +385 -0
- package/src/store/plugins/selection.js +104 -0
- package/src/store/useObjectStore.js +793 -625
- package/src/types/auditTrail.d.ts +32 -32
- package/src/types/file.d.ts +23 -23
- package/src/types/index.d.ts +67 -35
- package/src/types/notification.d.ts +36 -36
- package/src/types/object.d.ts +40 -40
- package/src/types/organisation.d.ts +41 -41
- package/src/types/register.d.ts +25 -25
- package/src/types/schema.d.ts +39 -39
- package/src/types/shared.d.ts +79 -79
- package/src/types/source.d.ts +14 -14
- package/src/types/task.d.ts +31 -31
- package/src/utils/errors.js +96 -96
- package/src/utils/getTheme.js +9 -0
- package/src/utils/headers.js +80 -44
- package/src/utils/id.js +13 -0
- package/src/utils/index.js +4 -3
- package/src/utils/schema.js +423 -287
- package/src/utils/widgetVisibility.js +162 -0
- package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +0 -88
- package/src/components/CnDetailViewLayout/index.js +0 -1
- package/src/components/CnEmptyState/CnEmptyState.vue +0 -78
- package/src/components/CnEmptyState/index.js +0 -1
- package/src/components/CnListViewLayout/CnListViewLayout.vue +0 -80
- package/src/components/CnListViewLayout/index.js +0 -1
- package/src/components/CnViewModeToggle/CnViewModeToggle.vue +0 -77
- package/src/components/CnViewModeToggle/index.js +0 -1
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { defineStore } from 'pinia'
|
|
2
|
+
import { buildHeaders, prefixUrl } from '../utils/headers.js'
|
|
3
|
+
import { parseResponseError } from '../utils/errors.js'
|
|
4
|
+
import { mergePluginState, mergePluginGetters, mergePluginActions } from './pluginMerge.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Default fields stripped from items before POST/PUT.
|
|
8
|
+
* @type {string[]}
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_CLEAN_FIELDS = ['id', 'uuid', 'created', 'updated']
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Default base URL for the API.
|
|
14
|
+
* @type {string}
|
|
15
|
+
*/
|
|
16
|
+
const DEFAULT_BASE_URL = '/apps/openregister/api'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Default list-response parser — extracts the `results` array.
|
|
20
|
+
*
|
|
21
|
+
* Called with the store as `this`, so custom implementations can
|
|
22
|
+
* perform side-effects (e.g. update extra state from the response).
|
|
23
|
+
*
|
|
24
|
+
* @param {object} json Parsed response body
|
|
25
|
+
* @return {Array} The items array for setList
|
|
26
|
+
*/
|
|
27
|
+
function defaultParseListResponse(json) {
|
|
28
|
+
return json.results
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a Pinia store with standard CRUD operations.
|
|
33
|
+
*
|
|
34
|
+
* Generates a store with list/item state, pagination, filters, and
|
|
35
|
+
* async actions for fetching, creating, updating, and deleting items.
|
|
36
|
+
* Domain-specific state, getters, and actions can be added via `extend`.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // Minimal — pure CRUD
|
|
40
|
+
* import { createCrudStore } from '@conduction/nextcloud-vue'
|
|
41
|
+
* import { Source } from '../../entities/index.js'
|
|
42
|
+
*
|
|
43
|
+
* export const useSourceStore = createCrudStore('source', {
|
|
44
|
+
* endpoint: 'sources',
|
|
45
|
+
* entity: Source,
|
|
46
|
+
* })
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* // With features and extensions
|
|
50
|
+
* import { createCrudStore } from '@conduction/nextcloud-vue'
|
|
51
|
+
* import { Agent } from '../../entities/index.js'
|
|
52
|
+
*
|
|
53
|
+
* export const useAgentStore = createCrudStore('agent', {
|
|
54
|
+
* endpoint: 'agents',
|
|
55
|
+
* entity: Agent,
|
|
56
|
+
* features: { loading: true, viewMode: true },
|
|
57
|
+
* extend: {
|
|
58
|
+
* actions: {
|
|
59
|
+
* async getStats() {
|
|
60
|
+
* const response = await fetch(this._options.baseApiUrl + '/stats')
|
|
61
|
+
* if (!response.ok) throw new Error('HTTP ' + response.status)
|
|
62
|
+
* return response.json()
|
|
63
|
+
* },
|
|
64
|
+
* },
|
|
65
|
+
* },
|
|
66
|
+
* })
|
|
67
|
+
*
|
|
68
|
+
* @param {string} name Pinia store ID (e.g. 'source', 'agent')
|
|
69
|
+
* @param {object} config Store configuration
|
|
70
|
+
* @param {string} config.endpoint API resource path segment (e.g. 'sources')
|
|
71
|
+
* @param {string} [config.baseUrl] API base URL (before endpoint)
|
|
72
|
+
* @param {Function|null} [config.entity] Entity class constructor for wrapping items, or null for raw data
|
|
73
|
+
* @param {string[]} [config.cleanFields] Fields to strip in cleanForSave
|
|
74
|
+
* @param {object} [config.features] Feature flags to enable optional state/getters/actions
|
|
75
|
+
* @param {boolean} [config.features.loading] Add loading/error state and isLoading/getError getters
|
|
76
|
+
* @param {boolean} [config.features.viewMode] Add viewMode state, getViewMode getter, setViewMode action
|
|
77
|
+
* @param {Function} [config.parseListResponse] Custom response parser for refreshList.
|
|
78
|
+
* Receives the parsed JSON body with the store instance as `this`.
|
|
79
|
+
* Must return an array of items. Default: `(json) => json.results`
|
|
80
|
+
* @param {Array} [config.plugins] Array of plugin definitions to merge into the store.
|
|
81
|
+
* Each plugin is `{ name, state?, getters?, actions? }` — same shape as object-store
|
|
82
|
+
* plugins. Merge order is base → plugins → extend, so `extend` can still override
|
|
83
|
+
* anything a plugin provides.
|
|
84
|
+
* @param {object} [config.extend] Extra state/getters/actions to merge into the store
|
|
85
|
+
* @param {Function} [config.extend.state] State factory returning extra state properties
|
|
86
|
+
* @param {object} [config.extend.getters] Extra getters (or overrides of base getters)
|
|
87
|
+
* @param {object} [config.extend.actions] Extra actions (or overrides of base/plugin actions)
|
|
88
|
+
* @return {Function} Pinia store composable (useXxxStore)
|
|
89
|
+
*/
|
|
90
|
+
export function createCrudStore(name, config = {}) {
|
|
91
|
+
const {
|
|
92
|
+
endpoint,
|
|
93
|
+
baseUrl = DEFAULT_BASE_URL,
|
|
94
|
+
entity: Entity = null,
|
|
95
|
+
cleanFields = DEFAULT_CLEAN_FIELDS,
|
|
96
|
+
features = {},
|
|
97
|
+
parseListResponse = defaultParseListResponse,
|
|
98
|
+
plugins = [],
|
|
99
|
+
extend = {},
|
|
100
|
+
} = config
|
|
101
|
+
|
|
102
|
+
if (!endpoint) {
|
|
103
|
+
throw new Error(`createCrudStore("${name}"): config.endpoint is required`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const baseApiUrl = prefixUrl(`${baseUrl}/${endpoint}`)
|
|
107
|
+
|
|
108
|
+
const pluginState = mergePluginState(plugins)
|
|
109
|
+
const pluginGetters = mergePluginGetters(plugins)
|
|
110
|
+
const pluginActions = mergePluginActions(plugins)
|
|
111
|
+
const setupPlugins = plugins.filter((p) => typeof p.setup === 'function')
|
|
112
|
+
// Track which store instances have already been set up so plugin setup
|
|
113
|
+
// hooks run exactly once per instance, even if useStore() is called many
|
|
114
|
+
// times. WeakSet lets garbage collection reclaim entries when a Pinia
|
|
115
|
+
// instance (and therefore its stores) are discarded — e.g. between tests
|
|
116
|
+
// that call createPinia() afresh.
|
|
117
|
+
const initialized = new WeakSet()
|
|
118
|
+
|
|
119
|
+
const useStore = defineStore(name, {
|
|
120
|
+
state: () => ({
|
|
121
|
+
// ── Core state ──
|
|
122
|
+
item: null,
|
|
123
|
+
list: [],
|
|
124
|
+
filters: {},
|
|
125
|
+
pagination: { page: 1, limit: 20 },
|
|
126
|
+
|
|
127
|
+
// ── Optional feature state ──
|
|
128
|
+
...(features.loading ? { loading: false, error: null } : {}),
|
|
129
|
+
...(features.viewMode ? { viewMode: 'cards' } : {}),
|
|
130
|
+
|
|
131
|
+
// ── Plugin state ──
|
|
132
|
+
...pluginState,
|
|
133
|
+
|
|
134
|
+
// ── Internal config (available to extend actions and plugins) ──
|
|
135
|
+
_options: { endpoint, cleanFields, baseApiUrl, entity: Entity },
|
|
136
|
+
|
|
137
|
+
// ── Domain-specific state ──
|
|
138
|
+
...(typeof extend.state === 'function' ? extend.state() : {}),
|
|
139
|
+
}),
|
|
140
|
+
|
|
141
|
+
getters: {
|
|
142
|
+
// ── Optional feature getters ──
|
|
143
|
+
...(features.viewMode ? { getViewMode: (state) => state.viewMode } : {}),
|
|
144
|
+
...(features.loading
|
|
145
|
+
? {
|
|
146
|
+
isLoading: (state) => state.loading,
|
|
147
|
+
getError: (state) => state.error,
|
|
148
|
+
}
|
|
149
|
+
: {}),
|
|
150
|
+
|
|
151
|
+
// ── Plugin getters ──
|
|
152
|
+
...pluginGetters,
|
|
153
|
+
|
|
154
|
+
// ── Domain-specific getters ──
|
|
155
|
+
...(extend.getters ?? {}),
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
actions: {
|
|
159
|
+
// ── Setters ──
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Set the active item. Wraps in Entity class if configured.
|
|
163
|
+
* @param {object|null} data Raw item data or null to clear
|
|
164
|
+
*/
|
|
165
|
+
setItem(data) {
|
|
166
|
+
this.item = data
|
|
167
|
+
? (Entity ? new Entity(data) : data)
|
|
168
|
+
: null
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Set the item list. Wraps each item in Entity class if configured.
|
|
173
|
+
* @param {Array} data Array of raw item objects
|
|
174
|
+
*/
|
|
175
|
+
setList(data) {
|
|
176
|
+
this.list = Entity
|
|
177
|
+
? data.map((item) => new Entity(item))
|
|
178
|
+
: [...data]
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Set pagination parameters.
|
|
183
|
+
* @param {number} page Current page number
|
|
184
|
+
* @param {number} [limit] Items per page
|
|
185
|
+
*/
|
|
186
|
+
setPagination(page, limit = 20) {
|
|
187
|
+
this.pagination = { page, limit }
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Merge filter criteria into the current filters.
|
|
192
|
+
* @param {object} filters Key-value filter pairs to merge
|
|
193
|
+
*/
|
|
194
|
+
setFilters(filters) {
|
|
195
|
+
this.filters = { ...this.filters, ...filters }
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
// ── Optional feature actions ──
|
|
199
|
+
...(features.viewMode
|
|
200
|
+
? {
|
|
201
|
+
/**
|
|
202
|
+
* Set the view mode (e.g. 'cards', 'table').
|
|
203
|
+
* @param {string} mode View mode identifier
|
|
204
|
+
*/
|
|
205
|
+
setViewMode(mode) {
|
|
206
|
+
this.viewMode = mode
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
: {}),
|
|
210
|
+
|
|
211
|
+
// ── CRUD actions ──
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Fetch the item list from the API.
|
|
215
|
+
* @param {string|null} [search] Optional search query
|
|
216
|
+
* @param {boolean} [soft] If true, don't toggle loading state
|
|
217
|
+
* @return {Promise<{response: Response, data: Array}>}
|
|
218
|
+
*/
|
|
219
|
+
async refreshList(search = null, soft = false) {
|
|
220
|
+
if (features.loading && !soft) {
|
|
221
|
+
this.loading = true
|
|
222
|
+
this.error = null
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
let url = this._options.baseApiUrl
|
|
226
|
+
if (search) {
|
|
227
|
+
url += '?_search=' + encodeURIComponent(search)
|
|
228
|
+
}
|
|
229
|
+
const response = await fetch(url, {
|
|
230
|
+
method: 'GET',
|
|
231
|
+
headers: buildHeaders(),
|
|
232
|
+
})
|
|
233
|
+
if (!response.ok) {
|
|
234
|
+
throw await parseResponseError(response, name)
|
|
235
|
+
}
|
|
236
|
+
const json = await response.json()
|
|
237
|
+
const data = parseListResponse.call(this, json)
|
|
238
|
+
this.setList(data)
|
|
239
|
+
return { response, data }
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (features.loading) {
|
|
242
|
+
this.error = error.message ?? error.toString()
|
|
243
|
+
}
|
|
244
|
+
throw error
|
|
245
|
+
} finally {
|
|
246
|
+
if (features.loading && !soft) {
|
|
247
|
+
this.loading = false
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Fetch a single item by ID and set it as the active item.
|
|
254
|
+
* @param {string|number} id Item ID or UUID
|
|
255
|
+
* @return {Promise<object>} The fetched item data
|
|
256
|
+
*/
|
|
257
|
+
async getOne(id) {
|
|
258
|
+
if (features.loading) {
|
|
259
|
+
this.loading = true
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
const response = await fetch(`${this._options.baseApiUrl}/${id}`, {
|
|
263
|
+
method: 'GET',
|
|
264
|
+
headers: buildHeaders(),
|
|
265
|
+
})
|
|
266
|
+
if (!response.ok) {
|
|
267
|
+
throw await parseResponseError(response, name)
|
|
268
|
+
}
|
|
269
|
+
const data = await response.json()
|
|
270
|
+
this.setItem(data)
|
|
271
|
+
return data
|
|
272
|
+
} catch (error) {
|
|
273
|
+
if (features.loading) {
|
|
274
|
+
this.error = error.message ?? error.toString()
|
|
275
|
+
}
|
|
276
|
+
throw error
|
|
277
|
+
} finally {
|
|
278
|
+
if (features.loading) {
|
|
279
|
+
this.loading = false
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Delete an item by ID. Refreshes the list and clears the active item.
|
|
286
|
+
* @param {object} item Item object (must have .id)
|
|
287
|
+
* @return {Promise<{response: Response}>}
|
|
288
|
+
*/
|
|
289
|
+
async deleteOne(item) {
|
|
290
|
+
if (!item.id) {
|
|
291
|
+
throw new Error(`No ${name} to delete`)
|
|
292
|
+
}
|
|
293
|
+
if (features.loading) {
|
|
294
|
+
this.loading = true
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const response = await fetch(`${this._options.baseApiUrl}/${item.id}`, {
|
|
298
|
+
method: 'DELETE',
|
|
299
|
+
headers: buildHeaders(),
|
|
300
|
+
})
|
|
301
|
+
if (!response.ok) {
|
|
302
|
+
throw await parseResponseError(response, name)
|
|
303
|
+
}
|
|
304
|
+
await this.refreshList()
|
|
305
|
+
this.setItem(null)
|
|
306
|
+
return { response }
|
|
307
|
+
} catch (error) {
|
|
308
|
+
if (features.loading) {
|
|
309
|
+
this.error = error.message ?? error.toString()
|
|
310
|
+
}
|
|
311
|
+
throw error
|
|
312
|
+
} finally {
|
|
313
|
+
if (features.loading) {
|
|
314
|
+
this.loading = false
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Strip read-only fields from an item before saving.
|
|
321
|
+
* Uses the `cleanFields` config array. Override in `extend.actions`
|
|
322
|
+
* for custom cleaning (the configured fields are in `this._options.cleanFields`).
|
|
323
|
+
* @param {object} item Raw item data
|
|
324
|
+
* @return {object} Cleaned copy safe for POST/PUT
|
|
325
|
+
*/
|
|
326
|
+
cleanForSave(item) {
|
|
327
|
+
const cleaned = { ...item }
|
|
328
|
+
for (const field of this._options.cleanFields) {
|
|
329
|
+
delete cleaned[field]
|
|
330
|
+
}
|
|
331
|
+
return cleaned
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Create or update an item. Determines method from presence of `.id`.
|
|
336
|
+
* @param {object} item Item data (without .id = create, with .id = update)
|
|
337
|
+
* @return {Promise<{response: Response, data: object}>}
|
|
338
|
+
*/
|
|
339
|
+
async save(item) {
|
|
340
|
+
if (!item) {
|
|
341
|
+
throw new Error(`No ${name} to save`)
|
|
342
|
+
}
|
|
343
|
+
if (features.loading) {
|
|
344
|
+
this.loading = true
|
|
345
|
+
}
|
|
346
|
+
const isNew = !item.id
|
|
347
|
+
const url = isNew
|
|
348
|
+
? this._options.baseApiUrl
|
|
349
|
+
: `${this._options.baseApiUrl}/${item.id}`
|
|
350
|
+
const method = isNew ? 'POST' : 'PUT'
|
|
351
|
+
const body = this.cleanForSave(item)
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
const response = await fetch(url, {
|
|
355
|
+
method,
|
|
356
|
+
headers: buildHeaders(),
|
|
357
|
+
body: JSON.stringify(body),
|
|
358
|
+
})
|
|
359
|
+
if (!response.ok) {
|
|
360
|
+
throw await parseResponseError(response, name)
|
|
361
|
+
}
|
|
362
|
+
const responseData = await response.json()
|
|
363
|
+
const data = Entity ? new Entity(responseData) : responseData
|
|
364
|
+
this.setItem(data)
|
|
365
|
+
await this.refreshList()
|
|
366
|
+
return { response, data }
|
|
367
|
+
} catch (error) {
|
|
368
|
+
if (features.loading) {
|
|
369
|
+
this.error = error.message ?? error.toString()
|
|
370
|
+
}
|
|
371
|
+
throw error
|
|
372
|
+
} finally {
|
|
373
|
+
if (features.loading) {
|
|
374
|
+
this.loading = false
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
// ── Plugin actions (may override base actions) ──
|
|
380
|
+
...pluginActions,
|
|
381
|
+
|
|
382
|
+
// ── Domain-specific actions (may override base/plugin actions) ──
|
|
383
|
+
...(extend.actions ?? {}),
|
|
384
|
+
},
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
// When no plugin declares a setup hook, return Pinia's composable
|
|
388
|
+
// directly — zero runtime overhead for the common case.
|
|
389
|
+
if (setupPlugins.length === 0) {
|
|
390
|
+
return useStore
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Wrapped composable: resolves the Pinia store, then runs each plugin's
|
|
395
|
+
* `setup(store)` exactly once per instance. Plugins typically use the
|
|
396
|
+
* setup hook to register `store.$onAction` / `store.$subscribe`
|
|
397
|
+
* subscriptions that observe base or other plugin actions without
|
|
398
|
+
* overriding them.
|
|
399
|
+
*
|
|
400
|
+
* @param {import('pinia').Pinia} [pinia] Optional Pinia instance override
|
|
401
|
+
* @return {object} The Pinia store instance with all plugin setups applied
|
|
402
|
+
*/
|
|
403
|
+
return function useCrudStore(pinia) {
|
|
404
|
+
const store = useStore(pinia)
|
|
405
|
+
if (!initialized.has(store)) {
|
|
406
|
+
initialized.add(store)
|
|
407
|
+
for (const plugin of setupPlugins) {
|
|
408
|
+
plugin.setup(store)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return store
|
|
412
|
+
}
|
|
413
|
+
}
|
|
@@ -1,135 +1,125 @@
|
|
|
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
|
|
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
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* @
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* @
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
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
|
-
}
|
|
1
|
+
import { buildHeaders, buildQueryString, capitalize } 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] 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
|
+
* Create a sub-resource plugin for the object store.
|
|
16
|
+
*
|
|
17
|
+
* Generates state, getters, and actions for a standard OpenRegister sub-resource
|
|
18
|
+
* endpoint that returns paginated results. The generated plugin follows the
|
|
19
|
+
* naming convention: fetch{Name}, clear{Name}, get{Name}, is{Name}Loading, etc.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} name Camel-case name for the sub-resource (e.g. 'auditTrails')
|
|
22
|
+
* @param {string} endpoint URL path segment appended to the object URL (e.g. 'audit-trails')
|
|
23
|
+
* @param {object} [options] Plugin options
|
|
24
|
+
* @param {number} [options.limit] Default page size
|
|
25
|
+
* @return {Function} Plugin factory that returns the plugin definition
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* // Simple read-only sub-resource
|
|
29
|
+
* export const auditTrailsPlugin = createSubResourcePlugin('auditTrails', 'audit-trails')
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // With custom limit
|
|
33
|
+
* export const contractsPlugin = createSubResourcePlugin('contracts', 'contracts', { limit: 50 })
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* // Usage in store creation
|
|
37
|
+
* const useStore = createObjectStore('object', {
|
|
38
|
+
* plugins: [auditTrailsPlugin()],
|
|
39
|
+
* })
|
|
40
|
+
* const store = useStore()
|
|
41
|
+
* await store.fetchAuditTrails('case', caseId)
|
|
42
|
+
* console.log(store.auditTrails.results)
|
|
43
|
+
*/
|
|
44
|
+
export function createSubResourcePlugin(name, endpoint, options = {}) {
|
|
45
|
+
const cap = capitalize(name)
|
|
46
|
+
const limit = options.limit || 20
|
|
47
|
+
|
|
48
|
+
return () => ({
|
|
49
|
+
name,
|
|
50
|
+
|
|
51
|
+
state: () => ({
|
|
52
|
+
[name]: emptyPaginated(limit),
|
|
53
|
+
[`${name}Loading`]: false,
|
|
54
|
+
[`${name}Error`]: null,
|
|
55
|
+
}),
|
|
56
|
+
|
|
57
|
+
getters: {
|
|
58
|
+
[`get${cap}`]: (state) => state[name],
|
|
59
|
+
[`is${cap}Loading`]: (state) => state[`${name}Loading`],
|
|
60
|
+
[`get${cap}Error`]: (state) => state[`${name}Error`],
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
actions: {
|
|
64
|
+
/**
|
|
65
|
+
* Fetch the sub-resource collection for an object.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} type The registered object type slug
|
|
68
|
+
* @param {string} objectId The parent object ID
|
|
69
|
+
* @param {object} [params] Query parameters (_search, _limit, _page)
|
|
70
|
+
* @return {Promise<Array>} The fetched results
|
|
71
|
+
*/
|
|
72
|
+
async [`fetch${cap}`](type, objectId, params = {}) {
|
|
73
|
+
this[`${name}Loading`] = true
|
|
74
|
+
this[`${name}Error`] = null
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const url = this._buildUrl(type, objectId)
|
|
78
|
+
+ '/' + endpoint
|
|
79
|
+
+ buildQueryString(params)
|
|
80
|
+
|
|
81
|
+
const response = await fetch(url, {
|
|
82
|
+
method: 'GET',
|
|
83
|
+
headers: buildHeaders(),
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
if (!response.ok) {
|
|
87
|
+
this[`${name}Error`] = await parseResponseError(response, name)
|
|
88
|
+
console.error(`Error fetching ${name} for ${type}/${objectId}:`, this[`${name}Error`])
|
|
89
|
+
return []
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const data = await response.json()
|
|
93
|
+
|
|
94
|
+
this[name] = {
|
|
95
|
+
results: data.results || data,
|
|
96
|
+
total: data.total || (data.results || data).length,
|
|
97
|
+
page: data.page || 1,
|
|
98
|
+
pages: data.pages || 0,
|
|
99
|
+
limit: params._limit || limit,
|
|
100
|
+
offset: data.offset || 0,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return this[name].results
|
|
104
|
+
} catch (error) {
|
|
105
|
+
this[`${name}Error`] = error.name === 'TypeError'
|
|
106
|
+
? networkError(error)
|
|
107
|
+
: { status: null, message: error.message, details: null, isValidation: false, fields: null, toString() { return this.message } }
|
|
108
|
+
console.error(`Error fetching ${name} for ${type}/${objectId}:`, error)
|
|
109
|
+
return []
|
|
110
|
+
} finally {
|
|
111
|
+
this[`${name}Loading`] = false
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Clear all sub-resource state back to empty defaults.
|
|
117
|
+
*/
|
|
118
|
+
[`clear${cap}`]() {
|
|
119
|
+
this[name] = emptyPaginated(limit)
|
|
120
|
+
this[`${name}Loading`] = false
|
|
121
|
+
this[`${name}Error`] = null
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
}
|
package/src/store/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { useObjectStore, createObjectStore } from './useObjectStore.js'
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
1
|
+
export { useObjectStore, createObjectStore } from './useObjectStore.js'
|
|
2
|
+
export { createCrudStore } from './createCrudStore.js'
|
|
3
|
+
export { createSubResourcePlugin, emptyPaginated } from './createSubResourcePlugin.js'
|
|
4
|
+
export { auditTrailsPlugin, relationsPlugin, filesPlugin, lifecyclePlugin, selectionPlugin, searchPlugin, SEARCH_TYPE, getRegisterApiUrl, getSchemaApiUrl } from './plugins/index.js'
|