@stonecrop/desktop 0.13.8 → 0.13.9
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.js +1662 -1665
- package/dist/desktop.js.map +1 -1
- package/package.json +6 -6
- package/src/components/Desktop.vue +95 -92
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stonecrop/desktop",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": {
|
|
@@ -32,11 +32,11 @@
|
|
|
32
32
|
"**/*.css"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@stonecrop/aform": "0.13.
|
|
36
|
-
"@stonecrop/atable": "0.13.
|
|
37
|
-
"@stonecrop/
|
|
38
|
-
"@stonecrop/
|
|
39
|
-
"@stonecrop/
|
|
35
|
+
"@stonecrop/aform": "0.13.9",
|
|
36
|
+
"@stonecrop/atable": "0.13.9",
|
|
37
|
+
"@stonecrop/schema": "0.13.9",
|
|
38
|
+
"@stonecrop/stonecrop": "0.13.9",
|
|
39
|
+
"@stonecrop/themes": "0.13.9"
|
|
40
40
|
},
|
|
41
41
|
"peerDependencies": {
|
|
42
42
|
"vue": "^3.5.33"
|
|
@@ -32,9 +32,9 @@
|
|
|
32
32
|
|
|
33
33
|
<script setup lang="ts">
|
|
34
34
|
import { useStonecrop } from '@stonecrop/stonecrop'
|
|
35
|
-
import { AForm, type AFormLinkNavigator, type
|
|
35
|
+
import { AForm, type AFormLinkNavigator, type ResolvedField, type ResolvedTable } from '@stonecrop/aform'
|
|
36
36
|
import type { ColumnSchema } from '@stonecrop/schema'
|
|
37
|
-
import { computed, onMounted, provide, ref, unref, watch } from 'vue'
|
|
37
|
+
import { computed, onMounted, onUnmounted, provide, ref, unref, watch } from 'vue'
|
|
38
38
|
|
|
39
39
|
import ActionSet from './ActionSet.vue'
|
|
40
40
|
import SheetNav from './SheetNav.vue'
|
|
@@ -49,7 +49,12 @@ import type {
|
|
|
49
49
|
LoadRecordEventPayload,
|
|
50
50
|
} from '../types'
|
|
51
51
|
|
|
52
|
-
const
|
|
52
|
+
const {
|
|
53
|
+
availableDoctypes = [],
|
|
54
|
+
routeAdapter,
|
|
55
|
+
confirmFn,
|
|
56
|
+
recordIdField,
|
|
57
|
+
} = defineProps<{
|
|
53
58
|
availableDoctypes?: string[]
|
|
54
59
|
/**
|
|
55
60
|
* Pluggable router adapter. When provided, Desktop uses these functions for all
|
|
@@ -98,19 +103,45 @@ const emit = defineEmits<{
|
|
|
98
103
|
'load-record': [payload: LoadRecordEventPayload]
|
|
99
104
|
}>()
|
|
100
105
|
|
|
101
|
-
const { availableDoctypes = [] } = props
|
|
102
|
-
|
|
103
106
|
const { stonecrop } = useStonecrop()
|
|
104
107
|
|
|
105
108
|
// State
|
|
106
109
|
const loading = ref(false)
|
|
107
110
|
const commandPaletteOpen = ref(false)
|
|
108
111
|
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
+
// Form/list data management — each view produces a different data shape.
|
|
113
|
+
// List views (doctypes, records) return table row data keyed by fieldname.
|
|
114
|
+
// Record view returns HST record fields for two-way binding.
|
|
112
115
|
const currentViewData = computed<Record<string, any>>({
|
|
113
116
|
get() {
|
|
117
|
+
// Doctypes list — rows come from availableDoctypes prop (reactive via availableDoctypes)
|
|
118
|
+
if (currentView.value === 'doctypes') {
|
|
119
|
+
return {
|
|
120
|
+
doctypes_table:
|
|
121
|
+
availableDoctypes?.map(doctype => ({
|
|
122
|
+
id: doctype,
|
|
123
|
+
doctype,
|
|
124
|
+
display_name: formatDoctypeName(doctype),
|
|
125
|
+
record_count: getRecordCount(doctype),
|
|
126
|
+
actions: 'View Records',
|
|
127
|
+
})) ?? [],
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Records list — rows come from HST store (reactive because HST is Vue reactive())
|
|
132
|
+
if (currentView.value === 'records') {
|
|
133
|
+
const idField = recordIdField || 'id'
|
|
134
|
+
return {
|
|
135
|
+
records_table: getRecords().map(record =>
|
|
136
|
+
Object.assign({}, record, {
|
|
137
|
+
id: record[idField] || record.id || '',
|
|
138
|
+
actions: 'Edit | Delete',
|
|
139
|
+
})
|
|
140
|
+
),
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Record form — read single record from HST
|
|
114
145
|
if (!stonecrop.value || !currentDoctype.value || !currentRecordId.value) {
|
|
115
146
|
return {}
|
|
116
147
|
}
|
|
@@ -128,14 +159,9 @@ const currentViewData = computed<Record<string, any>>({
|
|
|
128
159
|
const doctype = stonecrop.value.registry.registry[currentDoctype.value]
|
|
129
160
|
if (doctype) {
|
|
130
161
|
for (const field of doctype.getSchemaArray()) {
|
|
131
|
-
if (
|
|
132
|
-
'fieldtype' in field &&
|
|
133
|
-
field.fieldtype === 'Fieldset' &&
|
|
134
|
-
'schema' in field &&
|
|
135
|
-
Array.isArray(field.schema)
|
|
136
|
-
) {
|
|
162
|
+
if (field.kind === 'fieldset') {
|
|
137
163
|
const nested: Record<string, any> = {}
|
|
138
|
-
for (const child of field.schema
|
|
164
|
+
for (const child of field.schema) {
|
|
139
165
|
if (child.fieldname) nested[child.fieldname] = flat[child.fieldname]
|
|
140
166
|
}
|
|
141
167
|
flat[field.fieldname] = nested
|
|
@@ -148,6 +174,8 @@ const currentViewData = computed<Record<string, any>>({
|
|
|
148
174
|
}
|
|
149
175
|
},
|
|
150
176
|
set(newData: Record<string, any>) {
|
|
177
|
+
// List views are read-only from AForm's perspective — HST writes only apply to record form
|
|
178
|
+
if (currentView.value !== 'record') return
|
|
151
179
|
if (!stonecrop.value || !currentDoctype.value || !currentRecordId.value) {
|
|
152
180
|
return
|
|
153
181
|
}
|
|
@@ -159,7 +187,7 @@ const currentViewData = computed<Record<string, any>>({
|
|
|
159
187
|
const fieldsetNames = new Set<string>()
|
|
160
188
|
if (doctype) {
|
|
161
189
|
for (const field of doctype.getSchemaArray()) {
|
|
162
|
-
if (
|
|
190
|
+
if (field.kind === 'fieldset') {
|
|
163
191
|
fieldsetNames.add(field.fieldname)
|
|
164
192
|
}
|
|
165
193
|
}
|
|
@@ -182,9 +210,12 @@ const currentViewData = computed<Record<string, any>>({
|
|
|
182
210
|
Object.assign(flatData, nestedValue)
|
|
183
211
|
}
|
|
184
212
|
|
|
185
|
-
// Only update fields that actually changed
|
|
213
|
+
// Only update fields that actually changed. Never write undefined — AForm may emit
|
|
214
|
+
// schema fields absent from the record as undefined; writing them would silently
|
|
215
|
+
// clear values that exist in HST. Explicit null is allowed (intentional clear).
|
|
186
216
|
const hstStore = stonecrop.value.getStore()
|
|
187
217
|
for (const [fieldname, value] of Object.entries(flatData)) {
|
|
218
|
+
if (value === undefined) continue
|
|
188
219
|
const fieldPath = `${currentDoctype.value}.${currentRecordId.value}.${fieldname}`
|
|
189
220
|
const currentValue = hstStore.has(fieldPath) ? hstStore.get(fieldPath) : undefined
|
|
190
221
|
if (currentValue !== value) {
|
|
@@ -199,10 +230,10 @@ const currentViewData = computed<Record<string, any>>({
|
|
|
199
230
|
|
|
200
231
|
// Computed properties for current route context.
|
|
201
232
|
// When a routeAdapter is provided it takes full precedence over the registry's internal router.
|
|
202
|
-
const route = computed(() => (
|
|
203
|
-
const router = computed(() => (
|
|
233
|
+
const route = computed(() => (routeAdapter ? null : unref(stonecrop.value?.registry.router?.currentRoute)))
|
|
234
|
+
const router = computed(() => (routeAdapter ? null : stonecrop.value?.registry.router))
|
|
204
235
|
const currentDoctype = computed(() => {
|
|
205
|
-
if (
|
|
236
|
+
if (routeAdapter) return routeAdapter.getCurrentDoctype()
|
|
206
237
|
if (!route.value) return ''
|
|
207
238
|
|
|
208
239
|
// First check if we have actualDoctype in meta (from registered routes)
|
|
@@ -226,7 +257,7 @@ const currentDoctype = computed(() => {
|
|
|
226
257
|
|
|
227
258
|
// The route doctype for display and navigation (e.g., 'todo')
|
|
228
259
|
const routeDoctype = computed(() => {
|
|
229
|
-
if (
|
|
260
|
+
if (routeAdapter) return routeAdapter.getCurrentDoctype()
|
|
230
261
|
if (!route.value) return ''
|
|
231
262
|
|
|
232
263
|
// Check route meta first
|
|
@@ -249,7 +280,7 @@ const routeDoctype = computed(() => {
|
|
|
249
280
|
})
|
|
250
281
|
|
|
251
282
|
const currentRecordId = computed(() => {
|
|
252
|
-
if (
|
|
283
|
+
if (routeAdapter) return routeAdapter.getCurrentRecordId()
|
|
253
284
|
if (!route.value) return ''
|
|
254
285
|
|
|
255
286
|
// For named routes, use params.recordId
|
|
@@ -269,7 +300,7 @@ const isNewRecord = computed(() => currentRecordId.value?.startsWith('new-'))
|
|
|
269
300
|
|
|
270
301
|
// Determine current view based on route
|
|
271
302
|
const currentView = computed(() => {
|
|
272
|
-
if (
|
|
303
|
+
if (routeAdapter) return routeAdapter.getCurrentView()
|
|
273
304
|
if (!route.value) {
|
|
274
305
|
return 'doctypes'
|
|
275
306
|
}
|
|
@@ -468,8 +499,8 @@ const getRecordCount = (doctype: string): number => {
|
|
|
468
499
|
// or falls back to the registry's Vue Router instance.
|
|
469
500
|
const doNavigate = async (target: NavigationTarget) => {
|
|
470
501
|
emit('navigate', target)
|
|
471
|
-
if (
|
|
472
|
-
await
|
|
502
|
+
if (routeAdapter) {
|
|
503
|
+
await routeAdapter.navigate(target)
|
|
473
504
|
} else {
|
|
474
505
|
if (target.view === 'doctypes') {
|
|
475
506
|
await router.value?.push('/')
|
|
@@ -499,11 +530,11 @@ const createNewRecord = async () => {
|
|
|
499
530
|
// Flatten Fieldset containers into individual columns for list/table views.
|
|
500
531
|
// A Fieldset groups form fields visually but has no DB column; its children are the
|
|
501
532
|
// actual data fields and should appear as flat columns in a list view.
|
|
502
|
-
const flattenFieldsets = (fields:
|
|
503
|
-
const result:
|
|
533
|
+
const flattenFieldsets = (fields: ResolvedField[]): ResolvedField[] => {
|
|
534
|
+
const result: ResolvedField[] = []
|
|
504
535
|
for (const field of fields) {
|
|
505
|
-
if (
|
|
506
|
-
result.push(...flattenFieldsets(field.schema
|
|
536
|
+
if (field.kind === 'fieldset') {
|
|
537
|
+
result.push(...flattenFieldsets(field.schema))
|
|
507
538
|
} else {
|
|
508
539
|
result.push(field)
|
|
509
540
|
}
|
|
@@ -512,65 +543,55 @@ const flattenFieldsets = (fields: SchemaTypes[]): SchemaTypes[] => {
|
|
|
512
543
|
}
|
|
513
544
|
|
|
514
545
|
// Schema generator functions - moved here to be available to computed properties
|
|
515
|
-
const getDoctypesSchema = ():
|
|
516
|
-
if (!availableDoctypes
|
|
517
|
-
|
|
518
|
-
const rows = availableDoctypes.map(doctype => ({
|
|
519
|
-
id: doctype,
|
|
520
|
-
doctype,
|
|
521
|
-
display_name: formatDoctypeName(doctype),
|
|
522
|
-
record_count: getRecordCount(doctype),
|
|
523
|
-
actions: 'View Records',
|
|
524
|
-
}))
|
|
546
|
+
const getDoctypesSchema = (): ResolvedField[] => {
|
|
547
|
+
if (!availableDoctypes?.length) return []
|
|
525
548
|
|
|
526
549
|
return [
|
|
527
550
|
{
|
|
551
|
+
kind: 'table' as const,
|
|
528
552
|
fieldname: 'doctypes_table',
|
|
529
553
|
component: 'ATable',
|
|
530
|
-
|
|
554
|
+
label: 'Doctypes',
|
|
555
|
+
schema: [
|
|
531
556
|
{
|
|
557
|
+
fieldname: 'doctype',
|
|
532
558
|
label: 'Doctype',
|
|
533
|
-
name: 'doctype',
|
|
534
559
|
fieldtype: 'Data',
|
|
535
|
-
align: 'left',
|
|
560
|
+
align: 'left' as const,
|
|
536
561
|
edit: false,
|
|
537
562
|
width: '20ch',
|
|
538
563
|
},
|
|
539
564
|
{
|
|
565
|
+
fieldname: 'display_name',
|
|
540
566
|
label: 'Name',
|
|
541
|
-
name: 'display_name',
|
|
542
567
|
fieldtype: 'Data',
|
|
543
|
-
align: 'left',
|
|
568
|
+
align: 'left' as const,
|
|
544
569
|
edit: false,
|
|
545
570
|
width: '30ch',
|
|
546
571
|
},
|
|
547
572
|
{
|
|
573
|
+
fieldname: 'record_count',
|
|
548
574
|
label: 'Records',
|
|
549
|
-
name: 'record_count',
|
|
550
575
|
fieldtype: 'Int',
|
|
551
|
-
align: 'center',
|
|
576
|
+
align: 'center' as const,
|
|
552
577
|
edit: false,
|
|
553
578
|
width: '15ch',
|
|
554
579
|
},
|
|
555
580
|
{
|
|
581
|
+
fieldname: 'actions',
|
|
556
582
|
label: 'Actions',
|
|
557
|
-
name: 'actions',
|
|
558
583
|
fieldtype: 'Data',
|
|
559
|
-
align: 'center',
|
|
584
|
+
align: 'center' as const,
|
|
560
585
|
edit: false,
|
|
561
586
|
width: '20ch',
|
|
562
587
|
},
|
|
563
588
|
],
|
|
564
|
-
config: {
|
|
565
|
-
|
|
566
|
-
fullWidth: true,
|
|
567
|
-
},
|
|
568
|
-
rows,
|
|
569
|
-
},
|
|
589
|
+
config: { view: 'list' as const, fullWidth: true },
|
|
590
|
+
} satisfies ResolvedTable,
|
|
570
591
|
]
|
|
571
592
|
}
|
|
572
593
|
|
|
573
|
-
const getRecordsSchema = ():
|
|
594
|
+
const getRecordsSchema = (): ResolvedField[] => {
|
|
574
595
|
if (!currentDoctype.value) return []
|
|
575
596
|
if (!stonecrop.value) return []
|
|
576
597
|
|
|
@@ -584,35 +605,21 @@ const getRecordsSchema = (): SchemaTypes[] => {
|
|
|
584
605
|
// If no schema is available, let the template fallback handle the loading state
|
|
585
606
|
if (schema.length === 0) return []
|
|
586
607
|
|
|
587
|
-
const records = getRecords()
|
|
588
|
-
const idField = props.recordIdField || 'id'
|
|
589
|
-
|
|
590
|
-
const rows = records.map(record =>
|
|
591
|
-
Object.assign({}, record, {
|
|
592
|
-
id: record[idField] || record.id || '',
|
|
593
|
-
actions: 'Edit | Delete',
|
|
594
|
-
})
|
|
595
|
-
)
|
|
596
|
-
|
|
597
608
|
return [
|
|
598
609
|
{
|
|
610
|
+
kind: 'table' as const,
|
|
599
611
|
fieldname: 'records_table',
|
|
600
612
|
component: 'ATable',
|
|
601
|
-
kind: 'table',
|
|
602
613
|
schema: [
|
|
603
614
|
...(flattenFieldsets(schema) as ColumnSchema[]),
|
|
604
615
|
{ fieldname: 'actions', label: 'Actions', fieldtype: 'Data' },
|
|
605
616
|
],
|
|
606
|
-
config: {
|
|
607
|
-
|
|
608
|
-
fullWidth: true,
|
|
609
|
-
},
|
|
610
|
-
rows,
|
|
611
|
-
},
|
|
617
|
+
config: { view: 'list' as const, fullWidth: true },
|
|
618
|
+
} satisfies ResolvedTable,
|
|
612
619
|
]
|
|
613
620
|
}
|
|
614
621
|
|
|
615
|
-
const getRecordFormSchema = ():
|
|
622
|
+
const getRecordFormSchema = (): ResolvedField[] => {
|
|
616
623
|
if (!currentDoctype.value) return []
|
|
617
624
|
if (!stonecrop.value) return []
|
|
618
625
|
|
|
@@ -674,8 +681,8 @@ const handleDelete = async (recordId?: string) => {
|
|
|
674
681
|
const targetRecordId = recordId || currentRecordId.value
|
|
675
682
|
if (!targetRecordId) return
|
|
676
683
|
|
|
677
|
-
const confirmed =
|
|
678
|
-
? await
|
|
684
|
+
const confirmed = confirmFn
|
|
685
|
+
? await confirmFn('Are you sure you want to delete this record?')
|
|
679
686
|
: confirm('Are you sure you want to delete this record?')
|
|
680
687
|
|
|
681
688
|
if (confirmed) {
|
|
@@ -703,7 +710,7 @@ const getRecordIdFromRow = (rowElement: HTMLTableRowElement): string | null => {
|
|
|
703
710
|
const record = records[rowIndex]
|
|
704
711
|
if (!record) return null
|
|
705
712
|
|
|
706
|
-
const idField =
|
|
713
|
+
const idField = recordIdField || 'id'
|
|
707
714
|
return record[idField] || record.id || null
|
|
708
715
|
}
|
|
709
716
|
|
|
@@ -834,25 +841,21 @@ provide('aformLinkResolver', async (doctypeSlug: string, id: string): Promise<st
|
|
|
834
841
|
}
|
|
835
842
|
})
|
|
836
843
|
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
}
|
|
845
|
-
// Escape to close command palette
|
|
846
|
-
if (event.key === 'Escape' && commandPaletteOpen.value) {
|
|
847
|
-
commandPaletteOpen.value = false
|
|
848
|
-
}
|
|
844
|
+
const handleKeydown = (event: KeyboardEvent) => {
|
|
845
|
+
if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
|
|
846
|
+
event.preventDefault()
|
|
847
|
+
commandPaletteOpen.value = true
|
|
848
|
+
}
|
|
849
|
+
if (event.key === 'Escape' && commandPaletteOpen.value) {
|
|
850
|
+
commandPaletteOpen.value = false
|
|
849
851
|
}
|
|
852
|
+
}
|
|
850
853
|
|
|
854
|
+
onMounted(() => {
|
|
851
855
|
document.addEventListener('keydown', handleKeydown)
|
|
856
|
+
})
|
|
852
857
|
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
document.removeEventListener('keydown', handleKeydown)
|
|
856
|
-
}
|
|
858
|
+
onUnmounted(() => {
|
|
859
|
+
document.removeEventListener('keydown', handleKeydown)
|
|
857
860
|
})
|
|
858
861
|
</script>
|