@conduction/nextcloud-vue 0.1.0-beta.1 → 0.1.0-beta.10

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 (197) hide show
  1. package/README.md +226 -0
  2. package/css/index.css +5 -0
  3. package/dist/nextcloud-vue.cjs +67614 -0
  4. package/dist/nextcloud-vue.cjs.js +76311 -5905
  5. package/dist/nextcloud-vue.cjs.js.map +1 -1
  6. package/dist/nextcloud-vue.cjs.map +1 -0
  7. package/dist/nextcloud-vue.css +3279 -203
  8. package/dist/nextcloud-vue.esm.js +76240 -5882
  9. package/dist/nextcloud-vue.esm.js.map +1 -1
  10. package/package.json +89 -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 +569 -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 +23 -20
  22. package/src/components/CnCardGrid/index.js +1 -1
  23. package/src/components/CnCellRenderer/index.js +1 -1
  24. package/src/components/CnChartWidget/CnChartWidget.vue +318 -0
  25. package/src/components/CnChartWidget/index.js +1 -0
  26. package/src/components/CnConfigurationCard/index.js +1 -1
  27. package/src/components/CnContextMenu/CnContextMenu.vue +142 -0
  28. package/src/components/CnContextMenu/index.js +1 -0
  29. package/src/components/CnCopyDialog/CnCopyDialog.vue +257 -0
  30. package/src/components/CnCopyDialog/index.js +1 -0
  31. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +229 -0
  32. package/src/components/CnDashboardGrid/index.js +1 -0
  33. package/src/components/CnDashboardPage/CnDashboardPage.vue +396 -0
  34. package/src/components/CnDashboardPage/index.js +1 -0
  35. package/src/components/CnDataTable/CnDataTable.vue +24 -16
  36. package/src/components/CnDataTable/index.js +1 -1
  37. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +177 -0
  38. package/src/components/CnDeleteDialog/index.js +1 -0
  39. package/src/components/CnDetailCard/CnDetailCard.vue +225 -0
  40. package/src/components/CnDetailCard/index.js +1 -0
  41. package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
  42. package/src/components/CnDetailGrid/index.js +1 -0
  43. package/src/components/CnDetailPage/CnDetailPage.vue +431 -0
  44. package/src/components/CnDetailPage/index.js +1 -0
  45. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +12 -2
  46. package/src/components/CnFacetSidebar/index.js +1 -1
  47. package/src/components/CnFilterBar/index.js +1 -1
  48. package/src/components/CnFormDialog/CnFormDialog.vue +934 -0
  49. package/src/components/CnFormDialog/index.js +1 -0
  50. package/src/components/CnIcon/CnIcon.vue +89 -0
  51. package/src/components/CnIcon/index.js +1 -0
  52. package/src/components/CnIndexPage/CnIndexPage.vue +589 -291
  53. package/src/components/CnIndexPage/index.js +1 -1
  54. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +535 -0
  55. package/src/components/CnIndexSidebar/index.js +1 -0
  56. package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
  57. package/src/components/CnInfoWidget/index.js +1 -0
  58. package/src/components/CnItemCard/CnItemCard.vue +134 -0
  59. package/src/components/CnItemCard/index.js +1 -0
  60. package/src/components/CnJsonViewer/CnJsonViewer.vue +283 -0
  61. package/src/components/CnJsonViewer/index.js +1 -0
  62. package/src/components/CnKpiGrid/CnKpiGrid.vue +5 -1
  63. package/src/components/CnKpiGrid/index.js +1 -1
  64. package/src/components/CnMassActionBar/CnMassActionBar.vue +6 -5
  65. package/src/components/CnMassActionBar/index.js +1 -1
  66. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +16 -9
  67. package/src/components/CnMassCopyDialog/index.js +1 -1
  68. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +16 -9
  69. package/src/components/CnMassDeleteDialog/index.js +1 -1
  70. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +8 -7
  71. package/src/components/CnMassExportDialog/index.js +1 -1
  72. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +20 -17
  73. package/src/components/CnMassImportDialog/index.js +1 -1
  74. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  75. package/src/components/CnNoteCard/index.js +1 -0
  76. package/src/components/CnNotesCard/CnNotesCard.vue +415 -0
  77. package/src/components/CnNotesCard/index.js +1 -0
  78. package/src/components/CnObjectCard/CnObjectCard.vue +3 -1
  79. package/src/components/CnObjectCard/index.js +1 -1
  80. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +853 -0
  81. package/src/components/CnObjectDataWidget/index.js +1 -0
  82. package/src/components/CnObjectMetadataWidget/CnObjectMetadataWidget.vue +288 -0
  83. package/src/components/CnObjectMetadataWidget/index.js +1 -0
  84. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +368 -0
  85. package/src/components/CnObjectSidebar/CnFilesTab.vue +286 -0
  86. package/src/components/CnObjectSidebar/CnNotesTab.vue +249 -0
  87. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +254 -0
  88. package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
  89. package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
  90. package/src/components/CnObjectSidebar/index.js +6 -0
  91. package/src/components/CnPageHeader/CnPageHeader.vue +61 -0
  92. package/src/components/CnPageHeader/index.js +1 -0
  93. package/src/components/CnPagination/CnPagination.vue +7 -6
  94. package/src/components/CnPagination/index.js +1 -1
  95. package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
  96. package/src/components/CnProgressBar/index.js +1 -0
  97. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -0
  98. package/src/components/CnRegisterMapping/index.js +1 -0
  99. package/src/components/CnRowActions/CnRowActions.vue +25 -3
  100. package/src/components/CnRowActions/index.js +1 -1
  101. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  102. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
  103. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  104. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  105. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  106. package/src/components/CnSchemaFormDialog/index.js +1 -0
  107. package/src/components/CnSettingsCard/index.js +1 -1
  108. package/src/components/CnSettingsSection/index.js +1 -1
  109. package/src/components/CnStatsBlock/CnStatsBlock.vue +89 -19
  110. package/src/components/CnStatsBlock/index.js +1 -1
  111. package/src/components/CnStatsPanel/CnStatsPanel.vue +320 -0
  112. package/src/components/CnStatsPanel/index.js +1 -0
  113. package/src/components/CnStatusBadge/CnStatusBadge.vue +15 -2
  114. package/src/components/CnStatusBadge/index.js +1 -1
  115. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +544 -0
  116. package/src/components/CnTabbedFormDialog/index.js +1 -0
  117. package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
  118. package/src/components/CnTableWidget/index.js +1 -0
  119. package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
  120. package/src/components/CnTasksCard/index.js +1 -0
  121. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  122. package/src/components/CnTileWidget/index.js +1 -0
  123. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
  124. package/src/components/CnTimelineStages/index.js +1 -0
  125. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
  126. package/src/components/CnUserActionMenu/index.js +1 -0
  127. package/src/components/CnVersionInfoCard/index.js +1 -1
  128. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  129. package/src/components/CnWidgetRenderer/index.js +1 -0
  130. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +246 -0
  131. package/src/components/CnWidgetWrapper/index.js +1 -0
  132. package/src/components/index.js +57 -25
  133. package/src/composables/index.js +5 -3
  134. package/src/composables/useContextMenu.js +126 -0
  135. package/src/composables/useDashboardView.js +286 -0
  136. package/src/composables/useDetailView.js +290 -132
  137. package/src/composables/useListView.js +364 -153
  138. package/src/composables/useSubResource.js +142 -142
  139. package/src/constants/metadata.js +30 -0
  140. package/src/css/CnSchemaFormDialog.css +546 -0
  141. package/src/css/__sample_nextcloud_tokens.css +110 -0
  142. package/src/css/actions-bar.css +54 -0
  143. package/src/css/badge.css +83 -51
  144. package/src/css/card.css +129 -128
  145. package/src/css/context-menu.css +20 -0
  146. package/src/css/dashboard.css +70 -0
  147. package/src/css/detail-page.css +235 -0
  148. package/src/css/detail.css +68 -68
  149. package/src/css/index-page.css +44 -0
  150. package/src/css/index-sidebar.css +193 -0
  151. package/src/css/index.css +17 -8
  152. package/src/css/layout.css +90 -90
  153. package/src/css/page-header.css +35 -0
  154. package/src/css/pagination.css +72 -72
  155. package/src/css/table.css +142 -143
  156. package/src/css/timeline-stages.css +220 -0
  157. package/src/css/utilities.css +46 -46
  158. package/src/index.js +91 -50
  159. package/src/mixins/gridLayout.js +118 -0
  160. package/src/store/createCrudStore.js +360 -0
  161. package/src/store/createSubResourcePlugin.js +125 -135
  162. package/src/store/index.js +4 -3
  163. package/src/store/plugins/auditTrails.js +357 -17
  164. package/src/store/plugins/files.js +250 -186
  165. package/src/store/plugins/index.js +7 -4
  166. package/src/store/plugins/lifecycle.js +180 -180
  167. package/src/store/plugins/registerMapping.js +195 -0
  168. package/src/store/plugins/relations.js +68 -68
  169. package/src/store/plugins/search.js +385 -0
  170. package/src/store/plugins/selection.js +104 -0
  171. package/src/store/useObjectStore.js +823 -625
  172. package/src/types/auditTrail.d.ts +32 -32
  173. package/src/types/file.d.ts +23 -23
  174. package/src/types/index.d.ts +35 -35
  175. package/src/types/notification.d.ts +36 -36
  176. package/src/types/object.d.ts +40 -40
  177. package/src/types/organisation.d.ts +41 -41
  178. package/src/types/register.d.ts +25 -25
  179. package/src/types/schema.d.ts +39 -39
  180. package/src/types/shared.d.ts +79 -79
  181. package/src/types/source.d.ts +14 -14
  182. package/src/types/task.d.ts +31 -31
  183. package/src/utils/errors.js +96 -96
  184. package/src/utils/getTheme.js +9 -0
  185. package/src/utils/headers.js +80 -44
  186. package/src/utils/id.js +13 -0
  187. package/src/utils/index.js +4 -3
  188. package/src/utils/schema.js +422 -287
  189. package/src/utils/widgetVisibility.js +162 -0
  190. package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +0 -88
  191. package/src/components/CnDetailViewLayout/index.js +0 -1
  192. package/src/components/CnEmptyState/CnEmptyState.vue +0 -78
  193. package/src/components/CnEmptyState/index.js +0 -1
  194. package/src/components/CnListViewLayout/CnListViewLayout.vue +0 -80
  195. package/src/components/CnListViewLayout/index.js +0 -1
  196. package/src/components/CnViewModeToggle/CnViewModeToggle.vue +0 -77
  197. package/src/components/CnViewModeToggle/index.js +0 -1
