@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.
Files changed (208) hide show
  1. package/README.md +226 -0
  2. package/css/index.css +5 -0
  3. package/dist/nextcloud-vue.cjs.js +79416 -7715
  4. package/dist/nextcloud-vue.cjs.js.map +1 -1
  5. package/dist/nextcloud-vue.css +3583 -504
  6. package/dist/nextcloud-vue.esm.js +79343 -7692
  7. package/dist/nextcloud-vue.esm.js.map +1 -1
  8. package/l10n/en.json +164 -0
  9. package/l10n/nl.json +164 -0
  10. package/package.json +104 -63
  11. package/src/components/CnActionsBar/CnActionsBar.vue +254 -0
  12. package/src/components/CnActionsBar/index.js +1 -0
  13. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +570 -0
  14. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  15. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  16. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +422 -0
  17. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  18. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  19. package/src/components/CnCard/CnCard.vue +415 -0
  20. package/src/components/CnCard/index.js +1 -0
  21. package/src/components/CnCardGrid/CnCardGrid.vue +156 -152
  22. package/src/components/CnCardGrid/index.js +1 -1
  23. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  24. package/src/components/CnCellRenderer/index.js +1 -1
  25. package/src/components/CnChartWidget/CnChartWidget.vue +346 -0
  26. package/src/components/CnChartWidget/index.js +1 -0
  27. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  28. package/src/components/CnConfigurationCard/index.js +1 -1
  29. package/src/components/CnContextMenu/CnContextMenu.vue +142 -0
  30. package/src/components/CnContextMenu/index.js +1 -0
  31. package/src/components/CnCopyDialog/CnCopyDialog.vue +266 -0
  32. package/src/components/CnCopyDialog/index.js +1 -0
  33. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +229 -0
  34. package/src/components/CnDashboardGrid/index.js +1 -0
  35. package/src/components/CnDashboardPage/CnDashboardPage.vue +397 -0
  36. package/src/components/CnDashboardPage/index.js +1 -0
  37. package/src/components/CnDataTable/CnDataTable.vue +362 -354
  38. package/src/components/CnDataTable/index.js +1 -1
  39. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +177 -0
  40. package/src/components/CnDeleteDialog/index.js +1 -0
  41. package/src/components/CnDetailCard/CnDetailCard.vue +225 -0
  42. package/src/components/CnDetailCard/index.js +1 -0
  43. package/src/components/CnDetailGrid/CnDetailGrid.vue +256 -0
  44. package/src/components/CnDetailGrid/index.js +1 -0
  45. package/src/components/CnDetailPage/CnDetailPage.vue +432 -0
  46. package/src/components/CnDetailPage/index.js +1 -0
  47. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +234 -223
  48. package/src/components/CnFacetSidebar/index.js +1 -1
  49. package/src/components/CnFilterBar/CnFilterBar.vue +153 -152
  50. package/src/components/CnFilterBar/index.js +1 -1
  51. package/src/components/CnFormDialog/CnFormDialog.vue +1047 -0
  52. package/src/components/CnFormDialog/index.js +1 -0
  53. package/src/components/CnIcon/CnIcon.vue +89 -0
  54. package/src/components/CnIcon/index.js +1 -0
  55. package/src/components/CnIndexPage/CnIndexPage.vue +980 -682
  56. package/src/components/CnIndexPage/index.js +1 -1
  57. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +536 -0
  58. package/src/components/CnIndexSidebar/index.js +1 -0
  59. package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
  60. package/src/components/CnInfoWidget/index.js +1 -0
  61. package/src/components/CnItemCard/CnItemCard.vue +134 -0
  62. package/src/components/CnItemCard/index.js +1 -0
  63. package/src/components/CnJsonViewer/CnJsonViewer.vue +312 -0
  64. package/src/components/CnJsonViewer/index.js +1 -0
  65. package/src/components/CnKpiGrid/CnKpiGrid.vue +93 -89
  66. package/src/components/CnKpiGrid/index.js +1 -1
  67. package/src/components/CnMassActionBar/CnMassActionBar.vue +161 -160
  68. package/src/components/CnMassActionBar/index.js +1 -1
  69. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +327 -320
  70. package/src/components/CnMassCopyDialog/index.js +1 -1
  71. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +245 -238
  72. package/src/components/CnMassDeleteDialog/index.js +1 -1
  73. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +191 -190
  74. package/src/components/CnMassExportDialog/index.js +1 -1
  75. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +494 -491
  76. package/src/components/CnMassImportDialog/index.js +1 -1
  77. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  78. package/src/components/CnNoteCard/index.js +1 -0
  79. package/src/components/CnNotesCard/CnNotesCard.vue +416 -0
  80. package/src/components/CnNotesCard/index.js +1 -0
  81. package/src/components/CnObjectCard/CnObjectCard.vue +294 -292
  82. package/src/components/CnObjectCard/index.js +1 -1
  83. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +854 -0
  84. package/src/components/CnObjectDataWidget/index.js +1 -0
  85. package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +289 -0
  86. package/src/components/CnObjectMetadataWidget/index.js +1 -0
  87. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +369 -0
  88. package/src/components/CnObjectSidebar/CnFilesTab.vue +287 -0
  89. package/src/components/CnObjectSidebar/CnNotesTab.vue +250 -0
  90. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +255 -0
  91. package/src/components/CnObjectSidebar/CnTagsTab.vue +259 -0
  92. package/src/components/CnObjectSidebar/CnTasksTab.vue +483 -0
  93. package/src/components/CnObjectSidebar/index.js +6 -0
  94. package/src/components/CnPageHeader/CnPageHeader.vue +61 -0
  95. package/src/components/CnPageHeader/index.js +1 -0
  96. package/src/components/CnPagination/CnPagination.vue +253 -252
  97. package/src/components/CnPagination/index.js +1 -1
  98. package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
  99. package/src/components/CnProgressBar/index.js +1 -0
  100. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +793 -0
  101. package/src/components/CnRegisterMapping/index.js +1 -0
  102. package/src/components/CnRowActions/CnRowActions.vue +95 -73
  103. package/src/components/CnRowActions/index.js +1 -1
  104. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  105. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +788 -0
  106. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  107. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  108. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  109. package/src/components/CnSchemaFormDialog/index.js +1 -0
  110. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  111. package/src/components/CnSettingsCard/index.js +1 -1
  112. package/src/components/CnSettingsSection/CnSettingsSection.vue +267 -266
  113. package/src/components/CnSettingsSection/index.js +1 -1
  114. package/src/components/CnStatsBlock/CnStatsBlock.vue +437 -366
  115. package/src/components/CnStatsBlock/index.js +1 -1
  116. package/src/components/CnStatsPanel/CnStatsPanel.vue +321 -0
  117. package/src/components/CnStatsPanel/index.js +1 -0
  118. package/src/components/CnStatusBadge/CnStatusBadge.vue +90 -77
  119. package/src/components/CnStatusBadge/index.js +1 -1
  120. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +545 -0
  121. package/src/components/CnTabbedFormDialog/index.js +1 -0
  122. package/src/components/CnTableWidget/CnTableWidget.vue +333 -0
  123. package/src/components/CnTableWidget/index.js +1 -0
  124. package/src/components/CnTasksCard/CnTasksCard.vue +374 -0
  125. package/src/components/CnTasksCard/index.js +1 -0
  126. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  127. package/src/components/CnTileWidget/index.js +1 -0
  128. package/src/components/CnTimelineStages/CnTimelineStages.vue +294 -0
  129. package/src/components/CnTimelineStages/index.js +1 -0
  130. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +436 -0
  131. package/src/components/CnUserActionMenu/index.js +1 -0
  132. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +313 -312
  133. package/src/components/CnVersionInfoCard/index.js +1 -1
  134. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  135. package/src/components/CnWidgetRenderer/index.js +1 -0
  136. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +248 -0
  137. package/src/components/CnWidgetWrapper/index.js +1 -0
  138. package/src/components/index.js +57 -25
  139. package/src/composables/index.js +5 -3
  140. package/src/composables/useContextMenu.js +126 -0
  141. package/src/composables/useDashboardView.js +286 -0
  142. package/src/composables/useDetailView.js +290 -132
  143. package/src/composables/useListView.js +364 -153
  144. package/src/composables/useSubResource.js +142 -142
  145. package/src/constants/metadata.js +30 -0
  146. package/src/css/CnSchemaFormDialog.css +546 -0
  147. package/src/css/__sample_nextcloud_tokens.css +110 -0
  148. package/src/css/actions-bar.css +54 -0
  149. package/src/css/badge.css +83 -51
  150. package/src/css/card.css +129 -128
  151. package/src/css/context-menu.css +20 -0
  152. package/src/css/dashboard.css +70 -0
  153. package/src/css/detail-page.css +235 -0
  154. package/src/css/detail.css +68 -68
  155. package/src/css/index-page.css +44 -0
  156. package/src/css/index-sidebar.css +193 -0
  157. package/src/css/index.css +17 -8
  158. package/src/css/layout.css +90 -90
  159. package/src/css/page-header.css +35 -0
  160. package/src/css/pagination.css +72 -72
  161. package/src/css/table.css +142 -143
  162. package/src/css/timeline-stages.css +220 -0
  163. package/src/css/utilities.css +46 -46
  164. package/src/index.js +95 -50
  165. package/src/l10n/index.js +12 -0
  166. package/src/mixins/gridLayout.js +118 -0
  167. package/src/store/createCrudStore.d.ts +350 -0
  168. package/src/store/createCrudStore.js +413 -0
  169. package/src/store/createSubResourcePlugin.js +125 -135
  170. package/src/store/index.js +4 -3
  171. package/src/store/pluginMerge.js +55 -0
  172. package/src/store/plugins/auditTrails.js +357 -17
  173. package/src/store/plugins/files.js +250 -186
  174. package/src/store/plugins/index.js +8 -4
  175. package/src/store/plugins/lifecycle.js +180 -180
  176. package/src/store/plugins/logs.d.ts +22 -0
  177. package/src/store/plugins/logs.js +172 -0
  178. package/src/store/plugins/registerMapping.js +195 -0
  179. package/src/store/plugins/relations.js +68 -68
  180. package/src/store/plugins/search.js +385 -0
  181. package/src/store/plugins/selection.js +104 -0
  182. package/src/store/useObjectStore.js +793 -625
  183. package/src/types/auditTrail.d.ts +32 -32
  184. package/src/types/file.d.ts +23 -23
  185. package/src/types/index.d.ts +67 -35
  186. package/src/types/notification.d.ts +36 -36
  187. package/src/types/object.d.ts +40 -40
  188. package/src/types/organisation.d.ts +41 -41
  189. package/src/types/register.d.ts +25 -25
  190. package/src/types/schema.d.ts +39 -39
  191. package/src/types/shared.d.ts +79 -79
  192. package/src/types/source.d.ts +14 -14
  193. package/src/types/task.d.ts +31 -31
  194. package/src/utils/errors.js +96 -96
  195. package/src/utils/getTheme.js +9 -0
  196. package/src/utils/headers.js +80 -44
  197. package/src/utils/id.js +13 -0
  198. package/src/utils/index.js +4 -3
  199. package/src/utils/schema.js +423 -287
  200. package/src/utils/widgetVisibility.js +162 -0
  201. package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +0 -88
  202. package/src/components/CnDetailViewLayout/index.js +0 -1
  203. package/src/components/CnEmptyState/CnEmptyState.vue +0 -78
  204. package/src/components/CnEmptyState/index.js +0 -1
  205. package/src/components/CnListViewLayout/CnListViewLayout.vue +0 -80
  206. package/src/components/CnListViewLayout/index.js +0 -1
  207. package/src/components/CnViewModeToggle/CnViewModeToggle.vue +0 -77
  208. 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=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
- }
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
+ }
@@ -1,3 +1,4 @@
1
- export { useObjectStore, createObjectStore } from './useObjectStore.js'
2
- export { createSubResourcePlugin, emptyPaginated } from './createSubResourcePlugin.js'
3
- export { auditTrailsPlugin, relationsPlugin, filesPlugin, lifecyclePlugin } from './plugins/index.js'
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'