@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonecrop/desktop",
3
- "version": "0.9.2",
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.9.2",
36
- "@stonecrop/atable": "0.9.2",
37
- "@stonecrop/stonecrop": "0.9.2",
38
- "@stonecrop/themes": "0.9.2"
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="writableSchema.length > 0" v-model="writableSchema" :data="currentViewData" />
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
- return record?.get('') || {}
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
- // Update each field in HST, which will automatically trigger field actions
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.set(fieldPath, value)
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 (error) {
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, show a loading or empty state
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: '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
- `,
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
- // 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
- ]
538
+ // Let the template fallback handle the loading state
539
+ return []
641
540
  }
642
541
 
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
- ]
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 (error) {
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 (error) {
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 = (label: string, action: (() => void | Promise<void>) | undefined) => {
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
  }