@@ -0,0 +1,360 @@
1
+ import { defineStore } from 'pinia'
2
+ import { buildHeaders, prefixUrl } from '../utils/headers.js'
3
+ import { parseResponseError } from '../utils/errors.js'
4
+
5
+ /**
6
+ * Default fields stripped from items before POST/PUT.
7
+ * @type {string[]}
8
+ */
9
+ const DEFAULT_CLEAN_FIELDS = ['id', 'uuid', 'created', 'updated']
10
+
11
+ /**
12
+ * Default base URL for the API.
13
+ * @type {string}
14
+ */
15
+ const DEFAULT_BASE_URL = '/apps/openregister/api'
16
+
17
+ /**
18
+ * Default list-response parser — extracts the `results` array.
19
+ *
20
+ * Called with the store as `this`, so custom implementations can
21
+ * perform side-effects (e.g. update extra state from the response).
22
+ *
23
+ * @param {object} json Parsed response body
24
+ * @return {Array} The items array for setList
25
+ */
26
+ function defaultParseListResponse(json) {
27
+ return json.results
28
+ }
29
+
30
+ /**
31
+ * Create a Pinia store with standard CRUD operations.
32
+ *
33
+ * Generates a store with list/item state, pagination, filters, and
34
+ * async actions for fetching, creating, updating, and deleting items.
35
+ * Domain-specific state, getters, and actions can be added via `extend`.
36
+ *
37
+ * @example
38
+ * // Minimal — pure CRUD
39
+ * import { createCrudStore } from '@conduction/nextcloud-vue'
40
+ * import { Source } from '../../entities/index.js'
41
+ *
42
+ * export const useSourceStore = createCrudStore('source', {
43
+ * endpoint: 'sources',
44
+ * entity: Source,
45
+ * })
46
+ *
47
+ * @example
48
+ * // With features and extensions
49
+ * import { createCrudStore } from '@conduction/nextcloud-vue'
50
+ * import { Agent } from '../../entities/index.js'
51
+ *
52
+ * export const useAgentStore = createCrudStore('agent', {
53
+ * endpoint: 'agents',
54
+ * entity: Agent,
55
+ * features: { loading: true, viewMode: true },
56
+ * extend: {
57
+ * actions: {
58
+ * async getStats() {
59
+ * const response = await fetch(this._options.baseApiUrl + '/stats')
60
+ * if (!response.ok) throw new Error('HTTP ' + response.status)
61
+ * return response.json()
62
+ * },
63
+ * },
64
+ * },
65
+ * })
66
+ *
67
+ * @param {string} name Pinia store ID (e.g. 'source', 'agent')
68
+ * @param {object} config Store configuration
69
+ * @param {string} config.endpoint API resource path segment (e.g. 'sources')
70
+ * @param {string} [config.baseUrl] API base URL (before endpoint)
71
+ * @param {Function|null} [config.entity] Entity class constructor for wrapping items, or null for raw data
72
+ * @param {string[]} [config.cleanFields] Fields to strip in cleanForSave
73
+ * @param {object} [config.features] Feature flags to enable optional state/getters/actions
74
+ * @param {boolean} [config.features.loading] Add loading/error state and isLoading/getError getters
75
+ * @param {boolean} [config.features.viewMode] Add viewMode state, getViewMode getter, setViewMode action
76
+ * @param {Function} [config.parseListResponse] Custom response parser for refreshList.
77
+ * Receives the parsed JSON body with the store instance as `this`.
78
+ * Must return an array of items. Default: `(json) => json.results`
79
+ * @param {object} [config.extend] Extra state/getters/actions to merge into the store
80
+ * @param {Function} [config.extend.state] State factory returning extra state properties
81
+ * @param {object} [config.extend.getters] Extra getters (or overrides of base getters)
82
+ * @param {object} [config.extend.actions] Extra actions (or overrides of base actions)
83
+ * @return {Function} Pinia store composable (useXxxStore)
84
+ */
85
+ export function createCrudStore(name, config = {}) {
86
+ const {
87
+ endpoint,
88
+ baseUrl = DEFAULT_BASE_URL,
89
+ entity: Entity = null,
90
+ cleanFields = DEFAULT_CLEAN_FIELDS,
91
+ features = {},
92
+ parseListResponse = defaultParseListResponse,
93
+ extend = {},
94
+ } = config
95
+
96
+ if (!endpoint) {
97
+ throw new Error(`createCrudStore("${name}"): config.endpoint is required`)
98
+ }
99
+
100
+ const baseApiUrl = prefixUrl(`${baseUrl}/${endpoint}`)
101
+
102
+ return defineStore(name, {
103
+ state: () => ({
104
+ // ── Core state ──
105
+ item: null,
106
+ list: [],
107
+ filters: {},
108
+ pagination: { page: 1, limit: 20 },
109
+
110
+ // ── Optional feature state ──
111
+ ...(features.loading ? { loading: false, error: null } : {}),
112
+ ...(features.viewMode ? { viewMode: 'cards' } : {}),
113
+
114
+ // ── Internal config (available to extend actions) ──
115
+ _options: { endpoint, cleanFields, baseApiUrl },
116
+
117
+ // ── Domain-specific state ──
118
+ ...(typeof extend.state === 'function' ? extend.state() : {}),
119
+ }),
120
+
121
+ getters: {
122
+ // ── Optional feature getters ──
123
+ ...(features.viewMode ? { getViewMode: (state) => state.viewMode } : {}),
124
+ ...(features.loading
125
+ ? {
126
+ isLoading: (state) => state.loading,
127
+ getError: (state) => state.error,
128
+ }
129
+ : {}),
130
+
131
+ // ── Domain-specific getters ──
132
+ ...(extend.getters ?? {}),
133
+ },
134
+
135
+ actions: {
136
+ // ── Setters ──
137
+
138
+ /**
139
+ * Set the active item. Wraps in Entity class if configured.
140
+ * @param {object|null} data Raw item data or null to clear
141
+ */
142
+ setItem(data) {
143
+ this.item = data
144
+ ? (Entity ? new Entity(data) : data)
145
+ : null
146
+ },
147
+
148
+ /**
149
+ * Set the item list. Wraps each item in Entity class if configured.
150
+ * @param {Array} data Array of raw item objects
151
+ */
152
+ setList(data) {
153
+ this.list = Entity
154
+ ? data.map((item) => new Entity(item))
155
+ : [...data]
156
+ },
157
+
158
+ /**
159
+ * Set pagination parameters.
160
+ * @param {number} page Current page number
161
+ * @param {number} [limit] Items per page
162
+ */
163
+ setPagination(page, limit = 20) {
164
+ this.pagination = { page, limit }
165
+ },
166
+
167
+ /**
168
+ * Merge filter criteria into the current filters.
169
+ * @param {object} filters Key-value filter pairs to merge
170
+ */
171
+ setFilters(filters) {
172
+ this.filters = { ...this.filters, ...filters }
173
+ },
174
+
175
+ // ── Optional feature actions ──
176
+ ...(features.viewMode
177
+ ? {
178
+ /**
179
+ * Set the view mode (e.g. 'cards', 'table').
180
+ * @param {string} mode View mode identifier
181
+ */
182
+ setViewMode(mode) {
183
+ this.viewMode = mode
184
+ },
185
+ }
186
+ : {}),
187
+
188
+ // ── CRUD actions ──
189
+
190
+ /**
191
+ * Fetch the item list from the API.
192
+ * @param {string|null} [search] Optional search query
193
+ * @param {boolean} [soft] If true, don't toggle loading state
194
+ * @return {Promise<{response: Response, data: Array}>}
195
+ */
196
+ async refreshList(search = null, soft = false) {
197
+ if (features.loading && !soft) {
198
+ this.loading = true
199
+ this.error = null
200
+ }
201
+ try {
202
+ let url = this._options.baseApiUrl
203
+ if (search) {
204
+ url += '?_search=' + encodeURIComponent(search)
205
+ }
206
+ const response = await fetch(url, {
207
+ method: 'GET',
208
+ headers: buildHeaders(),
209
+ })
210
+ if (!response.ok) {
211
+ throw await parseResponseError(response, name)
212
+ }
213
+ const json = await response.json()
214
+ const data = parseListResponse.call(this, json)
215
+ this.setList(data)
216
+ return { response, data }
217
+ } catch (error) {
218
+ if (features.loading) {
219
+ this.error = error.message ?? error.toString()
220
+ }
221
+ throw error
222
+ } finally {
223
+ if (features.loading && !soft) {
224
+ this.loading = false
225
+ }
226
+ }
227
+ },
228
+
229
+ /**
230
+ * Fetch a single item by ID and set it as the active item.
231
+ * @param {string|number} id Item ID or UUID
232
+ * @return {Promise<object>} The fetched item data
233
+ */
234
+ async getOne(id) {
235
+ if (features.loading) {
236
+ this.loading = true
237
+ }
238
+ try {
239
+ const response = await fetch(`${this._options.baseApiUrl}/${id}`, {
240
+ method: 'GET',
241
+ headers: buildHeaders(),
242
+ })
243
+ if (!response.ok) {
244
+ throw await parseResponseError(response, name)
245
+ }
246
+ const data = await response.json()
247
+ this.setItem(data)
248
+ return data
249
+ } catch (error) {
250
+ if (features.loading) {
251
+ this.error = error.message ?? error.toString()
252
+ }
253
+ throw error
254
+ } finally {
255
+ if (features.loading) {
256
+ this.loading = false
257
+ }
258
+ }
259
+ },
260
+
261
+ /**
262
+ * Delete an item by ID. Refreshes the list and clears the active item.
263
+ * @param {object} item Item object (must have .id)
264
+ * @return {Promise<{response: Response}>}
265
+ */
266
+ async deleteOne(item) {
267
+ if (!item.id) {
268
+ throw new Error(`No ${name} to delete`)
269
+ }
270
+ if (features.loading) {
271
+ this.loading = true
272
+ }
273
+ try {
274
+ const response = await fetch(`${this._options.baseApiUrl}/${item.id}`, {
275
+ method: 'DELETE',
276
+ headers: buildHeaders(),
277
+ })
278
+ if (!response.ok) {
279
+ throw await parseResponseError(response, name)
280
+ }
281
+ await this.refreshList()
282
+ this.setItem(null)
283
+ return { response }
284
+ } catch (error) {
285
+ if (features.loading) {
286
+ this.error = error.message ?? error.toString()
287
+ }
288
+ throw error
289
+ } finally {
290
+ if (features.loading) {
291
+ this.loading = false
292
+ }
293
+ }
294
+ },
295
+
296
+ /**
297
+ * Strip read-only fields from an item before saving.
298
+ * Uses the `cleanFields` config array. Override in `extend.actions`
299
+ * for custom cleaning (the configured fields are in `this._options.cleanFields`).
300
+ * @param {object} item Raw item data
301
+ * @return {object} Cleaned copy safe for POST/PUT
302
+ */
303
+ cleanForSave(item) {
304
+ const cleaned = { ...item }
305
+ for (const field of this._options.cleanFields) {
306
+ delete cleaned[field]
307
+ }
308
+ return cleaned
309
+ },
310
+
311
+ /**
312
+ * Create or update an item. Determines method from presence of `.id`.
313
+ * @param {object} item Item data (without .id = create, with .id = update)
314
+ * @return {Promise<{response: Response, data: object}>}
315
+ */
316
+ async save(item) {
317
+ if (!item) {
318
+ throw new Error(`No ${name} to save`)
319
+ }
320
+ if (features.loading) {
321
+ this.loading = true
322
+ }
323
+ const isNew = !item.id
324
+ const url = isNew
325
+ ? this._options.baseApiUrl
326
+ : `${this._options.baseApiUrl}/${item.id}`
327
+ const method = isNew ? 'POST' : 'PUT'
328
+ const body = this.cleanForSave(item)
329
+
330
+ try {
331
+ const response = await fetch(url, {
332
+ method,
333
+ headers: buildHeaders(),
334
+ body: JSON.stringify(body),
335
+ })
336
+ if (!response.ok) {
337
+ throw await parseResponseError(response, name)
338
+ }
339
+ const responseData = await response.json()
340
+ const data = Entity ? new Entity(responseData) : responseData
341
+ this.setItem(data)
342
+ await this.refreshList()
343
+ return { response, data }
344
+ } catch (error) {
345
+ if (features.loading) {
346
+ this.error = error.message ?? error.toString()
347
+ }
348
+ throw error
349
+ } finally {
350
+ if (features.loading) {
351
+ this.loading = false
352
+ }
353
+ }
354
+ },
355
+
356
+ // ── Domain-specific actions (may override base actions) ──
357
+ ...(extend.actions ?? {}),
358
+ },
359
+ })
360
+ }
@@ -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'