@stonecrop/desktop 0.9.2 → 0.10.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.js +1314 -1494
- package/dist/desktop.js.map +1 -1
- package/package.json +5 -5
- package/src/components/Desktop.vue +47 -260
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stonecrop/desktop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": {
|
|
@@ -32,10 +32,10 @@
|
|
|
32
32
|
"**/*.css"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@stonecrop/aform": "0.
|
|
36
|
-
"@stonecrop/atable": "0.
|
|
37
|
-
"@stonecrop/
|
|
38
|
-
"@stonecrop/
|
|
35
|
+
"@stonecrop/aform": "0.10.0",
|
|
36
|
+
"@stonecrop/atable": "0.10.0",
|
|
37
|
+
"@stonecrop/themes": "0.10.0",
|
|
38
|
+
"@stonecrop/stonecrop": "0.10.0"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"vue": "^3.5.28"
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<ActionSet :elements="actionElements" @action-click="handleActionClick" />
|
|
5
5
|
|
|
6
6
|
<!-- Main content using AForm -->
|
|
7
|
-
<AForm v-if="
|
|
7
|
+
<AForm v-if="currentViewSchema.length > 0" v-model:data="currentViewData" :schema="currentViewSchema" />
|
|
8
8
|
<div v-else-if="!stonecrop" class="loading"><p>Initializing Stonecrop...</p></div>
|
|
9
9
|
<div v-else class="loading">
|
|
10
10
|
<p>Loading {{ currentView }} data...</p>
|
|
@@ -60,7 +60,10 @@ const currentViewData = computed<Record<string, any>>({
|
|
|
60
60
|
|
|
61
61
|
try {
|
|
62
62
|
const record = stonecrop.value.getRecordById(currentDoctype.value, currentRecordId.value)
|
|
63
|
-
|
|
63
|
+
// Return a plain shallow copy so AForm mutations don't propagate directly into
|
|
64
|
+
// the HST reactive object, which would bypass field-trigger diffing and cause
|
|
65
|
+
// setupDeepReactivity to fire triggers for all fields on every keystroke.
|
|
66
|
+
return { ...(record?.get('') || {}) }
|
|
64
67
|
} catch {
|
|
65
68
|
return {}
|
|
66
69
|
}
|
|
@@ -71,11 +74,14 @@ const currentViewData = computed<Record<string, any>>({
|
|
|
71
74
|
}
|
|
72
75
|
|
|
73
76
|
try {
|
|
74
|
-
//
|
|
77
|
+
// Only update fields that actually changed to avoid triggering actions for unchanged fields
|
|
75
78
|
const hstStore = stonecrop.value.getStore()
|
|
76
79
|
for (const [fieldname, value] of Object.entries(newData)) {
|
|
77
80
|
const fieldPath = `${currentDoctype.value}.${currentRecordId.value}.${fieldname}`
|
|
78
|
-
hstStore.
|
|
81
|
+
const currentValue = hstStore.has(fieldPath) ? hstStore.get(fieldPath) : undefined
|
|
82
|
+
if (currentValue !== value) {
|
|
83
|
+
hstStore.set(fieldPath, value)
|
|
84
|
+
}
|
|
79
85
|
}
|
|
80
86
|
} catch (error) {
|
|
81
87
|
// eslint-disable-next-line no-console
|
|
@@ -406,7 +412,7 @@ const loadDoctypeMetadata = (doctype: string) => {
|
|
|
406
412
|
// The router should have already loaded the metadata, but this ensures the HST structure exists
|
|
407
413
|
try {
|
|
408
414
|
stonecrop.value.records(doctype)
|
|
409
|
-
} catch
|
|
415
|
+
} catch {
|
|
410
416
|
// Silent error handling - structure will be created if needed
|
|
411
417
|
}
|
|
412
418
|
}
|
|
@@ -424,15 +430,6 @@ const getDoctypesSchema = (): SchemaTypes[] => {
|
|
|
424
430
|
}))
|
|
425
431
|
|
|
426
432
|
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
433
|
{
|
|
437
434
|
fieldname: 'doctypes_table',
|
|
438
435
|
component: 'ATable',
|
|
@@ -486,33 +483,9 @@ const getRecordsSchema = (): SchemaTypes[] => {
|
|
|
486
483
|
const records = getRecords()
|
|
487
484
|
const columns = getColumns()
|
|
488
485
|
|
|
489
|
-
// If no columns are available,
|
|
486
|
+
// If no columns are available, let the template fallback handle the loading state
|
|
490
487
|
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
|
-
]
|
|
488
|
+
return []
|
|
516
489
|
}
|
|
517
490
|
|
|
518
491
|
const rows = records.map((record: any) => ({
|
|
@@ -524,74 +497,32 @@ const getRecordsSchema = (): SchemaTypes[] => {
|
|
|
524
497
|
|
|
525
498
|
return [
|
|
526
499
|
{
|
|
527
|
-
fieldname: '
|
|
528
|
-
component: '
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
500
|
+
fieldname: 'records_table',
|
|
501
|
+
component: 'ATable',
|
|
502
|
+
columns: [
|
|
503
|
+
...columns.map(col => ({
|
|
504
|
+
label: col.label,
|
|
505
|
+
name: col.fieldname,
|
|
506
|
+
fieldtype: col.fieldtype,
|
|
507
|
+
align: 'left',
|
|
508
|
+
edit: false,
|
|
509
|
+
width: '20ch',
|
|
510
|
+
})),
|
|
511
|
+
{
|
|
512
|
+
label: 'Actions',
|
|
513
|
+
name: 'actions',
|
|
514
|
+
fieldtype: 'Data',
|
|
515
|
+
align: 'center',
|
|
516
|
+
edit: false,
|
|
517
|
+
width: '20ch',
|
|
518
|
+
},
|
|
519
|
+
] as TableColumn[],
|
|
520
|
+
config: {
|
|
521
|
+
view: 'list',
|
|
522
|
+
fullWidth: true,
|
|
523
|
+
} as TableConfig,
|
|
524
|
+
rows,
|
|
550
525
|
},
|
|
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
|
-
fieldtype: col.fieldtype,
|
|
575
|
-
align: 'left',
|
|
576
|
-
edit: false,
|
|
577
|
-
width: '20ch',
|
|
578
|
-
})),
|
|
579
|
-
{
|
|
580
|
-
label: 'Actions',
|
|
581
|
-
name: 'actions',
|
|
582
|
-
fieldtype: '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
526
|
]
|
|
596
527
|
}
|
|
597
528
|
|
|
@@ -604,101 +535,14 @@ const getRecordFormSchema = (): SchemaTypes[] => {
|
|
|
604
535
|
const meta = registry?.registry[currentDoctype.value]
|
|
605
536
|
|
|
606
537
|
if (!meta?.schema) {
|
|
607
|
-
//
|
|
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
|
-
]
|
|
538
|
+
// Let the template fallback handle the loading state
|
|
539
|
+
return []
|
|
641
540
|
}
|
|
642
541
|
|
|
643
|
-
|
|
644
|
-
|
|
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
|
-
]
|
|
542
|
+
// Data is provided via v-model:data="currentViewData" — no need to spread values into schema
|
|
543
|
+
return 'toArray' in meta.schema ? meta.schema.toArray() : meta.schema
|
|
544
|
+
} catch {
|
|
545
|
+
return []
|
|
702
546
|
}
|
|
703
547
|
}
|
|
704
548
|
|
|
@@ -733,20 +577,13 @@ const getColumns = () => {
|
|
|
733
577
|
fieldtype: ('fieldtype' in field && field.fieldtype) || 'Data',
|
|
734
578
|
}))
|
|
735
579
|
}
|
|
736
|
-
} catch
|
|
580
|
+
} catch {
|
|
737
581
|
// Error getting schema - return empty array
|
|
738
582
|
}
|
|
739
583
|
|
|
740
584
|
return []
|
|
741
585
|
}
|
|
742
586
|
|
|
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
587
|
// Schema for different views - defined here after all helper functions are available
|
|
751
588
|
const currentViewSchema = computed<SchemaTypes[]>(() => {
|
|
752
589
|
switch (currentView.value) {
|
|
@@ -761,57 +598,8 @@ const currentViewSchema = computed<SchemaTypes[]>(() => {
|
|
|
761
598
|
}
|
|
762
599
|
})
|
|
763
600
|
|
|
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
601
|
// Action handlers (will be triggered by button clicks in the UI)
|
|
813
602
|
const handleSave = async () => {
|
|
814
|
-
// eslint-disable-next-line no-console
|
|
815
603
|
if (!stonecrop.value) return
|
|
816
604
|
|
|
817
605
|
saving.value = true
|
|
@@ -850,7 +638,7 @@ const handleSave = async () => {
|
|
|
850
638
|
})
|
|
851
639
|
}
|
|
852
640
|
}
|
|
853
|
-
} catch
|
|
641
|
+
} catch {
|
|
854
642
|
// Silently handle error
|
|
855
643
|
} finally {
|
|
856
644
|
saving.value = false
|
|
@@ -878,8 +666,7 @@ const handleCancel = async () => {
|
|
|
878
666
|
}
|
|
879
667
|
}
|
|
880
668
|
|
|
881
|
-
const handleActionClick = (
|
|
882
|
-
// eslint-disable-next-line no-console
|
|
669
|
+
const handleActionClick = (_label: string, action: (() => void | Promise<void>) | undefined) => {
|
|
883
670
|
if (action) {
|
|
884
671
|
void action()
|
|
885
672
|
}
|