@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.
- package/README.md +226 -226
- package/dist/nextcloud-vue.cjs.js +60455 -8755
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +2062 -528
- package/dist/nextcloud-vue.esm.js +60411 -8731
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +75 -62
- package/src/components/CnActionsBar/CnActionsBar.vue +235 -225
- package/src/components/CnActionsBar/index.js +1 -1
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -0
- package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
- package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -0
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
- package/src/components/CnAdvancedFormDialog/index.js +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
- package/src/components/CnCardGrid/index.js +1 -1
- package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
- package/src/components/CnCellRenderer/index.js +1 -1
- package/src/components/CnChartWidget/CnChartWidget.vue +320 -0
- package/src/components/CnChartWidget/index.js +1 -0
- package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
- package/src/components/CnConfigurationCard/index.js +1 -1
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -0
- package/src/components/CnDashboardGrid/index.js +1 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -0
- package/src/components/CnDashboardPage/index.js +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +349 -349
- package/src/components/CnDataTable/index.js +1 -1
- package/src/components/CnDetailCard/CnDetailCard.vue +214 -0
- package/src/components/CnDetailCard/index.js +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +281 -0
- package/src/components/CnDetailPage/index.js +1 -0
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -223
- package/src/components/CnFacetSidebar/index.js +1 -1
- package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
- package/src/components/CnFilterBar/index.js +1 -1
- package/src/components/CnIcon/CnIcon.vue +89 -89
- package/src/components/CnIcon/index.js +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +874 -816
- package/src/components/CnIndexPage/index.js +1 -1
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -484
- package/src/components/CnIndexSidebar/index.js +1 -1
- package/src/components/CnItemCard/CnItemCard.vue +132 -0
- package/src/components/CnItemCard/index.js +1 -0
- package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
- package/src/components/CnKpiGrid/index.js +1 -1
- package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
- package/src/components/CnMassActionBar/index.js +1 -1
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
- package/src/components/CnMassCopyDialog/index.js +1 -1
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
- package/src/components/CnMassDeleteDialog/index.js +1 -1
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
- package/src/components/CnMassExportDialog/index.js +1 -1
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
- package/src/components/CnMassImportDialog/index.js +1 -1
- package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
- package/src/components/CnNoteCard/index.js +1 -0
- package/src/components/CnNotesCard/CnNotesCard.vue +413 -0
- package/src/components/CnNotesCard/index.js +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
- package/src/components/CnObjectCard/index.js +1 -1
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -0
- package/src/components/CnObjectSidebar/index.js +1 -0
- package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
- package/src/components/CnPageHeader/index.js +1 -1
- package/src/components/CnPagination/CnPagination.vue +252 -252
- package/src/components/CnPagination/index.js +1 -1
- package/src/components/CnRowActions/CnRowActions.vue +73 -73
- package/src/components/CnRowActions/index.js +1 -1
- package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
- package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
- package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
- package/src/components/CnSchemaFormDialog/index.js +1 -0
- package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
- package/src/components/CnSettingsCard/index.js +1 -1
- package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
- package/src/components/CnSettingsSection/index.js +1 -1
- package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -366
- package/src/components/CnStatsBlock/index.js +1 -1
- package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
- package/src/components/CnStatusBadge/index.js +1 -1
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -0
- package/src/components/CnTabbedFormDialog/index.js +1 -0
- package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
- package/src/components/CnTasksCard/index.js +1 -0
- package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
- package/src/components/CnTileWidget/index.js +1 -0
- package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
- package/src/components/CnTimelineStages/index.js +1 -0
- package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
- package/src/components/CnUserActionMenu/index.js +1 -0
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
- package/src/components/CnVersionInfoCard/index.js +1 -1
- package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
- package/src/components/CnWidgetRenderer/index.js +1 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -0
- package/src/components/CnWidgetWrapper/index.js +1 -0
- package/src/components/index.js +43 -29
- package/src/composables/index.js +4 -3
- package/src/composables/useDashboardView.js +240 -0
- package/src/composables/useDetailView.js +289 -132
- package/src/composables/useListView.js +363 -362
- package/src/composables/useSubResource.js +142 -142
- package/src/constants/metadata.js +30 -30
- package/src/css/CnSchemaFormDialog.css +546 -0
- package/src/css/__sample_nextcloud_tokens.css +110 -0
- package/src/css/actions-bar.css +48 -48
- package/src/css/badge.css +51 -51
- package/src/css/card.css +128 -128
- package/src/css/dashboard.css +70 -0
- package/src/css/detail-page.css +168 -0
- package/src/css/detail.css +68 -68
- package/src/css/index-page.css +44 -32
- package/src/css/index-sidebar.css +193 -187
- package/src/css/index.css +16 -12
- package/src/css/layout.css +90 -90
- package/src/css/page-header.css +33 -33
- package/src/css/pagination.css +72 -72
- package/src/css/table.css +142 -142
- package/src/css/timeline-stages.css +218 -0
- package/src/css/utilities.css +46 -46
- package/src/index.js +72 -53
- package/src/store/createSubResourcePlugin.js +135 -135
- package/src/store/index.js +3 -3
- package/src/store/plugins/auditTrails.js +17 -17
- package/src/store/plugins/files.js +250 -186
- package/src/store/plugins/index.js +7 -5
- package/src/store/plugins/lifecycle.js +180 -180
- package/src/store/plugins/relations.js +68 -68
- package/src/store/plugins/search.js +372 -0
- package/src/store/plugins/selection.js +104 -0
- package/src/store/useObjectStore.js +829 -686
- package/src/types/auditTrail.d.ts +32 -32
- package/src/types/file.d.ts +23 -23
- package/src/types/index.d.ts +35 -35
- package/src/types/notification.d.ts +36 -36
- package/src/types/object.d.ts +40 -40
- package/src/types/organisation.d.ts +41 -41
- package/src/types/register.d.ts +25 -25
- package/src/types/schema.d.ts +39 -39
- package/src/types/shared.d.ts +79 -79
- package/src/types/source.d.ts +14 -14
- package/src/types/task.d.ts +31 -31
- package/src/utils/errors.js +96 -96
- package/src/utils/headers.js +68 -50
- package/src/utils/id.js +13 -0
- package/src/utils/index.js +3 -3
- 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
|
-
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* @param {
|
|
15
|
-
* @param {
|
|
16
|
-
* @
|
|
17
|
-
*
|
|
18
|
-
* @
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
}
|