@conduction/nextcloud-vue 0.1.0-beta.3 → 0.1.0-beta.4

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 (152) hide show
  1. package/README.md +226 -226
  2. package/dist/nextcloud-vue.cjs.js +60455 -8755
  3. package/dist/nextcloud-vue.cjs.js.map +1 -1
  4. package/dist/nextcloud-vue.css +2062 -528
  5. package/dist/nextcloud-vue.esm.js +60411 -8731
  6. package/dist/nextcloud-vue.esm.js.map +1 -1
  7. package/package.json +75 -62
  8. package/src/components/CnActionsBar/CnActionsBar.vue +235 -225
  9. package/src/components/CnActionsBar/index.js +1 -1
  10. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -0
  11. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  12. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  13. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -0
  14. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  15. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  16. package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
  17. package/src/components/CnCardGrid/index.js +1 -1
  18. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  19. package/src/components/CnCellRenderer/index.js +1 -1
  20. package/src/components/CnChartWidget/CnChartWidget.vue +320 -0
  21. package/src/components/CnChartWidget/index.js +1 -0
  22. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  23. package/src/components/CnConfigurationCard/index.js +1 -1
  24. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -0
  25. package/src/components/CnDashboardGrid/index.js +1 -0
  26. package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -0
  27. package/src/components/CnDashboardPage/index.js +1 -0
  28. package/src/components/CnDataTable/CnDataTable.vue +349 -349
  29. package/src/components/CnDataTable/index.js +1 -1
  30. package/src/components/CnDetailCard/CnDetailCard.vue +214 -0
  31. package/src/components/CnDetailCard/index.js +1 -0
  32. package/src/components/CnDetailPage/CnDetailPage.vue +281 -0
  33. package/src/components/CnDetailPage/index.js +1 -0
  34. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -223
  35. package/src/components/CnFacetSidebar/index.js +1 -1
  36. package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
  37. package/src/components/CnFilterBar/index.js +1 -1
  38. package/src/components/CnIcon/CnIcon.vue +89 -89
  39. package/src/components/CnIcon/index.js +1 -1
  40. package/src/components/CnIndexPage/CnIndexPage.vue +874 -816
  41. package/src/components/CnIndexPage/index.js +1 -1
  42. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -484
  43. package/src/components/CnIndexSidebar/index.js +1 -1
  44. package/src/components/CnItemCard/CnItemCard.vue +132 -0
  45. package/src/components/CnItemCard/index.js +1 -0
  46. package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
  47. package/src/components/CnKpiGrid/index.js +1 -1
  48. package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
  49. package/src/components/CnMassActionBar/index.js +1 -1
  50. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
  51. package/src/components/CnMassCopyDialog/index.js +1 -1
  52. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
  53. package/src/components/CnMassDeleteDialog/index.js +1 -1
  54. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
  55. package/src/components/CnMassExportDialog/index.js +1 -1
  56. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
  57. package/src/components/CnMassImportDialog/index.js +1 -1
  58. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  59. package/src/components/CnNoteCard/index.js +1 -0
  60. package/src/components/CnNotesCard/CnNotesCard.vue +413 -0
  61. package/src/components/CnNotesCard/index.js +1 -0
  62. package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
  63. package/src/components/CnObjectCard/index.js +1 -1
  64. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -0
  65. package/src/components/CnObjectSidebar/index.js +1 -0
  66. package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
  67. package/src/components/CnPageHeader/index.js +1 -1
  68. package/src/components/CnPagination/CnPagination.vue +252 -252
  69. package/src/components/CnPagination/index.js +1 -1
  70. package/src/components/CnRowActions/CnRowActions.vue +73 -73
  71. package/src/components/CnRowActions/index.js +1 -1
  72. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  73. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
  74. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  75. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  76. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  77. package/src/components/CnSchemaFormDialog/index.js +1 -0
  78. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  79. package/src/components/CnSettingsCard/index.js +1 -1
  80. package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
  81. package/src/components/CnSettingsSection/index.js +1 -1
  82. package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -366
  83. package/src/components/CnStatsBlock/index.js +1 -1
  84. package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
  85. package/src/components/CnStatusBadge/index.js +1 -1
  86. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -0
  87. package/src/components/CnTabbedFormDialog/index.js +1 -0
  88. package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
  89. package/src/components/CnTasksCard/index.js +1 -0
  90. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  91. package/src/components/CnTileWidget/index.js +1 -0
  92. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
  93. package/src/components/CnTimelineStages/index.js +1 -0
  94. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
  95. package/src/components/CnUserActionMenu/index.js +1 -0
  96. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
  97. package/src/components/CnVersionInfoCard/index.js +1 -1
  98. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  99. package/src/components/CnWidgetRenderer/index.js +1 -0
  100. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -0
  101. package/src/components/CnWidgetWrapper/index.js +1 -0
  102. package/src/components/index.js +43 -29
  103. package/src/composables/index.js +4 -3
  104. package/src/composables/useDashboardView.js +240 -0
  105. package/src/composables/useDetailView.js +289 -132
  106. package/src/composables/useListView.js +363 -362
  107. package/src/composables/useSubResource.js +142 -142
  108. package/src/constants/metadata.js +30 -30
  109. package/src/css/CnSchemaFormDialog.css +546 -0
  110. package/src/css/__sample_nextcloud_tokens.css +110 -0
  111. package/src/css/actions-bar.css +48 -48
  112. package/src/css/badge.css +51 -51
  113. package/src/css/card.css +128 -128
  114. package/src/css/dashboard.css +70 -0
  115. package/src/css/detail-page.css +168 -0
  116. package/src/css/detail.css +68 -68
  117. package/src/css/index-page.css +44 -32
  118. package/src/css/index-sidebar.css +193 -187
  119. package/src/css/index.css +16 -12
  120. package/src/css/layout.css +90 -90
  121. package/src/css/page-header.css +33 -33
  122. package/src/css/pagination.css +72 -72
  123. package/src/css/table.css +142 -142
  124. package/src/css/timeline-stages.css +218 -0
  125. package/src/css/utilities.css +46 -46
  126. package/src/index.js +72 -53
  127. package/src/store/createSubResourcePlugin.js +135 -135
  128. package/src/store/index.js +3 -3
  129. package/src/store/plugins/auditTrails.js +17 -17
  130. package/src/store/plugins/files.js +250 -186
  131. package/src/store/plugins/index.js +7 -5
  132. package/src/store/plugins/lifecycle.js +180 -180
  133. package/src/store/plugins/relations.js +68 -68
  134. package/src/store/plugins/search.js +372 -0
  135. package/src/store/plugins/selection.js +104 -0
  136. package/src/store/useObjectStore.js +829 -686
  137. package/src/types/auditTrail.d.ts +32 -32
  138. package/src/types/file.d.ts +23 -23
  139. package/src/types/index.d.ts +35 -35
  140. package/src/types/notification.d.ts +36 -36
  141. package/src/types/object.d.ts +40 -40
  142. package/src/types/organisation.d.ts +41 -41
  143. package/src/types/register.d.ts +25 -25
  144. package/src/types/schema.d.ts +39 -39
  145. package/src/types/shared.d.ts +79 -79
  146. package/src/types/source.d.ts +14 -14
  147. package/src/types/task.d.ts +31 -31
  148. package/src/utils/errors.js +96 -96
  149. package/src/utils/headers.js +68 -50
  150. package/src/utils/id.js +13 -0
  151. package/src/utils/index.js +3 -3
  152. package/src/utils/schema.js +422 -419
