@stonecrop/stonecrop 0.10.10 → 0.10.12

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/stonecrop",
3
- "version": "0.10.10",
3
+ "version": "0.10.12",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "author": {
@@ -34,7 +34,7 @@
34
34
  "pinia-shared-state": "^1.0.1",
35
35
  "pinia-xstate": "^3.0.0",
36
36
  "xstate": "^5.25.0",
37
- "@stonecrop/schema": "0.10.10"
37
+ "@stonecrop/schema": "0.10.12"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "pinia": "^3.0.4",
@@ -60,9 +60,9 @@
60
60
  "vue-router": "^5.0.2",
61
61
  "vite": "^7.3.1",
62
62
  "vitest": "^4.0.18",
63
- "stonecrop-rig": "0.7.0",
64
- "@stonecrop/aform": "0.10.10",
65
- "@stonecrop/atable": "0.10.10"
63
+ "@stonecrop/aform": "0.10.12",
64
+ "@stonecrop/atable": "0.10.12",
65
+ "stonecrop-rig": "0.7.0"
66
66
  },
67
67
  "description": "Schema-driven framework with XState workflows and HST state management",
68
68
  "publishConfig": {
@@ -1,13 +1,19 @@
1
- import { inject, onMounted, Ref, ref, watch, provide, computed, ComputedRef } from 'vue'
1
+ import {
2
+ type DoctypeManySchema,
3
+ type DoctypeOneSchema,
4
+ type DoctypeSchema,
5
+ type SchemaTypes,
6
+ isDoctypeMany,
7
+ } from '@stonecrop/aform'
8
+ import { storeToRefs } from 'pinia'
9
+ import { inject, onMounted, Ref, ref, watch, provide, computed, type ComputedRef } from 'vue'
2
10
 
3
11
  import Registry from '../registry'
4
12
  import { Stonecrop } from '../stonecrop'
5
13
  import Doctype from '../doctype'
6
14
  import type { HSTNode } from '../stores/hst'
7
- import { RouteContext } from '../types/registry'
8
- import { storeToRefs } from 'pinia'
15
+ import type { RouteContext } from '../types/registry'
9
16
  import type { HSTOperation, OperationLogConfig, OperationLogSnapshot } from '../types/operation-log'
10
- import { SchemaTypes, DoctypeSchema } from '@stonecrop/aform'
11
17
 
12
18
  /**
13
19
  * Operation Log API - nested object containing all operation log functionality
@@ -66,7 +72,7 @@ export type HSTStonecropReturn = BaseStonecropReturn & {
66
72
  formData: Ref<Record<string, any>>
67
73
  resolvedSchema: Ref<SchemaTypes[]>
68
74
  loadNestedData: (parentPath: string, childDoctype: Doctype, recordId?: string) => Record<string, any>
69
- saveRecursive: (doctype: Doctype, recordId: string) => Promise<Record<string, any>>
75
+ collectRecordPayload: (doctype: Doctype, recordId: string) => Record<string, any>
70
76
  createNestedContext: (
71
77
  basePath: string,
72
78
  childDoctype: Doctype
@@ -501,12 +507,12 @@ export function useStonecrop(options?: {
501
507
  }
502
508
 
503
509
  /**
504
- * Recursively save a record with all nested doctype fields
510
+ * Collect a record payload with all nested doctype fields from HST
505
511
  * @param doctype - The doctype metadata
506
- * @param recordId - The record ID to save
507
- * @returns The complete save payload
512
+ * @param recordId - The record ID to collect
513
+ * @returns The complete record payload ready for API submission
508
514
  */
509
- const saveRecursive = (doctype: Doctype, recordId: string): Record<string, any> => {
515
+ const collectRecordPayload = (doctype: Doctype, recordId: string): Record<string, any> => {
510
516
  if (!hstStore.value || !stonecrop.value) {
511
517
  throw new Error('HST store not initialized')
512
518
  }
@@ -524,18 +530,40 @@ export function useStonecrop(options?: {
524
530
  : Array.from(doctype.schema)
525
531
  : []
526
532
  const resolved = registry ? registry.resolveSchema(schemaArray) : schemaArray
533
+
534
+ // 1:1 nested Doctype fields (cardinality: 'one' or undefined)
527
535
  const doctypeFields = resolved.filter(
528
- field => 'fieldtype' in field && field.fieldtype === 'Doctype' && 'schema' in field && Array.isArray(field.schema)
536
+ field =>
537
+ 'fieldtype' in field &&
538
+ field.fieldtype === 'Doctype' &&
539
+ !isDoctypeMany(field as DoctypeSchema) &&
540
+ 'schema' in field &&
541
+ Array.isArray(field.schema)
529
542
  )
530
543
 
531
544
  // Recursively collect nested data from HST using resolved schemas
532
545
  for (const field of doctypeFields) {
533
- const doctypeField = field as DoctypeSchema
546
+ const doctypeField = field as DoctypeOneSchema
534
547
  const fieldPath = `${recordPath}.${doctypeField.fieldname}`
535
548
  const nestedData = collectNestedData(doctypeField.schema!, fieldPath, hstStore.value)
536
549
  payload[doctypeField.fieldname] = nestedData
537
550
  }
538
551
 
552
+ // 1:many child tables (cardinality: 'many')
553
+ const doctypeManyFields = resolved.filter(
554
+ field => 'fieldtype' in field && field.fieldtype === 'Doctype' && isDoctypeMany(field as DoctypeSchema)
555
+ )
556
+
557
+ // Read array data from HST for cardinality: 'many' fields
558
+ for (const field of doctypeManyFields) {
559
+ const doctypeField = field as DoctypeManySchema
560
+ const fieldPath = `${recordPath}.${doctypeField.fieldname}`
561
+ const arrayData = hstStore.value.get(fieldPath)
562
+ if (Array.isArray(arrayData)) {
563
+ payload[doctypeField.fieldname] = arrayData
564
+ }
565
+ }
566
+
539
567
  return payload
540
568
  }
541
569
 
@@ -599,7 +627,7 @@ export function useStonecrop(options?: {
599
627
  formData,
600
628
  resolvedSchema,
601
629
  loadNestedData,
602
- saveRecursive,
630
+ collectRecordPayload,
603
631
  createNestedContext,
604
632
  isLoading,
605
633
  error,
@@ -616,7 +644,7 @@ export function useStonecrop(options?: {
616
644
  formData,
617
645
  resolvedSchema,
618
646
  loadNestedData,
619
- saveRecursive,
647
+ collectRecordPayload,
620
648
  createNestedContext,
621
649
  isLoading,
622
650
  error,
@@ -656,12 +684,21 @@ function initializeNewRecord(doctype: Doctype): Record<string, any> {
656
684
  case 'Float':
657
685
  initialData[field.fieldname] = 0
658
686
  break
659
- case 'Table':
660
- initialData[field.fieldname] = []
661
- break
662
687
  case 'JSON':
663
688
  initialData[field.fieldname] = {}
664
689
  break
690
+ case 'Doctype': {
691
+ // Check cardinality to determine initial value
692
+ const cardinality = 'cardinality' in field ? field.cardinality : undefined
693
+ if (cardinality === 'many') {
694
+ // 1:many child table - initialize as empty array
695
+ initialData[field.fieldname] = []
696
+ } else {
697
+ // 1:1 nested form - initialize as empty object
698
+ initialData[field.fieldname] = {}
699
+ }
700
+ break
701
+ }
665
702
  default:
666
703
  initialData[field.fieldname] = null
667
704
  }
@@ -728,18 +765,37 @@ function collectNestedData(resolvedSchema: SchemaTypes[], basePath: string, hstS
728
765
  const data = hstStore.get(basePath) || {}
729
766
  const payload: Record<string, any> = { ...data }
730
767
 
731
- // Find Doctype fields that have resolved child schemas
768
+ // Find Doctype fields that have resolved child schemas (1:1 only, not cardinality: 'many')
732
769
  const doctypeFields = resolvedSchema.filter(
733
- field => 'fieldtype' in field && field.fieldtype === 'Doctype' && 'schema' in field && Array.isArray(field.schema)
770
+ field =>
771
+ 'fieldtype' in field &&
772
+ field.fieldtype === 'Doctype' &&
773
+ !isDoctypeMany(field as DoctypeSchema) &&
774
+ 'schema' in field &&
775
+ Array.isArray(field.schema)
734
776
  )
735
777
 
736
778
  // Recursively collect nested data
737
779
  for (const field of doctypeFields) {
738
- const doctypeField = field as DoctypeSchema
780
+ const doctypeField = field as DoctypeOneSchema
739
781
  const fieldPath = `${basePath}.${doctypeField.fieldname}`
740
782
  const nestedData = collectNestedData(doctypeField.schema!, fieldPath, hstStore)
741
783
  payload[doctypeField.fieldname] = nestedData
742
784
  }
743
785
 
786
+ // Also collect array data for cardinality: 'many' fields
787
+ const doctypeManyFields = resolvedSchema.filter(
788
+ field => 'fieldtype' in field && field.fieldtype === 'Doctype' && isDoctypeMany(field as DoctypeSchema)
789
+ )
790
+
791
+ for (const field of doctypeManyFields) {
792
+ const doctypeField = field as DoctypeManySchema
793
+ const fieldPath = `${basePath}.${doctypeField.fieldname}`
794
+ const arrayData = hstStore.get(fieldPath)
795
+ if (Array.isArray(arrayData)) {
796
+ payload[doctypeField.fieldname] = arrayData
797
+ }
798
+ }
799
+
744
800
  return payload
745
801
  }
@@ -1,4 +1,3 @@
1
- /* eslint-disable no-new-func, no-eval */
2
1
  import type { Map as ImmutableMap } from 'immutable'
3
2
  import { useOperationLogStore } from './stores/operation-log'
4
3
  import type {
@@ -101,11 +100,11 @@ export class FieldTriggerEngine {
101
100
  const transitionMap = new Map<string, string[]>()
102
101
 
103
102
  // Convert from different Map types to regular Map
104
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
105
- if (typeof (actions as any).entrySeq === 'function') {
103
+ // Check for Immutable.js Map first (has entrySeq method)
104
+ const immutableActions = actions as ImmutableMap<string, string[]>
105
+ if (typeof immutableActions.entrySeq === 'function') {
106
106
  // Immutable Map
107
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
108
- ;(actions as any).entrySeq().forEach(([key, value]: [string, string[]]) => {
107
+ immutableActions.entrySeq().forEach(([key, value]: [string, string[]]) => {
109
108
  this.categorizeAction(key, value, actionMap, transitionMap)
110
109
  })
111
110
  } else if (actions instanceof Map) {
@@ -506,7 +505,7 @@ export class FieldTriggerEngine {
506
505
  })
507
506
  .catch(error => {
508
507
  clearTimeout(timeoutId)
509
- reject(error)
508
+ reject(error instanceof Error ? error : new Error(String(error)))
510
509
  })
511
510
  })
512
511
  }
package/src/registry.ts CHANGED
@@ -85,17 +85,18 @@ export default class Registry {
85
85
  }
86
86
 
87
87
  /**
88
- * Resolve nested Doctype and Table fields in a schema by embedding child schemas inline.
88
+ * Resolve nested Doctype fields in a schema by embedding child schemas inline.
89
89
  *
90
90
  * @remarks
91
91
  * Walks the schema array and for each field with `fieldtype: 'Doctype'` and a string
92
- * `options` value, looks up the referenced doctype in the registry and embeds its schema
93
- * as the field's `schema` property. Recurses for deeply nested doctypes.
92
+ * `options` value, looks up the referenced doctype in the registry and:
94
93
  *
95
- * For fields with `fieldtype: 'Table'`, looks up the referenced child doctype and
96
- * auto-derives `columns` from its schema fields (unless columns are already provided).
97
- * Also sets sensible defaults for `component` (`'ATable'`) and `config` (`{ view: 'list' }`).
98
- * Row data is expected to come from the parent form's data model at `data[fieldname]`.
94
+ * - If `cardinality: 'many'`: auto-derives `columns` from the child doctype's schema,
95
+ * sets `component: 'ATable'`, `config: { view: 'list' }`, and initializes `rows: []`.
96
+ * - Otherwise (default/`cardinality: 'one'`): embeds the child schema as the field's
97
+ * `schema` property for 1:1 nested forms.
98
+ *
99
+ * Recurses for deeply nested doctypes. Circular references are protected against.
99
100
  *
100
101
  * Returns a new array — does not mutate the original schema.
101
102
  *
@@ -137,64 +138,52 @@ export default class Registry {
137
138
  // Convert Immutable.List to plain array if needed
138
139
  const childSchema: SchemaTypes[] = Array.isArray(doctype.schema) ? doctype.schema : Array.from(doctype.schema)
139
140
 
140
- // Recurse into child schema to resolve deeply nested doctypes
141
- seen.add(doctypeSlug)
142
- const resolvedChild = this.resolveSchema(childSchema, seen)
143
- seen.delete(doctypeSlug)
144
-
145
- return { ...field, schema: resolvedChild }
146
- }
147
- }
141
+ // Check cardinality to determine handling
142
+ const cardinality = 'cardinality' in field ? field.cardinality : undefined
148
143
 
149
- // Resolve Table fieldtype — 1:many child doctype rendered as ATable
150
- if (
151
- 'fieldtype' in field &&
152
- field.fieldtype === 'Table' &&
153
- 'options' in field &&
154
- typeof field.options === 'string'
155
- ) {
156
- const doctypeSlug = field.options
144
+ if (cardinality === 'many') {
145
+ // 1:many child table - derive columns, set component, config, rows
146
+ const resolved: Record<string, any> = { ...field }
157
147
 
158
- // Circular reference protection
159
- if (seen.has(doctypeSlug)) {
160
- return { ...field }
161
- }
148
+ // Auto-derive columns from child schema fields if not already provided
149
+ if (!('columns' in field) || !field.columns) {
150
+ resolved.columns = childSchema.map(childField => ({
151
+ name: childField.fieldname,
152
+ fieldname: childField.fieldname,
153
+ label: ('label' in childField && childField.label) || childField.fieldname,
154
+ fieldtype: 'fieldtype' in childField ? childField.fieldtype : 'Data',
155
+ align: ('align' in childField && childField.align) || 'left',
156
+ edit: 'edit' in childField ? childField.edit : true,
157
+ width: ('width' in childField && childField.width) || '20ch',
158
+ }))
159
+ }
162
160
 
163
- const doctype = this.registry[doctypeSlug]
164
- if (doctype && doctype.schema) {
165
- const childSchema: SchemaTypes[] = Array.isArray(doctype.schema) ? doctype.schema : Array.from(doctype.schema)
166
- const resolved: Record<string, any> = { ...field }
161
+ // Set default component if not already specified
162
+ if (!resolved.component) {
163
+ resolved.component = 'ATable'
164
+ }
167
165
 
168
- // Auto-derive columns from child schema fields if not already provided
169
- if (!('columns' in field) || !field.columns) {
170
- resolved.columns = childSchema.map(childField => ({
171
- name: childField.fieldname,
172
- fieldname: childField.fieldname,
173
- label: ('label' in childField && childField.label) || childField.fieldname,
174
- fieldtype: 'fieldtype' in childField ? childField.fieldtype : 'Data',
175
- align: ('align' in childField && childField.align) || 'left',
176
- edit: 'edit' in childField ? childField.edit : true,
177
- width: ('width' in childField && childField.width) || '20ch',
178
- }))
179
- }
166
+ // Set default config if not already specified
167
+ if (!('config' in field) || !field.config) {
168
+ resolved.config = { view: 'list' }
169
+ }
180
170
 
181
- // Set default component if not already specified
182
- if (!resolved.component) {
183
- resolved.component = 'ATable'
184
- }
171
+ // Initialize rows to empty array so componentProps fallback
172
+ // routes data from the form's dataModel[fieldname]
173
+ if (!('rows' in field) || !field.rows) {
174
+ resolved.rows = []
175
+ }
185
176
 
186
- // Set default config if not already specified
187
- if (!('config' in field) || !field.config) {
188
- resolved.config = { view: 'list' }
189
- }
177
+ return resolved as SchemaTypes
178
+ } else {
179
+ // 1:1 nested form (default cardinality: 'one')
180
+ // Recurse into child schema to resolve deeply nested doctypes
181
+ seen.add(doctypeSlug)
182
+ const resolvedChild = this.resolveSchema(childSchema, seen)
183
+ seen.delete(doctypeSlug)
190
184
 
191
- // Initialize rows to empty array so componentProps fallback
192
- // routes data from the form's dataModel[fieldname]
193
- if (!('rows' in field) || !field.rows) {
194
- resolved.rows = []
185
+ return { ...field, schema: resolvedChild }
195
186
  }
196
-
197
- return resolved as SchemaTypes
198
187
  }
199
188
  }
200
189
 
@@ -211,11 +200,13 @@ export default class Registry {
211
200
  * - Data, Text → `''`
212
201
  * - Check → `false`
213
202
  * - Int, Float, Decimal, Currency, Quantity → `0`
214
- * - Table → `[]`
215
- * - JSON, Doctype → `{}`
203
+ * - JSON → `{}`
204
+ * - Doctype with `cardinality: 'many'` → `[]`
205
+ * - Doctype without `cardinality` or `cardinality: 'one'` → recursively initializes nested record
216
206
  * - All others → `null`
217
207
  *
218
- * For Doctype fields with a resolved `schema` array, recursively initializes the nested record.
208
+ * For Doctype fields with a resolved `schema` array (cardinality: 'one'), recursively
209
+ * initializes the nested record.
219
210
  *
220
211
  * @param schema - The schema array to derive defaults from
221
212
  * @returns A plain object with default values for each field
@@ -250,20 +241,24 @@ export default class Registry {
250
241
  case 'Quantity':
251
242
  record[field.fieldname] = 0
252
243
  break
253
- case 'Table':
254
- record[field.fieldname] = []
255
- break
256
244
  case 'JSON':
257
245
  record[field.fieldname] = {}
258
246
  break
259
- case 'Doctype':
260
- // If nested schema is resolved, recursively initialize
261
- if ('schema' in field && Array.isArray(field.schema)) {
247
+ case 'Doctype': {
248
+ // Check cardinality to determine initial value
249
+ const cardinality = 'cardinality' in field ? field.cardinality : undefined
250
+ if (cardinality === 'many') {
251
+ // 1:many child table - initialize as empty array
252
+ record[field.fieldname] = []
253
+ } else if ('schema' in field && Array.isArray(field.schema)) {
254
+ // 1:1 nested form with resolved schema - recursively initialize
262
255
  record[field.fieldname] = this.initializeRecord(field.schema)
263
256
  } else {
257
+ // 1:1 without resolved schema - empty object
264
258
  record[field.fieldname] = {}
265
259
  }
266
260
  break
261
+ }
267
262
  default:
268
263
  record[field.fieldname] = null
269
264
  }
package/src/stonecrop.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DataClient, WorkflowMeta } from '@stonecrop/schema'
1
+ import type { DataClient } from '@stonecrop/schema'
2
2
  import { reactive } from 'vue'
3
3
 
4
4
  import Doctype from './doctype'