@stonecrop/desktop 0.4.37 → 0.5.0
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/dist/desktop.css +1 -1
- package/dist/desktop.d.ts +3 -5
- package/dist/desktop.js +2626 -8835
- package/dist/desktop.js.map +1 -1
- package/dist/desktop.umd.cjs +84 -17
- package/dist/desktop.umd.cjs.map +1 -1
- package/dist/index.js +2 -3
- package/dist/plugins/index.js +2 -4
- package/dist/src/index.d.ts +2 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/plugins/index.d.ts.map +1 -1
- package/dist/src/types/index.d.ts +1 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/package.json +5 -6
- package/src/components/ActionSet.vue +43 -14
- package/src/components/Desktop.vue +1066 -2
- package/src/components/SheetNav.vue +40 -27
- package/src/index.ts +2 -3
- package/src/plugins/index.ts +2 -4
- package/src/types/index.ts +1 -0
- package/src/components/Doctype.vue +0 -45
- package/src/components/Records.vue +0 -20
|
@@ -1,8 +1,1072 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<
|
|
3
|
-
|
|
2
|
+
<div class="desktop" @click="handleClick">
|
|
3
|
+
<!-- Action Set -->
|
|
4
|
+
<ActionSet :elements="actionElements" @action-click="handleActionClick" />
|
|
5
|
+
|
|
6
|
+
<!-- Main content using AForm -->
|
|
7
|
+
<AForm v-if="writableSchema.length > 0" v-model="writableSchema" :data="currentViewData" />
|
|
8
|
+
<div v-else-if="!stonecrop" class="loading"><p>Initializing Stonecrop...</p></div>
|
|
9
|
+
<div v-else class="loading">
|
|
10
|
+
<p>Loading {{ currentView }} data...</p>
|
|
11
|
+
</div>
|
|
12
|
+
|
|
13
|
+
<!-- Sheet Navigation -->
|
|
14
|
+
<SheetNav :breadcrumbs="navigationBreadcrumbs" />
|
|
15
|
+
|
|
16
|
+
<!-- Command Palette -->
|
|
17
|
+
<CommandPalette
|
|
18
|
+
:is-open="commandPaletteOpen"
|
|
19
|
+
:search="searchCommands"
|
|
20
|
+
placeholder="Type a command or search..."
|
|
21
|
+
@select="executeCommand"
|
|
22
|
+
@close="commandPaletteOpen = false">
|
|
23
|
+
<template #title="{ result }">
|
|
24
|
+
{{ result.title }}
|
|
25
|
+
</template>
|
|
26
|
+
<template #content="{ result }">
|
|
27
|
+
{{ result.description }}
|
|
28
|
+
</template>
|
|
29
|
+
</CommandPalette>
|
|
30
|
+
</div>
|
|
4
31
|
</template>
|
|
5
32
|
|
|
6
33
|
<script setup lang="ts">
|
|
34
|
+
import { useStonecrop } from '@stonecrop/stonecrop'
|
|
35
|
+
import { AForm, type SchemaTypes, type TableColumn, type TableConfig } from '@stonecrop/aform'
|
|
36
|
+
import { computed, nextTick, onMounted, provide, ref, unref, watch } from 'vue'
|
|
37
|
+
|
|
38
|
+
import ActionSet from './ActionSet.vue'
|
|
7
39
|
import SheetNav from './SheetNav.vue'
|
|
40
|
+
import CommandPalette from './CommandPalette.vue'
|
|
41
|
+
import type { ActionElements } from '../types'
|
|
42
|
+
|
|
43
|
+
const { availableDoctypes = [] } = defineProps<{ availableDoctypes?: string[] }>()
|
|
44
|
+
|
|
45
|
+
const { stonecrop } = useStonecrop()
|
|
46
|
+
|
|
47
|
+
// State
|
|
48
|
+
const loading = ref(false)
|
|
49
|
+
const saving = ref(false)
|
|
50
|
+
const commandPaletteOpen = ref(false)
|
|
51
|
+
|
|
52
|
+
// HST-based form data management - field triggers are handled automatically by HST
|
|
53
|
+
|
|
54
|
+
// Computed property that reads from HST store for reactive form data
|
|
55
|
+
const currentViewData = computed<Record<string, any>>({
|
|
56
|
+
get() {
|
|
57
|
+
if (!stonecrop.value || !currentDoctype.value || !currentRecordId.value) {
|
|
58
|
+
return {}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const record = stonecrop.value.getRecordById(currentDoctype.value, currentRecordId.value)
|
|
63
|
+
return record?.get('') || {}
|
|
64
|
+
} catch {
|
|
65
|
+
return {}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
set(newData: Record<string, any>) {
|
|
69
|
+
if (!stonecrop.value || !currentDoctype.value || !currentRecordId.value) {
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
// Update each field in HST, which will automatically trigger field actions
|
|
75
|
+
const hstStore = stonecrop.value.getStore()
|
|
76
|
+
for (const [fieldname, value] of Object.entries(newData)) {
|
|
77
|
+
const fieldPath = `${currentDoctype.value}.${currentRecordId.value}.${fieldname}`
|
|
78
|
+
hstStore.set(fieldPath, value)
|
|
79
|
+
}
|
|
80
|
+
} catch (error) {
|
|
81
|
+
// eslint-disable-next-line no-console
|
|
82
|
+
console.warn('HST update failed:', error)
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
// HST-based form data management - field triggers are handled automatically by HST
|
|
88
|
+
|
|
89
|
+
// Computed properties for current route context
|
|
90
|
+
const route = computed(() => unref(stonecrop.value?.registry.router?.currentRoute))
|
|
91
|
+
const router = computed(() => stonecrop.value?.registry.router)
|
|
92
|
+
const currentDoctype = computed(() => {
|
|
93
|
+
if (!route.value) return ''
|
|
94
|
+
|
|
95
|
+
// First check if we have actualDoctype in meta (from registered routes)
|
|
96
|
+
if (route.value.meta?.actualDoctype) {
|
|
97
|
+
return route.value.meta.actualDoctype as string
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// For named routes, use params.doctype
|
|
101
|
+
if (route.value.params.doctype) {
|
|
102
|
+
return route.value.params.doctype as string
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// For catch-all routes that haven't been registered yet, extract from path
|
|
106
|
+
const pathMatch = route.value.params.pathMatch as string[] | undefined
|
|
107
|
+
if (pathMatch && pathMatch.length > 0) {
|
|
108
|
+
return pathMatch[0]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return ''
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// The route doctype for display and navigation (e.g., 'todo')
|
|
115
|
+
const routeDoctype = computed(() => {
|
|
116
|
+
if (!route.value) return ''
|
|
117
|
+
|
|
118
|
+
// Check route meta first
|
|
119
|
+
if (route.value.meta?.doctype) {
|
|
120
|
+
return route.value.meta.doctype as string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// For named routes, use params.doctype
|
|
124
|
+
if (route.value.params.doctype) {
|
|
125
|
+
return route.value.params.doctype as string
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// For catch-all routes, extract from path
|
|
129
|
+
const pathMatch = route.value.params.pathMatch as string[] | undefined
|
|
130
|
+
if (pathMatch && pathMatch.length > 0) {
|
|
131
|
+
return pathMatch[0]
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return ''
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
const currentRecordId = computed(() => {
|
|
138
|
+
if (!route.value) return ''
|
|
139
|
+
|
|
140
|
+
// For named routes, use params.recordId
|
|
141
|
+
if (route.value.params.recordId) {
|
|
142
|
+
return route.value.params.recordId as string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// For catch-all routes that haven't been registered yet, extract from path
|
|
146
|
+
const pathMatch = route.value.params.pathMatch as string[] | undefined
|
|
147
|
+
if (pathMatch && pathMatch.length > 1) {
|
|
148
|
+
return pathMatch[1]
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return ''
|
|
152
|
+
})
|
|
153
|
+
const isNewRecord = computed(() => currentRecordId.value?.startsWith('new-'))
|
|
154
|
+
|
|
155
|
+
// Determine current view based on route
|
|
156
|
+
const currentView = computed(() => {
|
|
157
|
+
if (!route.value) {
|
|
158
|
+
return 'doctypes'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Home route
|
|
162
|
+
if (route.value.name === 'home' || route.value.path === '/') {
|
|
163
|
+
return 'doctypes'
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Named routes from registered doctypes
|
|
167
|
+
if (route.value.name && route.value.name !== 'catch-all') {
|
|
168
|
+
const routeName = route.value.name as string
|
|
169
|
+
if (routeName.includes('form') || route.value.params.recordId) {
|
|
170
|
+
return 'record'
|
|
171
|
+
} else if (routeName.includes('list') || route.value.params.doctype) {
|
|
172
|
+
return 'records'
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Catch-all route - determine from path structure
|
|
177
|
+
const pathMatch = route.value.params.pathMatch as string[] | undefined
|
|
178
|
+
if (pathMatch && pathMatch.length > 0) {
|
|
179
|
+
const view = pathMatch.length === 1 ? 'records' : 'record'
|
|
180
|
+
return view
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return 'doctypes'
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
// Computed properties (now that all helper functions are defined)
|
|
187
|
+
// Helper function to get available transitions for current record
|
|
188
|
+
const getAvailableTransitions = () => {
|
|
189
|
+
if (!stonecrop.value || !currentDoctype.value || !currentRecordId.value) {
|
|
190
|
+
return []
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const registry = stonecrop.value.registry
|
|
195
|
+
const meta = registry.registry[currentDoctype.value]
|
|
196
|
+
|
|
197
|
+
if (!meta?.workflow?.states) {
|
|
198
|
+
return []
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Get current FSM state (for now, use workflow initial state or 'editing')
|
|
202
|
+
// In a full implementation, this would track actual FSM state
|
|
203
|
+
const currentState = isNewRecord.value ? 'creating' : 'editing'
|
|
204
|
+
const stateConfig = meta.workflow.states[currentState]
|
|
205
|
+
|
|
206
|
+
if (!stateConfig?.on) {
|
|
207
|
+
return []
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Get available transitions from current state
|
|
211
|
+
const transitions = Object.keys(stateConfig.on)
|
|
212
|
+
|
|
213
|
+
// Create action elements for each transition
|
|
214
|
+
const actionElements = transitions.map(transition => {
|
|
215
|
+
const targetState = stateConfig.on?.[transition]
|
|
216
|
+
const targetStateName = typeof targetState === 'string' ? targetState : 'unknown'
|
|
217
|
+
|
|
218
|
+
const actionFn = async () => {
|
|
219
|
+
const node = stonecrop.value?.getRecordById(currentDoctype.value, currentRecordId.value)
|
|
220
|
+
if (node) {
|
|
221
|
+
const recordData = currentViewData.value || {}
|
|
222
|
+
await node.triggerTransition(transition, {
|
|
223
|
+
currentState,
|
|
224
|
+
targetState: targetStateName,
|
|
225
|
+
fsmContext: recordData,
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const element = {
|
|
231
|
+
label: `${transition} (→ ${targetStateName})`,
|
|
232
|
+
action: actionFn,
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return element
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
return actionElements
|
|
239
|
+
} catch (error) {
|
|
240
|
+
// eslint-disable-next-line no-console
|
|
241
|
+
console.warn('Error getting available transitions:', error)
|
|
242
|
+
return []
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// New component reactive properties// New component reactive properties
|
|
247
|
+
const actionElements = computed<ActionElements[]>(() => {
|
|
248
|
+
const elements: ActionElements[] = []
|
|
249
|
+
|
|
250
|
+
switch (currentView.value) {
|
|
251
|
+
case 'doctypes':
|
|
252
|
+
elements.push({
|
|
253
|
+
type: 'button',
|
|
254
|
+
label: 'Refresh',
|
|
255
|
+
action: () => {
|
|
256
|
+
// Refresh doctypes
|
|
257
|
+
window.location.reload()
|
|
258
|
+
},
|
|
259
|
+
})
|
|
260
|
+
break
|
|
261
|
+
case 'records':
|
|
262
|
+
elements.push(
|
|
263
|
+
{
|
|
264
|
+
type: 'button',
|
|
265
|
+
label: 'New Record',
|
|
266
|
+
action: () => void createNewRecord(),
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
type: 'button',
|
|
270
|
+
label: 'Refresh',
|
|
271
|
+
action: () => {
|
|
272
|
+
// Refresh records
|
|
273
|
+
window.location.reload()
|
|
274
|
+
},
|
|
275
|
+
}
|
|
276
|
+
)
|
|
277
|
+
break
|
|
278
|
+
case 'record': {
|
|
279
|
+
// Add XState Transitions dropdown for record view
|
|
280
|
+
const transitionActions = getAvailableTransitions()
|
|
281
|
+
if (transitionActions.length > 0) {
|
|
282
|
+
elements.push({
|
|
283
|
+
type: 'dropdown',
|
|
284
|
+
label: 'Actions',
|
|
285
|
+
actions: transitionActions,
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
break
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return elements
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
const navigationBreadcrumbs = computed(() => {
|
|
296
|
+
const breadcrumbs: { title: string; to: string }[] = []
|
|
297
|
+
|
|
298
|
+
if (currentView.value === 'records' && routeDoctype.value) {
|
|
299
|
+
breadcrumbs.push(
|
|
300
|
+
{ title: 'Home', to: '/' },
|
|
301
|
+
{ title: formatDoctypeName(routeDoctype.value), to: `/${routeDoctype.value}` }
|
|
302
|
+
)
|
|
303
|
+
} else if (currentView.value === 'record' && routeDoctype.value) {
|
|
304
|
+
breadcrumbs.push(
|
|
305
|
+
{ title: 'Home', to: '/' },
|
|
306
|
+
{ title: formatDoctypeName(routeDoctype.value), to: `/${routeDoctype.value}` },
|
|
307
|
+
{ title: isNewRecord.value ? 'New Record' : 'Edit Record', to: route.value?.fullPath || '' }
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return breadcrumbs
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
// Command palette functionality
|
|
315
|
+
type Command = {
|
|
316
|
+
title: string
|
|
317
|
+
description: string
|
|
318
|
+
action: () => void
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const searchCommands = (query: string): Command[] => {
|
|
322
|
+
const commands: Command[] = [
|
|
323
|
+
{
|
|
324
|
+
title: 'Go Home',
|
|
325
|
+
description: 'Navigate to the home page',
|
|
326
|
+
action: () => void router.value?.push('/'),
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
title: 'Toggle Command Palette',
|
|
330
|
+
description: 'Open/close the command palette',
|
|
331
|
+
action: () => (commandPaletteOpen.value = !commandPaletteOpen.value),
|
|
332
|
+
},
|
|
333
|
+
]
|
|
334
|
+
|
|
335
|
+
// Add doctype-specific commands
|
|
336
|
+
if (routeDoctype.value) {
|
|
337
|
+
commands.push({
|
|
338
|
+
title: `View ${formatDoctypeName(routeDoctype.value)} Records`,
|
|
339
|
+
description: `Navigate to ${routeDoctype.value} list`,
|
|
340
|
+
action: () => void router.value?.push(`/${routeDoctype.value}`),
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
commands.push({
|
|
344
|
+
title: `Create New ${formatDoctypeName(routeDoctype.value)}`,
|
|
345
|
+
description: `Create a new ${routeDoctype.value} record`,
|
|
346
|
+
action: () => void createNewRecord(),
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Add available doctypes as commands
|
|
351
|
+
availableDoctypes.forEach(doctype => {
|
|
352
|
+
commands.push({
|
|
353
|
+
title: `View ${formatDoctypeName(doctype)}`,
|
|
354
|
+
description: `Navigate to ${doctype} list`,
|
|
355
|
+
action: () => void router.value?.push(`/${doctype}`),
|
|
356
|
+
})
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
// Filter commands based on query
|
|
360
|
+
if (!query) return commands
|
|
361
|
+
|
|
362
|
+
return commands.filter(
|
|
363
|
+
cmd =>
|
|
364
|
+
cmd.title.toLowerCase().includes(query.toLowerCase()) ||
|
|
365
|
+
cmd.description.toLowerCase().includes(query.toLowerCase())
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const executeCommand = (command: Command) => {
|
|
370
|
+
command.action()
|
|
371
|
+
commandPaletteOpen.value = false
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Helper functions - moved here to avoid "before initialization" errors
|
|
375
|
+
const formatDoctypeName = (doctype: string): string => {
|
|
376
|
+
return doctype
|
|
377
|
+
.split('-')
|
|
378
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
379
|
+
.join(' ')
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const getRecordCount = (doctype: string): number => {
|
|
383
|
+
if (!stonecrop.value) return 0
|
|
384
|
+
const recordIds = stonecrop.value.getRecordIds(doctype)
|
|
385
|
+
return recordIds.length
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const navigateToDoctype = async (doctype: string) => {
|
|
389
|
+
await router.value?.push(`/${doctype}`)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const openRecord = async (recordId: string) => {
|
|
393
|
+
await router.value?.push(`/${routeDoctype.value}/${recordId}`)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const createNewRecord = async () => {
|
|
397
|
+
const newId = `new-${Date.now()}`
|
|
398
|
+
await router.value?.push(`/${routeDoctype.value}/${newId}`)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Doctype metadata loader - simplified since router handles most of this
|
|
402
|
+
const loadDoctypeMetadata = (doctype: string) => {
|
|
403
|
+
if (!stonecrop.value) return
|
|
404
|
+
|
|
405
|
+
// Ensure the doctype structure exists in HST
|
|
406
|
+
// The router should have already loaded the metadata, but this ensures the HST structure exists
|
|
407
|
+
try {
|
|
408
|
+
stonecrop.value.records(doctype)
|
|
409
|
+
} catch (error) {
|
|
410
|
+
// Silent error handling - structure will be created if needed
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Schema generator functions - moved here to be available to computed properties
|
|
415
|
+
const getDoctypesSchema = (): SchemaTypes[] => {
|
|
416
|
+
if (!availableDoctypes.length) return []
|
|
417
|
+
|
|
418
|
+
const rows = availableDoctypes.map(doctype => ({
|
|
419
|
+
id: doctype,
|
|
420
|
+
doctype,
|
|
421
|
+
display_name: formatDoctypeName(doctype),
|
|
422
|
+
record_count: getRecordCount(doctype),
|
|
423
|
+
actions: 'View Records',
|
|
424
|
+
}))
|
|
425
|
+
|
|
426
|
+
return [
|
|
427
|
+
{
|
|
428
|
+
fieldname: 'header',
|
|
429
|
+
component: 'div',
|
|
430
|
+
value: `
|
|
431
|
+
<div class="view-header">
|
|
432
|
+
<h1>Available Doctypes</h1>
|
|
433
|
+
</div>
|
|
434
|
+
`,
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
fieldname: 'doctypes_table',
|
|
438
|
+
component: 'ATable',
|
|
439
|
+
columns: [
|
|
440
|
+
{
|
|
441
|
+
label: 'Doctype',
|
|
442
|
+
name: 'doctype',
|
|
443
|
+
type: 'Data',
|
|
444
|
+
align: 'left',
|
|
445
|
+
edit: false,
|
|
446
|
+
width: '20ch',
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
label: 'Name',
|
|
450
|
+
name: 'display_name',
|
|
451
|
+
type: 'Data',
|
|
452
|
+
align: 'left',
|
|
453
|
+
edit: false,
|
|
454
|
+
width: '30ch',
|
|
455
|
+
},
|
|
456
|
+
{
|
|
457
|
+
label: 'Records',
|
|
458
|
+
name: 'record_count',
|
|
459
|
+
type: 'Data',
|
|
460
|
+
align: 'center',
|
|
461
|
+
edit: false,
|
|
462
|
+
width: '15ch',
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
label: 'Actions',
|
|
466
|
+
name: 'actions',
|
|
467
|
+
type: 'Data',
|
|
468
|
+
align: 'center',
|
|
469
|
+
edit: false,
|
|
470
|
+
width: '20ch',
|
|
471
|
+
},
|
|
472
|
+
] as TableColumn[],
|
|
473
|
+
config: {
|
|
474
|
+
view: 'list',
|
|
475
|
+
fullWidth: true,
|
|
476
|
+
} as TableConfig,
|
|
477
|
+
rows,
|
|
478
|
+
},
|
|
479
|
+
]
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const getRecordsSchema = (): SchemaTypes[] => {
|
|
483
|
+
if (!currentDoctype.value) return []
|
|
484
|
+
if (!stonecrop.value) return []
|
|
485
|
+
|
|
486
|
+
const records = getRecords()
|
|
487
|
+
const columns = getColumns()
|
|
488
|
+
|
|
489
|
+
// If no columns are available, show a loading or empty state
|
|
490
|
+
if (columns.length === 0) {
|
|
491
|
+
return [
|
|
492
|
+
{
|
|
493
|
+
fieldname: 'header',
|
|
494
|
+
component: 'div',
|
|
495
|
+
value: `
|
|
496
|
+
<div class="view-header">
|
|
497
|
+
<nav class="breadcrumbs">
|
|
498
|
+
<a href="/">Home</a>
|
|
499
|
+
<span class="separator">/</span>
|
|
500
|
+
<span class="current">${formatDoctypeName(routeDoctype.value || currentDoctype.value)}</span>
|
|
501
|
+
</nav>
|
|
502
|
+
<h1>${formatDoctypeName(routeDoctype.value || currentDoctype.value)} Records</h1>
|
|
503
|
+
</div>
|
|
504
|
+
`,
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
fieldname: 'loading',
|
|
508
|
+
component: 'div',
|
|
509
|
+
value: `
|
|
510
|
+
<div class="loading-state">
|
|
511
|
+
<p>Loading ${formatDoctypeName(routeDoctype.value || currentDoctype.value)} schema...</p>
|
|
512
|
+
</div>
|
|
513
|
+
`,
|
|
514
|
+
},
|
|
515
|
+
]
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const rows = records.map((record: any) => ({
|
|
519
|
+
...record,
|
|
520
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
521
|
+
id: record.id || '',
|
|
522
|
+
actions: 'Edit | Delete',
|
|
523
|
+
}))
|
|
524
|
+
|
|
525
|
+
return [
|
|
526
|
+
{
|
|
527
|
+
fieldname: 'header',
|
|
528
|
+
component: 'div',
|
|
529
|
+
value: `
|
|
530
|
+
<div class="view-header">
|
|
531
|
+
<nav class="breadcrumbs">
|
|
532
|
+
<a href="/">Home</a>
|
|
533
|
+
<span class="separator">/</span>
|
|
534
|
+
<span class="current">${formatDoctypeName(routeDoctype.value || currentDoctype.value)}</span>
|
|
535
|
+
</nav>
|
|
536
|
+
<h1>${formatDoctypeName(routeDoctype.value || currentDoctype.value)} Records</h1>
|
|
537
|
+
</div>
|
|
538
|
+
`,
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
fieldname: 'actions',
|
|
542
|
+
component: 'div',
|
|
543
|
+
value: `
|
|
544
|
+
<div class="view-actions">
|
|
545
|
+
<button class="btn-primary" data-action="create">
|
|
546
|
+
New ${formatDoctypeName(routeDoctype.value || currentDoctype.value)}
|
|
547
|
+
</button>
|
|
548
|
+
</div>
|
|
549
|
+
`,
|
|
550
|
+
},
|
|
551
|
+
...(records.length === 0
|
|
552
|
+
? [
|
|
553
|
+
{
|
|
554
|
+
fieldname: 'empty_state',
|
|
555
|
+
component: 'div',
|
|
556
|
+
value: `
|
|
557
|
+
<div class="empty-state">
|
|
558
|
+
<p>No ${routeDoctype.value || currentDoctype.value} records found.</p>
|
|
559
|
+
<button class="btn-primary" data-action="create">
|
|
560
|
+
Create First Record
|
|
561
|
+
</button>
|
|
562
|
+
</div>
|
|
563
|
+
`,
|
|
564
|
+
},
|
|
565
|
+
]
|
|
566
|
+
: [
|
|
567
|
+
{
|
|
568
|
+
fieldname: 'records_table',
|
|
569
|
+
component: 'ATable',
|
|
570
|
+
columns: [
|
|
571
|
+
...columns.map(col => ({
|
|
572
|
+
label: col.label,
|
|
573
|
+
name: col.fieldname,
|
|
574
|
+
type: col.fieldtype,
|
|
575
|
+
align: 'left',
|
|
576
|
+
edit: false,
|
|
577
|
+
width: '20ch',
|
|
578
|
+
})),
|
|
579
|
+
{
|
|
580
|
+
label: 'Actions',
|
|
581
|
+
name: 'actions',
|
|
582
|
+
type: 'Data',
|
|
583
|
+
align: 'center',
|
|
584
|
+
edit: false,
|
|
585
|
+
width: '20ch',
|
|
586
|
+
},
|
|
587
|
+
] as TableColumn[],
|
|
588
|
+
config: {
|
|
589
|
+
view: 'list',
|
|
590
|
+
fullWidth: true,
|
|
591
|
+
} as TableConfig,
|
|
592
|
+
rows,
|
|
593
|
+
},
|
|
594
|
+
]),
|
|
595
|
+
]
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const getRecordFormSchema = (): SchemaTypes[] => {
|
|
599
|
+
if (!currentDoctype.value) return []
|
|
600
|
+
if (!stonecrop.value) return []
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
const registry = stonecrop.value?.registry
|
|
604
|
+
const meta = registry?.registry[currentDoctype.value]
|
|
605
|
+
|
|
606
|
+
if (!meta?.schema) {
|
|
607
|
+
// Return loading state if schema isn't available yet
|
|
608
|
+
return [
|
|
609
|
+
{
|
|
610
|
+
fieldname: 'header',
|
|
611
|
+
component: 'div',
|
|
612
|
+
value: `
|
|
613
|
+
<div class="view-header">
|
|
614
|
+
<nav class="breadcrumbs">
|
|
615
|
+
<a href="/">Home</a>
|
|
616
|
+
<span class="separator">/</span>
|
|
617
|
+
<a href="/${routeDoctype.value || currentDoctype.value}">${formatDoctypeName(
|
|
618
|
+
routeDoctype.value || currentDoctype.value
|
|
619
|
+
)}</a>
|
|
620
|
+
<span class="separator">/</span>
|
|
621
|
+
<span class="current">${isNewRecord.value ? 'New Record' : currentRecordId.value}</span>
|
|
622
|
+
</nav>
|
|
623
|
+
<h1>${
|
|
624
|
+
isNewRecord.value
|
|
625
|
+
? `New ${formatDoctypeName(routeDoctype.value || currentDoctype.value)}`
|
|
626
|
+
: `Edit ${formatDoctypeName(routeDoctype.value || currentDoctype.value)}`
|
|
627
|
+
}</h1>
|
|
628
|
+
</div>
|
|
629
|
+
`,
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
fieldname: 'loading',
|
|
633
|
+
component: 'div',
|
|
634
|
+
value: `
|
|
635
|
+
<div class="loading-state">
|
|
636
|
+
<p>Loading ${formatDoctypeName(routeDoctype.value || currentDoctype.value)} form...</p>
|
|
637
|
+
</div>
|
|
638
|
+
`,
|
|
639
|
+
},
|
|
640
|
+
]
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const schemaArray = 'toArray' in meta.schema ? meta.schema.toArray() : meta.schema
|
|
644
|
+
const currentRecord = getCurrentRecord()
|
|
645
|
+
|
|
646
|
+
return [
|
|
647
|
+
{
|
|
648
|
+
fieldname: 'header',
|
|
649
|
+
component: 'div',
|
|
650
|
+
value: `
|
|
651
|
+
<div class="view-header">
|
|
652
|
+
<nav class="breadcrumbs">
|
|
653
|
+
<a href="/">Home</a>
|
|
654
|
+
<span class="separator">/</span>
|
|
655
|
+
<a href="/${routeDoctype.value || currentDoctype.value}">${formatDoctypeName(
|
|
656
|
+
routeDoctype.value || currentDoctype.value
|
|
657
|
+
)}</a>
|
|
658
|
+
<span class="separator">/</span>
|
|
659
|
+
<span class="current">${isNewRecord.value ? 'New Record' : currentRecordId.value}</span>
|
|
660
|
+
</nav>
|
|
661
|
+
<h1>
|
|
662
|
+
${
|
|
663
|
+
isNewRecord.value
|
|
664
|
+
? `New ${formatDoctypeName(routeDoctype.value || currentDoctype.value)}`
|
|
665
|
+
: `Edit ${formatDoctypeName(routeDoctype.value || currentDoctype.value)}`
|
|
666
|
+
}
|
|
667
|
+
</h1>
|
|
668
|
+
</div>
|
|
669
|
+
`,
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
fieldname: 'actions',
|
|
673
|
+
component: 'div',
|
|
674
|
+
value: `
|
|
675
|
+
<div class="view-actions">
|
|
676
|
+
<button class="btn-primary" data-action="save" ${saving.value ? 'disabled' : ''}>
|
|
677
|
+
${saving.value ? 'Saving...' : 'Save'}
|
|
678
|
+
</button>
|
|
679
|
+
<button class="btn-secondary" data-action="cancel">Cancel</button>
|
|
680
|
+
${!isNewRecord.value ? '<button class="btn-danger" data-action="delete">Delete</button>' : ''}
|
|
681
|
+
</div>
|
|
682
|
+
`,
|
|
683
|
+
},
|
|
684
|
+
...schemaArray.map(field => ({
|
|
685
|
+
...field,
|
|
686
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
|
687
|
+
value: currentRecord[field.fieldname] || '',
|
|
688
|
+
})),
|
|
689
|
+
]
|
|
690
|
+
} catch (error) {
|
|
691
|
+
return [
|
|
692
|
+
{
|
|
693
|
+
fieldname: 'error',
|
|
694
|
+
component: 'div',
|
|
695
|
+
value: `
|
|
696
|
+
<div class="error-state">
|
|
697
|
+
<p>Unable to load form schema for ${formatDoctypeName(routeDoctype.value || currentDoctype.value)}</p>
|
|
698
|
+
</div>
|
|
699
|
+
`,
|
|
700
|
+
},
|
|
701
|
+
]
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Additional data helper functions
|
|
706
|
+
const getRecords = () => {
|
|
707
|
+
if (!stonecrop.value || !currentDoctype.value) {
|
|
708
|
+
return []
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const recordsNode = stonecrop.value.records(currentDoctype.value)
|
|
712
|
+
const recordsData = recordsNode?.get('')
|
|
713
|
+
|
|
714
|
+
if (recordsData && typeof recordsData === 'object' && !Array.isArray(recordsData)) {
|
|
715
|
+
return Object.values(recordsData as Record<string, any>)
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
return []
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const getColumns = () => {
|
|
722
|
+
if (!stonecrop.value || !currentDoctype.value) return []
|
|
723
|
+
|
|
724
|
+
try {
|
|
725
|
+
const registry = stonecrop.value.registry
|
|
726
|
+
const meta = registry.registry[currentDoctype.value]
|
|
727
|
+
|
|
728
|
+
if (meta?.schema) {
|
|
729
|
+
const schemaArray = 'toArray' in meta.schema ? meta.schema.toArray() : meta.schema
|
|
730
|
+
return schemaArray.map(field => ({
|
|
731
|
+
fieldname: field.fieldname,
|
|
732
|
+
label: ('label' in field && field.label) || field.fieldname,
|
|
733
|
+
fieldtype: ('fieldtype' in field && field.fieldtype) || 'Data',
|
|
734
|
+
}))
|
|
735
|
+
}
|
|
736
|
+
} catch (error) {
|
|
737
|
+
// Error getting schema - return empty array
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return []
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const getCurrentRecord = () => {
|
|
744
|
+
if (!stonecrop.value || !currentDoctype.value || isNewRecord.value) return {}
|
|
745
|
+
|
|
746
|
+
const record = stonecrop.value.getRecordById(currentDoctype.value, currentRecordId.value)
|
|
747
|
+
return record?.get('') || {}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Schema for different views - defined here after all helper functions are available
|
|
751
|
+
const currentViewSchema = computed<SchemaTypes[]>(() => {
|
|
752
|
+
switch (currentView.value) {
|
|
753
|
+
case 'doctypes':
|
|
754
|
+
return getDoctypesSchema()
|
|
755
|
+
case 'records':
|
|
756
|
+
return getRecordsSchema()
|
|
757
|
+
case 'record':
|
|
758
|
+
return getRecordFormSchema()
|
|
759
|
+
default:
|
|
760
|
+
return []
|
|
761
|
+
}
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
// Writable schema for AForm v-model binding
|
|
765
|
+
const writableSchema = ref<SchemaTypes[]>([])
|
|
766
|
+
|
|
767
|
+
// Sync computed schema to writable schema when it changes
|
|
768
|
+
watch(
|
|
769
|
+
currentViewSchema,
|
|
770
|
+
newSchema => {
|
|
771
|
+
writableSchema.value = [...newSchema]
|
|
772
|
+
},
|
|
773
|
+
{ immediate: true, deep: true }
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
// Watch for field changes in writable schema and sync to HST
|
|
777
|
+
watch(
|
|
778
|
+
writableSchema,
|
|
779
|
+
newSchema => {
|
|
780
|
+
if (!stonecrop.value || !currentDoctype.value || !currentRecordId.value || isNewRecord.value) {
|
|
781
|
+
return
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
try {
|
|
785
|
+
const hstStore = stonecrop.value.getStore()
|
|
786
|
+
|
|
787
|
+
// Process form field updates from schema
|
|
788
|
+
newSchema.forEach(field => {
|
|
789
|
+
// Only process fields that have a fieldname and value (form fields)
|
|
790
|
+
if (
|
|
791
|
+
field.fieldname &&
|
|
792
|
+
'value' in field &&
|
|
793
|
+
!['header', 'actions', 'loading', 'error'].includes(field.fieldname)
|
|
794
|
+
) {
|
|
795
|
+
const fieldPath = `${currentDoctype.value}.${currentRecordId.value}.${field.fieldname}`
|
|
796
|
+
const currentValue = hstStore.has(fieldPath) ? hstStore.get(fieldPath) : undefined
|
|
797
|
+
|
|
798
|
+
// Only update if value actually changed to avoid infinite loops
|
|
799
|
+
if (currentValue !== field.value) {
|
|
800
|
+
hstStore.set(fieldPath, field.value)
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
})
|
|
804
|
+
} catch (error) {
|
|
805
|
+
// eslint-disable-next-line no-console
|
|
806
|
+
console.warn('HST schema sync failed:', error)
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
{ deep: true }
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
// Action handlers (will be triggered by button clicks in the UI)
|
|
813
|
+
const handleSave = async () => {
|
|
814
|
+
// eslint-disable-next-line no-console
|
|
815
|
+
if (!stonecrop.value) return
|
|
816
|
+
|
|
817
|
+
saving.value = true
|
|
818
|
+
|
|
819
|
+
try {
|
|
820
|
+
const formData = currentViewData.value || {}
|
|
821
|
+
|
|
822
|
+
if (isNewRecord.value) {
|
|
823
|
+
const newId = `record-${Date.now()}`
|
|
824
|
+
const recordData = { id: newId, ...formData }
|
|
825
|
+
|
|
826
|
+
stonecrop.value.addRecord(currentDoctype.value, newId, recordData)
|
|
827
|
+
|
|
828
|
+
// Trigger SAVE transition for new record
|
|
829
|
+
const node = stonecrop.value.getRecordById(currentDoctype.value, newId)
|
|
830
|
+
if (node) {
|
|
831
|
+
await node.triggerTransition('SAVE', {
|
|
832
|
+
currentState: 'creating',
|
|
833
|
+
targetState: 'saved',
|
|
834
|
+
fsmContext: recordData,
|
|
835
|
+
})
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
await router.value?.replace(`/${routeDoctype.value}/${newId}`)
|
|
839
|
+
} else {
|
|
840
|
+
const recordData = { id: currentRecordId.value, ...formData }
|
|
841
|
+
stonecrop.value.addRecord(currentDoctype.value, currentRecordId.value, recordData)
|
|
842
|
+
|
|
843
|
+
// Trigger SAVE transition for existing record
|
|
844
|
+
const node = stonecrop.value.getRecordById(currentDoctype.value, currentRecordId.value)
|
|
845
|
+
if (node) {
|
|
846
|
+
await node.triggerTransition('SAVE', {
|
|
847
|
+
currentState: 'editing',
|
|
848
|
+
targetState: 'saved',
|
|
849
|
+
fsmContext: recordData,
|
|
850
|
+
})
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
} catch (error) {
|
|
854
|
+
// Silently handle error
|
|
855
|
+
} finally {
|
|
856
|
+
saving.value = false
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const handleCancel = async () => {
|
|
861
|
+
if (isNewRecord.value) {
|
|
862
|
+
// For new records, we don't have a specific record node yet
|
|
863
|
+
// Just navigate back without triggering transition
|
|
864
|
+
await router.value?.push(`/${routeDoctype.value}`)
|
|
865
|
+
} else {
|
|
866
|
+
// Trigger CANCEL transition for existing record
|
|
867
|
+
if (stonecrop.value) {
|
|
868
|
+
const node = stonecrop.value.getRecordById(currentDoctype.value, currentRecordId.value)
|
|
869
|
+
if (node) {
|
|
870
|
+
await node.triggerTransition('CANCEL', {
|
|
871
|
+
currentState: 'editing',
|
|
872
|
+
targetState: 'cancelled',
|
|
873
|
+
})
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
// Reload current record data
|
|
877
|
+
loadRecordData()
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const handleActionClick = (label: string, action: (() => void | Promise<void>) | undefined) => {
|
|
882
|
+
// eslint-disable-next-line no-console
|
|
883
|
+
if (action) {
|
|
884
|
+
void action()
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const handleDelete = async (recordId?: string) => {
|
|
889
|
+
if (!stonecrop.value) return
|
|
890
|
+
|
|
891
|
+
const targetRecordId = recordId || currentRecordId.value
|
|
892
|
+
if (!targetRecordId) return
|
|
893
|
+
|
|
894
|
+
if (confirm('Are you sure you want to delete this record?')) {
|
|
895
|
+
// Trigger DELETE transition before removing
|
|
896
|
+
const node = stonecrop.value.getRecordById(currentDoctype.value, targetRecordId)
|
|
897
|
+
if (node) {
|
|
898
|
+
await node.triggerTransition('DELETE', {
|
|
899
|
+
currentState: 'editing',
|
|
900
|
+
targetState: 'deleted',
|
|
901
|
+
})
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
stonecrop.value.removeRecord(currentDoctype.value, targetRecordId)
|
|
905
|
+
|
|
906
|
+
if (currentView.value === 'record') {
|
|
907
|
+
await router.value?.push(`/${routeDoctype.value}`)
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Event handlers
|
|
913
|
+
const handleClick = async (event: Event) => {
|
|
914
|
+
const target = event.target as HTMLElement
|
|
915
|
+
const action = target.getAttribute('data-action')
|
|
916
|
+
|
|
917
|
+
if (action) {
|
|
918
|
+
switch (action) {
|
|
919
|
+
case 'create':
|
|
920
|
+
await createNewRecord()
|
|
921
|
+
break
|
|
922
|
+
case 'save':
|
|
923
|
+
await handleSave()
|
|
924
|
+
break
|
|
925
|
+
case 'cancel':
|
|
926
|
+
await handleCancel()
|
|
927
|
+
break
|
|
928
|
+
case 'delete':
|
|
929
|
+
await handleDelete()
|
|
930
|
+
break
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Handle table cell clicks for actions
|
|
935
|
+
const cell = target.closest('td, th')
|
|
936
|
+
if (cell) {
|
|
937
|
+
const cellText = cell.textContent?.trim()
|
|
938
|
+
const row = cell.closest('tr')
|
|
939
|
+
|
|
940
|
+
if (cellText === 'View Records' && row) {
|
|
941
|
+
// Get the doctype from the row data
|
|
942
|
+
const cells = row.querySelectorAll('td')
|
|
943
|
+
if (cells.length > 0) {
|
|
944
|
+
const doctypeCell = cells[1] // Assuming doctype is in second column (first column is index)
|
|
945
|
+
const doctype = doctypeCell.textContent?.trim()
|
|
946
|
+
if (doctype) {
|
|
947
|
+
await navigateToDoctype(doctype)
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
} else if (cellText?.includes('Edit') && row) {
|
|
951
|
+
// Get the record ID from the row
|
|
952
|
+
const cells = row.querySelectorAll('td')
|
|
953
|
+
if (cells.length > 0) {
|
|
954
|
+
const idCell = cells[0] // Assuming ID is in first column
|
|
955
|
+
const recordId = idCell.textContent?.trim()
|
|
956
|
+
if (recordId) {
|
|
957
|
+
await openRecord(recordId)
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
} else if (cellText?.includes('Delete') && row) {
|
|
961
|
+
// Get the record ID from the row
|
|
962
|
+
const cells = row.querySelectorAll('td')
|
|
963
|
+
if (cells.length > 0) {
|
|
964
|
+
const idCell = cells[0] // Assuming ID is in first column
|
|
965
|
+
const recordId = idCell.textContent?.trim()
|
|
966
|
+
if (recordId) {
|
|
967
|
+
await handleDelete(recordId)
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Watch for route changes to load appropriate data
|
|
975
|
+
watch(
|
|
976
|
+
[currentView, currentDoctype, currentRecordId],
|
|
977
|
+
() => {
|
|
978
|
+
if (currentView.value === 'record') {
|
|
979
|
+
loadRecordData()
|
|
980
|
+
}
|
|
981
|
+
},
|
|
982
|
+
{ immediate: true }
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
// Watch for Stonecrop instance to become available
|
|
986
|
+
watch(
|
|
987
|
+
stonecrop,
|
|
988
|
+
newStonecrop => {
|
|
989
|
+
if (newStonecrop) {
|
|
990
|
+
// Force a re-evaluation of the current view schema when Stonecrop becomes available
|
|
991
|
+
// This is handled automatically by the reactive computed properties
|
|
992
|
+
}
|
|
993
|
+
},
|
|
994
|
+
{ immediate: true }
|
|
995
|
+
)
|
|
996
|
+
|
|
997
|
+
// Watch for when we need to load data for records view
|
|
998
|
+
watch(
|
|
999
|
+
[currentView, currentDoctype, stonecrop],
|
|
1000
|
+
([view, doctype, stonecropInstance]) => {
|
|
1001
|
+
if (view === 'records' && doctype && stonecropInstance) {
|
|
1002
|
+
// Ensure doctype metadata is loaded
|
|
1003
|
+
loadDoctypeMetadata(doctype)
|
|
1004
|
+
}
|
|
1005
|
+
},
|
|
1006
|
+
{ immediate: true }
|
|
1007
|
+
)
|
|
1008
|
+
|
|
1009
|
+
const loadRecordData = () => {
|
|
1010
|
+
if (!stonecrop.value || !currentDoctype.value) return
|
|
1011
|
+
|
|
1012
|
+
loading.value = true
|
|
1013
|
+
|
|
1014
|
+
try {
|
|
1015
|
+
if (!isNewRecord.value) {
|
|
1016
|
+
// For existing records, ensure the record exists in HST
|
|
1017
|
+
// The computed currentViewData will automatically read from HST
|
|
1018
|
+
stonecrop.value.getRecordById(currentDoctype.value, currentRecordId.value)
|
|
1019
|
+
}
|
|
1020
|
+
// For new records, currentViewData computed property will return {} automatically
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
// eslint-disable-next-line no-console
|
|
1023
|
+
console.warn('Error loading record data:', error)
|
|
1024
|
+
} finally {
|
|
1025
|
+
loading.value = false
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// Provide methods for action components
|
|
1030
|
+
const desktopMethods = {
|
|
1031
|
+
navigateToDoctype,
|
|
1032
|
+
openRecord,
|
|
1033
|
+
createNewRecord,
|
|
1034
|
+
handleSave,
|
|
1035
|
+
handleCancel,
|
|
1036
|
+
handleDelete,
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
provide('desktopMethods', desktopMethods)
|
|
1040
|
+
|
|
1041
|
+
// Register action components in Vue app
|
|
1042
|
+
onMounted(() => {
|
|
1043
|
+
// Wait a tick for stonecrop to be ready, then load initial data
|
|
1044
|
+
void nextTick(() => {
|
|
1045
|
+
if (currentView.value === 'records' && currentDoctype.value && stonecrop.value) {
|
|
1046
|
+
loadDoctypeMetadata(currentDoctype.value)
|
|
1047
|
+
}
|
|
1048
|
+
})
|
|
1049
|
+
|
|
1050
|
+
// Components will be automatically registered via the global component system
|
|
1051
|
+
|
|
1052
|
+
// Add keyboard shortcuts
|
|
1053
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
1054
|
+
// Ctrl+K or Cmd+K to open command palette
|
|
1055
|
+
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
|
|
1056
|
+
event.preventDefault()
|
|
1057
|
+
commandPaletteOpen.value = true
|
|
1058
|
+
}
|
|
1059
|
+
// Escape to close command palette
|
|
1060
|
+
if (event.key === 'Escape' && commandPaletteOpen.value) {
|
|
1061
|
+
commandPaletteOpen.value = false
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
document.addEventListener('keydown', handleKeydown)
|
|
1066
|
+
|
|
1067
|
+
// Cleanup event listener on unmount
|
|
1068
|
+
return () => {
|
|
1069
|
+
document.removeEventListener('keydown', handleKeydown)
|
|
1070
|
+
}
|
|
1071
|
+
})
|
|
8
1072
|
</script>
|