@@ -0,0 +1,240 @@
1
+ import { ref, computed, onMounted } from 'vue'
2
+ import axios from '@nextcloud/axios'
3
+ import { generateOcsUrl } from '@nextcloud/router'
4
+
5
+ /**
6
+ * Composable for managing dashboard view state.
7
+ *
8
+ * Handles widget definition loading (including NC Dashboard API widgets),
9
+ * layout management, and edit mode. Apps provide their own widget
10
+ * definitions and persist layouts however they choose (app config,
11
+ * OpenRegister objects, etc.).
12
+ *
13
+ * @param {object} [options] Configuration options
14
+ * @param {Array} [options.widgets=[]] Static widget definitions from the app
15
+ * @param {Array} [options.defaultLayout=[]] Default layout if no saved layout exists
16
+ * @param {Function} [options.loadLayout] Async function that returns saved layout array, or null
17
+ * @param {Function} [options.saveLayout] Async function that persists layout: (layout) => Promise
18
+ * @param {boolean} [options.includeNcWidgets=false] Whether to also load NC Dashboard API widgets
19
+ * @param {number} [options.columns=12] Grid columns
20
+ * @return {object} Reactive state and methods for CnDashboardPage
21
+ *
22
+ * @example Basic usage with static widgets
23
+ * const { widgets, layout, loading, onLayoutChange } = useDashboardView({
24
+ * widgets: [
25
+ * { id: 'kpis', title: 'KPIs', type: 'custom' },
26
+ * { id: 'chart', title: 'Status Chart', type: 'custom' },
27
+ * ],
28
+ * defaultLayout: [
29
+ * { id: 1, widgetId: 'kpis', gridX: 0, gridY: 0, gridWidth: 12, gridHeight: 2 },
30
+ * { id: 2, widgetId: 'chart', gridX: 0, gridY: 2, gridWidth: 6, gridHeight: 4 },
31
+ * ],
32
+ * })
33
+ *
34
+ * @example With persistence and NC widgets
35
+ * const dashboard = useDashboardView({
36
+ * widgets: myWidgets,
37
+ * defaultLayout: defaultLayout,
38
+ * loadLayout: () => fetch('/api/dashboard-layout').then(r => r.json()),
39
+ * saveLayout: (layout) => fetch('/api/dashboard-layout', { method: 'PUT', body: JSON.stringify(layout) }),
40
+ * includeNcWidgets: true,
41
+ * })
42
+ */
43
+ export function useDashboardView(options = {}) {
44
+ const opts = {
45
+ widgets: [],
46
+ defaultLayout: [],
47
+ loadLayout: null,
48
+ saveLayout: null,
49
+ includeNcWidgets: false,
50
+ columns: 12,
51
+ ...options,
52
+ }
53
+
54
+ // ── State ────────────────────────────────────────────────────────────
55
+ const appWidgets = ref(opts.widgets)
56
+ const ncWidgets = ref([])
57
+ const layout = ref([])
58
+ const loading = ref(false)
59
+ const saving = ref(false)
60
+ const isEditing = ref(false)
61
+
62
+ // ── Computed ─────────────────────────────────────────────────────────
63
+
64
+ /** All available widgets (app + NC Dashboard API) */
65
+ const widgets = computed(() => {
66
+ return [...appWidgets.value, ...ncWidgets.value]
67
+ })
68
+
69
+ /** Widget IDs currently on the dashboard */
70
+ const activeWidgetIds = computed(() => {
71
+ return layout.value.map(item => item.widgetId)
72
+ })
73
+
74
+ /** Widgets not yet placed on the dashboard */
75
+ const availableWidgets = computed(() => {
76
+ return widgets.value.filter(w => !activeWidgetIds.value.includes(w.id))
77
+ })
78
+
79
+ // ── Methods ──────────────────────────────────────────────────────────
80
+
81
+ /**
82
+ * Load NC Dashboard API widgets from the OCS endpoint.
83
+ */
84
+ async function loadNcWidgets() {
85
+ try {
86
+ const url = generateOcsUrl('/apps/dashboard/api/v1/widgets')
87
+ const response = await axios.get(url)
88
+ const data = response.data?.ocs?.data || {}
89
+
90
+ ncWidgets.value = Object.values(data).map(w => ({
91
+ id: w.id,
92
+ title: w.title,
93
+ iconClass: w.icon_class,
94
+ iconUrl: w.icon_url,
95
+ widgetUrl: w.widget_url,
96
+ itemApiVersions: w.item_api_versions || [],
97
+ itemIconsRound: w.item_icons_round || false,
98
+ reloadInterval: w.reload_interval || 0,
99
+ buttons: w.buttons || [],
100
+ type: 'nc-widget',
101
+ }))
102
+ } catch (error) {
103
+ console.error('[useDashboardView] Failed to load NC widgets:', error)
104
+ ncWidgets.value = []
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Initialize the dashboard: load layout and optionally NC widgets.
110
+ */
111
+ async function init() {
112
+ loading.value = true
113
+ try {
114
+ const tasks = []
115
+
116
+ if (opts.includeNcWidgets) {
117
+ tasks.push(loadNcWidgets())
118
+ }
119
+
120
+ if (opts.loadLayout) {
121
+ tasks.push(
122
+ opts.loadLayout().then(saved => {
123
+ if (saved && saved.length > 0) {
124
+ layout.value = saved
125
+ } else {
126
+ layout.value = [...opts.defaultLayout]
127
+ }
128
+ }),
129
+ )
130
+ } else {
131
+ layout.value = [...opts.defaultLayout]
132
+ }
133
+
134
+ await Promise.all(tasks)
135
+ } catch (error) {
136
+ console.error('[useDashboardView] Init failed:', error)
137
+ layout.value = [...opts.defaultLayout]
138
+ } finally {
139
+ loading.value = false
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Handle layout change from the grid. Persists if saveLayout is provided.
145
+ *
146
+ * @param {Array} newLayout Updated layout array
147
+ */
148
+ async function onLayoutChange(newLayout) {
149
+ layout.value = newLayout
150
+
151
+ if (opts.saveLayout) {
152
+ saving.value = true
153
+ try {
154
+ await opts.saveLayout(newLayout)
155
+ } catch (error) {
156
+ console.error('[useDashboardView] Failed to save layout:', error)
157
+ } finally {
158
+ saving.value = false
159
+ }
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Add a widget to the dashboard at the next available position.
165
+ *
166
+ * @param {string} widgetId Widget ID to add
167
+ * @param {object} [position] Override position { gridX, gridY, gridWidth, gridHeight }
168
+ */
169
+ function addWidget(widgetId, position = {}) {
170
+ const maxId = layout.value.reduce((max, item) => {
171
+ const num = typeof item.id === 'number' ? item.id : 0
172
+ return num > max ? num : max
173
+ }, 0)
174
+
175
+ const maxY = layout.value.reduce((max, item) => {
176
+ const bottom = (item.gridY || 0) + (item.gridHeight || 3)
177
+ return bottom > max ? bottom : max
178
+ }, 0)
179
+
180
+ const newItem = {
181
+ id: maxId + 1,
182
+ widgetId,
183
+ gridX: position.gridX ?? 0,
184
+ gridY: position.gridY ?? maxY,
185
+ gridWidth: position.gridWidth ?? 6,
186
+ gridHeight: position.gridHeight ?? 3,
187
+ }
188
+
189
+ const newLayout = [...layout.value, newItem]
190
+ onLayoutChange(newLayout)
191
+ }
192
+
193
+ /**
194
+ * Remove a widget from the dashboard by layout item ID.
195
+ *
196
+ * @param {string|number} itemId Layout item ID to remove
197
+ */
198
+ function removeWidget(itemId) {
199
+ const newLayout = layout.value.filter(item => item.id !== itemId)
200
+ onLayoutChange(newLayout)
201
+ }
202
+
203
+ /**
204
+ * Update app widget definitions (e.g., when data changes).
205
+ *
206
+ * @param {Array} newWidgets Updated widget definitions
207
+ */
208
+ function setWidgets(newWidgets) {
209
+ appWidgets.value = newWidgets
210
+ }
211
+
212
+ // ── Lifecycle ────────────────────────────────────────────────────────
213
+
214
+ onMounted(() => {
215
+ init()
216
+ })
217
+
218
+ // ── Return ───────────────────────────────────────────────────────────
219
+
220
+ return {
221
+ // State
222
+ widgets,
223
+ layout,
224
+ loading,
225
+ saving,
226
+ isEditing,
227
+
228
+ // Derived
229
+ activeWidgetIds,
230
+ availableWidgets,
231
+ ncWidgets,
232
+
233
+ // Methods
234
+ onLayoutChange,
235
+ addWidget,
236
+ removeWidget,
237
+ setWidgets,
238
+ init,
239
+ }
240
+ }
@@ -1,132 +1,289 @@
1
- import { ref } from 'vue'
2
-
3
- /**
4
- * Composable for managing detail view state: loading, editing, deleting.
5
- *
6
- * Extracts the load/edit/delete pattern found in every detail view
7
- * across Pipelinq and Procest.
8
- *
9
- * @param {object} options Configuration options
10
- * @param {string} options.objectType The registered object type slug
11
- * @param {Function} options.fetchFn (type, id) => Promise<object>
12
- * @param {Function} options.saveFn (type, data) => Promise<object>
13
- * @param {Function} options.deleteFn (type, id) => Promise<boolean>
14
- * @param {Function} [options.onSaved] Callback after successful save
15
- * @param {Function} [options.onDeleted] Callback after successful delete
16
- * @return {object} Reactive state and methods
17
- *
18
- * @example
19
- * import { useDetailView } from '@conduction/nextcloud-vue'
20
- *
21
- * const { objectData, editing, loading, showDeleteDialog, load, save, confirmDelete, executeDelete } = useDetailView({
22
- * objectType: 'client',
23
- * fetchFn: (type, id) => objectStore.fetchObject(type, id),
24
- * saveFn: (type, data) => objectStore.saveObject(type, data),
25
- * deleteFn: (type, id) => objectStore.deleteObject(type, id),
26
- * onSaved: (result) => router.push(`/clients/${result.id}`),
27
- * onDeleted: () => router.push('/clients'),
28
- * })
29
- */
30
- export function useDetailView(options) {
31
- const objectData = ref({})
32
- const editing = ref(false)
33
- const loading = ref(false)
34
- const saving = ref(false)
35
- const showDeleteDialog = ref(false)
36
- const error = ref(null)
37
-
38
- /**
39
- * Load an object by ID.
40
- * @param {string} id Object ID
41
- * @return {Promise<object|null>}
42
- */
43
- async function load(id) {
44
- loading.value = true
45
- error.value = null
46
- try {
47
- const result = await options.fetchFn(options.objectType, id)
48
- if (result) {
49
- objectData.value = { ...result }
50
- }
51
- return result
52
- } catch (err) {
53
- error.value = err.message || 'Failed to load'
54
- return null
55
- } finally {
56
- loading.value = false
57
- }
58
- }
59
-
60
- /**
61
- * Save the current object data (create or update).
62
- * @param {object} [formData] Optional form data override (defaults to objectData)
63
- * @return {Promise<object|null>}
64
- */
65
- async function save(formData) {
66
- saving.value = true
67
- error.value = null
68
- try {
69
- const data = formData || objectData.value
70
- const result = await options.saveFn(options.objectType, data)
71
- if (result) {
72
- objectData.value = { ...result }
73
- editing.value = false
74
- if (options.onSaved) {
75
- options.onSaved(result)
76
- }
77
- }
78
- return result
79
- } catch (err) {
80
- error.value = err.message || 'Failed to save'
81
- return null
82
- } finally {
83
- saving.value = false
84
- }
85
- }
86
-
87
- /**
88
- * Show the delete confirmation dialog.
89
- */
90
- function confirmDelete() {
91
- showDeleteDialog.value = true
92
- }
93
-
94
- /**
95
- * Execute the delete operation.
96
- * @param {string} [id] Object ID (defaults to objectData.id)
97
- * @return {Promise<boolean>}
98
- */
99
- async function executeDelete(id) {
100
- const deleteId = id || objectData.value.id
101
- loading.value = true
102
- error.value = null
103
- try {
104
- const success = await options.deleteFn(options.objectType, deleteId)
105
- if (success) {
106
- showDeleteDialog.value = false
107
- if (options.onDeleted) {
108
- options.onDeleted()
109
- }
110
- }
111
- return success
112
- } catch (err) {
113
- error.value = err.message || 'Failed to delete'
114
- return false
115
- } finally {
116
- loading.value = false
117
- }
118
- }
119
-
120
- return {
121
- objectData,
122
- editing,
123
- loading,
124
- saving,
125
- showDeleteDialog,
126
- error,
127
- load,
128
- save,
129
- confirmDelete,
130
- executeDelete,
131
- }
132
- }
1
+ import { ref, computed, isRef, watch, onMounted } from 'vue'
2
+ import { useObjectStore } from '../store/index.js'
3
+
4
+ /**
5
+ * Composable for managing detail view state with full objectStore integration.
6
+ *
7
+ * When called with `objectType` and `id`, connects to the objectStore and handles
8
+ * fetching on mount, re-fetching on `id` changes, save/delete operations, and
9
+ * optional router navigation eliminating boilerplate from every detail-view component.
10
+ *
11
+ * Backward-compatible: existing `useDetailView(options)` and `useDetailView()` calls
12
+ * continue to work without modification.
13
+ *
14
+ * @param {string|object} [objectTypeOrOptions] Object type slug (new API) or legacy options object
15
+ * @param {string|import('vue').Ref<string>} [id] Object ID or `'new'` for a new object
16
+ * @param {object} [options] Options (new API only)
17
+ * @param {object|null} [options.router] Vue Router instance — enables post-save/delete navigation
18
+ * @param {string|null} [options.listRouteName] Route name to navigate to after successful delete
19
+ * @param {string|null} [options.detailRouteName] Route name to navigate to after successful create
20
+ * @param {string} [options.nameField='title'] Field shown in error messages
21
+ * @return {object} Reactive state and operation functions
22
+ *
23
+ * @example
24
+ * // New API
25
+ * const { object, isNew, loading, saving, editing,
26
+ * onSave, confirmDelete, showDeleteDialog } = useDetailView('client', props.id, {
27
+ * router: useRouter(),
28
+ * listRouteName: 'ClientList',
29
+ * detailRouteName: 'ClientDetail',
30
+ * })
31
+ *
32
+ * @example
33
+ * // Legacy API — still works
34
+ * const { objectData, editing, load, save, confirmDelete } = useDetailView({
35
+ * objectType: 'client',
36
+ * fetchFn: (type, id) => objectStore.fetchObject(type, id),
37
+ * saveFn: (type, data) => objectStore.saveObject(type, data),
38
+ * deleteFn: (type, id) => objectStore.deleteObject(type, id),
39
+ * })
40
+ */
41
+ export function useDetailView(objectTypeOrOptions, id, options) {
42
+ // Backward compat: if first arg is an object or absent, delegate to legacy implementation
43
+ if (!objectTypeOrOptions || typeof objectTypeOrOptions === 'object') {
44
+ return useLegacyDetailView(objectTypeOrOptions || {})
45
+ }
46
+
47
+ // ── New API ──────────────────────────────────────────────────────────
48
+ const objectType = objectTypeOrOptions
49
+ const opts = options || {}
50
+ const router = opts.router || null
51
+ const listRouteName = opts.listRouteName || null
52
+ const detailRouteName = opts.detailRouteName || null
53
+
54
+ // Normalise `id` to a ref so we can watch it
55
+ const idRef = isRef(id) ? id : ref(id)
56
+
57
+ const objectStore = useObjectStore()
58
+
59
+ // ── State refs ───────────────────────────────────────────────────────
60
+ const editing = ref(false)
61
+ const saving = ref(false)
62
+ const showDeleteDialog = ref(false)
63
+ const error = ref(null)
64
+ const validationErrors = ref(null)
65
+
66
+ // ── Computed refs from the store ─────────────────────────────────────
67
+
68
+ const isNew = computed(() => !idRef.value || idRef.value === 'new')
69
+
70
+ const object = computed(() => {
71
+ if (isNew.value) return {}
72
+ return objectStore.getObject(objectType, idRef.value) || {}
73
+ })
74
+
75
+ const loading = computed(() => objectStore.loading[objectType] || false)
76
+
77
+ // ── Operations ───────────────────────────────────────────────────────
78
+
79
+ /**
80
+ * Save the object. Handles create (POST) and update (PUT) branches.
81
+ * On 422 validation error the `validationErrors` ref is populated.
82
+ * On successful create with `detailRouteName` set, the router navigates to the detail route.
83
+ * On successful update, `editing` is set to false and the object is re-fetched.
84
+ *
85
+ * @param {object} formData Data to save
86
+ * @return {Promise<object|null>} Saved object or null on error
87
+ */
88
+ async function onSave(formData) {
89
+ saving.value = true
90
+ error.value = null
91
+ validationErrors.value = null
92
+
93
+ try {
94
+ const dataToSave = isNew.value ? formData : { ...formData, id: idRef.value }
95
+ const result = await objectStore.saveObject(objectType, dataToSave)
96
+
97
+ if (!result) {
98
+ // Store already set error; surface it
99
+ error.value = objectStore.errors[objectType]?.message || 'Failed to save'
100
+ return null
101
+ }
102
+
103
+ if (isNew.value && router && detailRouteName) {
104
+ router.push({ name: detailRouteName, params: { id: result.id } })
105
+ } else {
106
+ editing.value = false
107
+ await objectStore.fetchObject(objectType, idRef.value)
108
+ }
109
+
110
+ return result
111
+ } catch (err) {
112
+ if (err?.response?.status === 422 || err?.status === 422) {
113
+ const data = err?.response?.data || err?.data || {}
114
+ validationErrors.value = data.errors || data
115
+ } else {
116
+ error.value = err.message || 'Failed to save'
117
+ }
118
+ return null
119
+ } finally {
120
+ saving.value = false
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Delete the current object. On success, navigate to `listRouteName` if configured.
126
+ * On failure, `error` ref is set.
127
+ *
128
+ * @return {Promise<boolean>} True if deleted successfully
129
+ */
130
+ async function confirmDelete() {
131
+ error.value = null
132
+ try {
133
+ const success = await objectStore.deleteObject(objectType, idRef.value)
134
+ if (success) {
135
+ showDeleteDialog.value = false
136
+ if (router && listRouteName) {
137
+ router.push({ name: listRouteName })
138
+ }
139
+ } else {
140
+ error.value = objectStore.errors[objectType]?.message || 'Failed to delete'
141
+ }
142
+ return success
143
+ } catch (err) {
144
+ error.value = err.message || 'Failed to delete'
145
+ return false
146
+ }
147
+ }
148
+
149
+ // ── Lifecycle ────────────────────────────────────────────────────────
150
+
151
+ async function fetchIfNeeded(currentId) {
152
+ if (!currentId || currentId === 'new') return
153
+ await objectStore.fetchObject(objectType, currentId)
154
+ }
155
+
156
+ onMounted(() => {
157
+ fetchIfNeeded(idRef.value)
158
+ })
159
+
160
+ watch(idRef, (newId) => {
161
+ fetchIfNeeded(newId)
162
+ })
163
+
164
+ // ── Return value ─────────────────────────────────────────────────────
165
+
166
+ return {
167
+ // Store-derived
168
+ object,
169
+ loading,
170
+ // Computed state
171
+ isNew,
172
+ // Local state
173
+ editing,
174
+ saving,
175
+ showDeleteDialog,
176
+ error,
177
+ validationErrors,
178
+ // Operations
179
+ onSave,
180
+ confirmDelete,
181
+ }
182
+ }
183
+
184
+ // ── Legacy implementation ─────────────────────────────────────────────────────
185
+
186
+ /**
187
+ * Legacy `useDetailView(options)` implementation.
188
+ * Preserved verbatim for backward compatibility.
189
+ *
190
+ * @param {object} options Legacy options object
191
+ * @param {string} [options.objectType] The registered object type slug
192
+ * @param {Function} [options.fetchFn] (type, id) => Promise<object>
193
+ * @param {Function} [options.saveFn] (type, data) => Promise<object>
194
+ * @param {Function} [options.deleteFn] (type, id) => Promise<boolean>
195
+ * @param {Function} [options.onSaved] Callback after successful save
196
+ * @param {Function} [options.onDeleted] Callback after successful delete
197
+ * @return {object} Reactive state and methods
198
+ */
199
+ function useLegacyDetailView(options) {
200
+ const objectData = ref({})
201
+ const editing = ref(false)
202
+ const loading = ref(false)
203
+ const saving = ref(false)
204
+ const showDeleteDialog = ref(false)
205
+ const error = ref(null)
206
+
207
+ async function load(id) {
208
+ loading.value = true
209
+ error.value = null
210
+ try {
211
+ const result = options.fetchFn
212
+ ? await options.fetchFn(options.objectType, id)
213
+ : null
214
+ if (result) {
215
+ objectData.value = { ...result }
216
+ }
217
+ return result
218
+ } catch (err) {
219
+ error.value = err.message || 'Failed to load'
220
+ return null
221
+ } finally {
222
+ loading.value = false
223
+ }
224
+ }
225
+
226
+ async function save(formData) {
227
+ saving.value = true
228
+ error.value = null
229
+ try {
230
+ const data = formData || objectData.value
231
+ const result = options.saveFn
232
+ ? await options.saveFn(options.objectType, data)
233
+ : null
234
+ if (result) {
235
+ objectData.value = { ...result }
236
+ editing.value = false
237
+ if (options.onSaved) {
238
+ options.onSaved(result)
239
+ }
240
+ }
241
+ return result
242
+ } catch (err) {
243
+ error.value = err.message || 'Failed to save'
244
+ return null
245
+ } finally {
246
+ saving.value = false
247
+ }
248
+ }
249
+
250
+ function confirmDelete() {
251
+ showDeleteDialog.value = true
252
+ }
253
+
254
+ async function executeDelete(id) {
255
+ const deleteId = id || objectData.value.id
256
+ loading.value = true
257
+ error.value = null
258
+ try {
259
+ const success = options.deleteFn
260
+ ? await options.deleteFn(options.objectType, deleteId)
261
+ : false
262
+ if (success) {
263
+ showDeleteDialog.value = false
264
+ if (options.onDeleted) {
265
+ options.onDeleted()
266
+ }
267
+ }
268
+ return success
269
+ } catch (err) {
270
+ error.value = err.message || 'Failed to delete'
271
+ return false
272
+ } finally {
273
+ loading.value = false
274
+ }
275
+ }
276
+
277
+ return {
278
+ objectData,
279
+ editing,
280
+ loading,
281
+ saving,
282
+ showDeleteDialog,
283
+ error,
284
+ load,
285
+ save,
286
+ confirmDelete,
287
+ executeDelete,
288
+ }
289
+ }