@stonecrop/stonecrop 0.7.8 → 0.8.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/src/composable.d.ts +8 -0
- package/dist/src/composable.d.ts.map +1 -1
- package/dist/src/composable.js +129 -0
- package/dist/src/registry.d.ts +59 -0
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +166 -0
- package/dist/src/stonecrop.d.ts.map +1 -1
- package/dist/src/stonecrop.js +6 -3
- package/dist/src/stores/hst.d.ts +5 -0
- package/dist/src/stores/hst.d.ts.map +1 -1
- package/dist/src/stores/hst.js +10 -3
- package/dist/stonecrop.d.ts +163 -1
- package/dist/stonecrop.js +917 -729
- package/dist/stonecrop.js.map +1 -1
- package/dist/stonecrop.umd.cjs +3 -3
- package/dist/stonecrop.umd.cjs.map +1 -1
- package/package.json +3 -3
- package/src/composable.ts +166 -0
- package/src/registry.ts +189 -0
- package/src/stonecrop.ts +6 -3
- package/src/stores/hst.ts +13 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stonecrop/stonecrop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "schema helper",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
"typescript-eslint": "^8.53.0",
|
|
61
61
|
"vite": "^7.3.1",
|
|
62
62
|
"vitest": "^4.0.17",
|
|
63
|
-
"@stonecrop/aform": "0.
|
|
63
|
+
"@stonecrop/aform": "0.8.0",
|
|
64
64
|
"stonecrop-rig": "0.7.0",
|
|
65
|
-
"@stonecrop/atable": "0.
|
|
65
|
+
"@stonecrop/atable": "0.8.0"
|
|
66
66
|
},
|
|
67
67
|
"publishConfig": {
|
|
68
68
|
"access": "public"
|
package/src/composable.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { HSTNode } from './stores/hst'
|
|
|
8
8
|
import { RouteContext } from './types/registry'
|
|
9
9
|
import { storeToRefs } from 'pinia'
|
|
10
10
|
import type { HSTOperation, OperationLogConfig, OperationLogSnapshot } from './types/operation-log'
|
|
11
|
+
import { SchemaTypes, DoctypeSchema } from '@stonecrop/aform'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Operation Log API - nested object containing all operation log functionality
|
|
@@ -64,6 +65,16 @@ export type HSTStonecropReturn = BaseStonecropReturn & {
|
|
|
64
65
|
handleHSTChange: (changeData: HSTChangeData) => void
|
|
65
66
|
hstStore: Ref<HSTNode | undefined>
|
|
66
67
|
formData: Ref<Record<string, any>>
|
|
68
|
+
resolvedSchema: Ref<SchemaTypes[]>
|
|
69
|
+
loadNestedData: (parentPath: string, childDoctype: DoctypeMeta, recordId?: string) => Record<string, any>
|
|
70
|
+
saveRecursive: (doctype: DoctypeMeta, recordId: string) => Promise<Record<string, any>>
|
|
71
|
+
createNestedContext: (
|
|
72
|
+
basePath: string,
|
|
73
|
+
childDoctype: DoctypeMeta
|
|
74
|
+
) => {
|
|
75
|
+
provideHSTPath: (fieldname: string) => string
|
|
76
|
+
handleHSTChange: (changeData: HSTChangeData) => void
|
|
77
|
+
}
|
|
67
78
|
}
|
|
68
79
|
|
|
69
80
|
/**
|
|
@@ -117,6 +128,19 @@ export function useStonecrop(options?: {
|
|
|
117
128
|
const routerDoctype = ref<DoctypeMeta | undefined>()
|
|
118
129
|
const routerRecordId = ref<string | undefined>()
|
|
119
130
|
|
|
131
|
+
// Resolved schema with nested Doctype fields expanded
|
|
132
|
+
const resolvedSchema = ref<SchemaTypes[]>([])
|
|
133
|
+
|
|
134
|
+
// Auto-resolve schema when doctype is available
|
|
135
|
+
if (options.doctype && registry) {
|
|
136
|
+
const schemaArray = options.doctype.schema
|
|
137
|
+
? Array.isArray(options.doctype.schema)
|
|
138
|
+
? options.doctype.schema
|
|
139
|
+
: Array.from(options.doctype.schema)
|
|
140
|
+
: []
|
|
141
|
+
resolvedSchema.value = registry.resolveSchema(schemaArray as SchemaTypes[])
|
|
142
|
+
}
|
|
143
|
+
|
|
120
144
|
// Operation log state and methods - will be populated after stonecrop instance is created
|
|
121
145
|
const operations = ref<HSTOperation[]>([])
|
|
122
146
|
const currentIndex = ref(-1)
|
|
@@ -254,6 +278,16 @@ export function useStonecrop(options?: {
|
|
|
254
278
|
routerRecordId.value = recordId
|
|
255
279
|
hstStore.value = stonecrop.value.getStore()
|
|
256
280
|
|
|
281
|
+
// Resolve schema for router-loaded doctype
|
|
282
|
+
if (registry) {
|
|
283
|
+
const schemaArray = doctype.schema
|
|
284
|
+
? Array.isArray(doctype.schema)
|
|
285
|
+
? doctype.schema
|
|
286
|
+
: Array.from(doctype.schema)
|
|
287
|
+
: []
|
|
288
|
+
resolvedSchema.value = registry.resolveSchema(schemaArray as SchemaTypes[])
|
|
289
|
+
}
|
|
290
|
+
|
|
257
291
|
if (recordId && recordId !== 'new') {
|
|
258
292
|
const existingRecord = stonecrop.value.getRecordById(doctype, recordId)
|
|
259
293
|
if (existingRecord) {
|
|
@@ -378,6 +412,103 @@ export function useStonecrop(options?: {
|
|
|
378
412
|
provide('hstChangeHandler', handleHSTChange)
|
|
379
413
|
}
|
|
380
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Load nested doctype data from API or initialize empty structure
|
|
417
|
+
* @param parentPath - The parent path (e.g., "customer.123.address")
|
|
418
|
+
* @param childDoctype - The child doctype metadata
|
|
419
|
+
* @param recordId - Optional record ID to load
|
|
420
|
+
* @returns Promise resolving to the loaded or initialized data
|
|
421
|
+
*/
|
|
422
|
+
const loadNestedData = (parentPath: string, childDoctype: DoctypeMeta, recordId?: string): Record<string, any> => {
|
|
423
|
+
if (!stonecrop.value) {
|
|
424
|
+
return initializeNewRecord(childDoctype)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// If recordId provided, try to load existing data
|
|
428
|
+
if (recordId) {
|
|
429
|
+
try {
|
|
430
|
+
// Check if data already exists in HST
|
|
431
|
+
const existingData = hstStore.value?.get(parentPath)
|
|
432
|
+
if (existingData && typeof existingData === 'object') {
|
|
433
|
+
return existingData as Record<string, any>
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// TODO: Add API fetch logic here if needed
|
|
437
|
+
// For now, initialize new record
|
|
438
|
+
return initializeNewRecord(childDoctype)
|
|
439
|
+
} catch {
|
|
440
|
+
return initializeNewRecord(childDoctype)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Initialize new record
|
|
445
|
+
return initializeNewRecord(childDoctype)
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Recursively save a record with all nested doctype fields
|
|
450
|
+
* @param doctype - The doctype metadata
|
|
451
|
+
* @param recordId - The record ID to save
|
|
452
|
+
* @returns Promise resolving to the complete save payload
|
|
453
|
+
*/
|
|
454
|
+
const saveRecursive = async (doctype: DoctypeMeta, recordId: string): Promise<Record<string, any>> => {
|
|
455
|
+
if (!hstStore.value || !stonecrop.value) {
|
|
456
|
+
throw new Error('HST store not initialized')
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const recordPath = `${doctype.slug}.${recordId}`
|
|
460
|
+
const recordData = hstStore.value.get(recordPath) || {}
|
|
461
|
+
|
|
462
|
+
// Build the save payload using resolved schema
|
|
463
|
+
const payload: Record<string, any> = { ...recordData }
|
|
464
|
+
|
|
465
|
+
// Use resolveSchema to get the full resolved tree, then walk Doctype fields
|
|
466
|
+
const schemaArray = (
|
|
467
|
+
doctype.schema ? (Array.isArray(doctype.schema) ? doctype.schema : Array.from(doctype.schema)) : []
|
|
468
|
+
) as SchemaTypes[]
|
|
469
|
+
const resolved = registry ? registry.resolveSchema(schemaArray) : schemaArray
|
|
470
|
+
const doctypeFields = resolved.filter(
|
|
471
|
+
field => 'fieldtype' in field && field.fieldtype === 'Doctype' && 'schema' in field && Array.isArray(field.schema)
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
// Recursively collect nested data from HST using resolved schemas
|
|
475
|
+
for (const field of doctypeFields) {
|
|
476
|
+
const doctypeField = field as DoctypeSchema
|
|
477
|
+
const fieldPath = `${recordPath}.${doctypeField.fieldname}`
|
|
478
|
+
const nestedData = collectNestedData(doctypeField.schema!, fieldPath, hstStore.value)
|
|
479
|
+
payload[doctypeField.fieldname] = nestedData
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return payload
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Create a nested context for child forms
|
|
487
|
+
* @param basePath - The base path for the nested context (e.g., "customer.123.address")
|
|
488
|
+
* @param _childDoctype - The child doctype metadata (unused but kept for API consistency)
|
|
489
|
+
* @returns Object with scoped provideHSTPath and handleHSTChange
|
|
490
|
+
*/
|
|
491
|
+
const createNestedContext = (basePath: string, _childDoctype: DoctypeMeta) => {
|
|
492
|
+
const nestedProvideHSTPath = (fieldname: string): string => {
|
|
493
|
+
return `${basePath}.${fieldname}`
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const nestedHandleHSTChange = (changeData: HSTChangeData): void => {
|
|
497
|
+
// Update the path to be relative to the nested base path
|
|
498
|
+
const nestedPath = changeData.path.startsWith(basePath) ? changeData.path : `${basePath}.${changeData.fieldname}`
|
|
499
|
+
|
|
500
|
+
handleHSTChange({
|
|
501
|
+
...changeData,
|
|
502
|
+
path: nestedPath,
|
|
503
|
+
})
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
return {
|
|
507
|
+
provideHSTPath: nestedProvideHSTPath,
|
|
508
|
+
handleHSTChange: nestedHandleHSTChange,
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
381
512
|
// Create operation log API object
|
|
382
513
|
const operationLog: OperationLogAPI = {
|
|
383
514
|
operations,
|
|
@@ -409,6 +540,10 @@ export function useStonecrop(options?: {
|
|
|
409
540
|
handleHSTChange,
|
|
410
541
|
hstStore,
|
|
411
542
|
formData,
|
|
543
|
+
resolvedSchema,
|
|
544
|
+
loadNestedData,
|
|
545
|
+
saveRecursive,
|
|
546
|
+
createNestedContext,
|
|
412
547
|
} as HSTStonecropReturn
|
|
413
548
|
} else if (!options.doctype && registry?.router) {
|
|
414
549
|
// Router-based - return HST (will be populated after mount)
|
|
@@ -419,6 +554,10 @@ export function useStonecrop(options?: {
|
|
|
419
554
|
handleHSTChange,
|
|
420
555
|
hstStore,
|
|
421
556
|
formData,
|
|
557
|
+
resolvedSchema,
|
|
558
|
+
loadNestedData,
|
|
559
|
+
saveRecursive,
|
|
560
|
+
createNestedContext,
|
|
422
561
|
} as HSTStonecropReturn
|
|
423
562
|
}
|
|
424
563
|
|
|
@@ -514,3 +653,30 @@ function updateNestedObject(obj: any, path: string[], value: any): void {
|
|
|
514
653
|
const finalKey = path[path.length - 1]
|
|
515
654
|
current[finalKey] = value
|
|
516
655
|
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Recursively collect nested data from HST using pre-resolved schemas
|
|
659
|
+
* @param resolvedSchema - The already-resolved schema (with nested schemas embedded)
|
|
660
|
+
* @param basePath - The base path in HST (e.g., "customer.123.address")
|
|
661
|
+
* @param hstStore - The HST store instance
|
|
662
|
+
* @returns The collected data object
|
|
663
|
+
*/
|
|
664
|
+
function collectNestedData(resolvedSchema: SchemaTypes[], basePath: string, hstStore: HSTNode): Record<string, any> {
|
|
665
|
+
const data = hstStore.get(basePath) || {}
|
|
666
|
+
const payload: Record<string, any> = { ...data }
|
|
667
|
+
|
|
668
|
+
// Find Doctype fields that have resolved child schemas
|
|
669
|
+
const doctypeFields = resolvedSchema.filter(
|
|
670
|
+
field => 'fieldtype' in field && field.fieldtype === 'Doctype' && 'schema' in field && Array.isArray(field.schema)
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
// Recursively collect nested data
|
|
674
|
+
for (const field of doctypeFields) {
|
|
675
|
+
const doctypeField = field as DoctypeSchema
|
|
676
|
+
const fieldPath = `${basePath}.${doctypeField.fieldname}`
|
|
677
|
+
const nestedData = collectNestedData(doctypeField.schema!, fieldPath, hstStore)
|
|
678
|
+
payload[doctypeField.fieldname] = nestedData
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
return payload
|
|
682
|
+
}
|
package/src/registry.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SchemaTypes } from '@stonecrop/aform'
|
|
1
2
|
import { Router } from 'vue-router'
|
|
2
3
|
|
|
3
4
|
import DoctypeMeta from './doctype'
|
|
@@ -83,6 +84,194 @@ export default class Registry {
|
|
|
83
84
|
}
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Resolve nested Doctype and Table fields in a schema by embedding child schemas inline.
|
|
89
|
+
*
|
|
90
|
+
* @remarks
|
|
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.
|
|
94
|
+
*
|
|
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]`.
|
|
99
|
+
*
|
|
100
|
+
* Returns a new array — does not mutate the original schema.
|
|
101
|
+
*
|
|
102
|
+
* @param schema - The schema array to resolve
|
|
103
|
+
* @returns A new schema array with nested Doctype fields resolved
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* registry.addDoctype(addressDoctype)
|
|
108
|
+
* registry.addDoctype(customerDoctype)
|
|
109
|
+
*
|
|
110
|
+
* // Before: customer schema has { fieldname: 'address', fieldtype: 'Doctype', options: 'address' }
|
|
111
|
+
* const resolved = registry.resolveSchema(customerSchema)
|
|
112
|
+
* // After: address field now has schema: [...address fields...]
|
|
113
|
+
* ```
|
|
114
|
+
*
|
|
115
|
+
* @public
|
|
116
|
+
*/
|
|
117
|
+
resolveSchema(schema: SchemaTypes[], visited?: Set<string>): SchemaTypes[] {
|
|
118
|
+
const seen = visited || new Set<string>()
|
|
119
|
+
|
|
120
|
+
return schema.map(field => {
|
|
121
|
+
// Check for Doctype fieldtype with a string options (slug reference)
|
|
122
|
+
if (
|
|
123
|
+
'fieldtype' in field &&
|
|
124
|
+
field.fieldtype === 'Doctype' &&
|
|
125
|
+
'options' in field &&
|
|
126
|
+
typeof field.options === 'string'
|
|
127
|
+
) {
|
|
128
|
+
const doctypeSlug = field.options
|
|
129
|
+
|
|
130
|
+
// Circular reference protection
|
|
131
|
+
if (seen.has(doctypeSlug)) {
|
|
132
|
+
return { ...field }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const doctype = this.registry[doctypeSlug]
|
|
136
|
+
if (doctype && doctype.schema) {
|
|
137
|
+
// Convert Immutable.List to plain array if needed
|
|
138
|
+
const childSchema: SchemaTypes[] = Array.isArray(doctype.schema) ? doctype.schema : Array.from(doctype.schema)
|
|
139
|
+
|
|
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
|
+
}
|
|
148
|
+
|
|
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 as string
|
|
157
|
+
|
|
158
|
+
// Circular reference protection
|
|
159
|
+
if (seen.has(doctypeSlug)) {
|
|
160
|
+
return { ...field }
|
|
161
|
+
}
|
|
162
|
+
|
|
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 }
|
|
167
|
+
|
|
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
|
+
}
|
|
180
|
+
|
|
181
|
+
// Set default component if not already specified
|
|
182
|
+
if (!resolved.component) {
|
|
183
|
+
resolved.component = 'ATable'
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Set default config if not already specified
|
|
187
|
+
if (!('config' in field) || !field.config) {
|
|
188
|
+
resolved.config = { view: 'list' }
|
|
189
|
+
}
|
|
190
|
+
|
|
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 = []
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return resolved as SchemaTypes
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { ...field }
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Initialize a new record with default values based on a schema.
|
|
207
|
+
*
|
|
208
|
+
* @remarks
|
|
209
|
+
* Creates a plain object with keys from the schema's fieldnames and default values
|
|
210
|
+
* derived from each field's `fieldtype`:
|
|
211
|
+
* - Data, Text → `''`
|
|
212
|
+
* - Check → `false`
|
|
213
|
+
* - Int, Float, Decimal, Currency, Quantity → `0`
|
|
214
|
+
* - Table → `[]`
|
|
215
|
+
* - JSON, Doctype → `{}`
|
|
216
|
+
* - All others → `null`
|
|
217
|
+
*
|
|
218
|
+
* For Doctype fields with a resolved `schema` array, recursively initializes the nested record.
|
|
219
|
+
*
|
|
220
|
+
* @param schema - The schema array to derive defaults from
|
|
221
|
+
* @returns A plain object with default values for each field
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```ts
|
|
225
|
+
* const defaults = registry.initializeRecord(addressSchema)
|
|
226
|
+
* // { street: '', city: '', state: '', zip_code: '' }
|
|
227
|
+
* ```
|
|
228
|
+
*
|
|
229
|
+
* @public
|
|
230
|
+
*/
|
|
231
|
+
initializeRecord(schema: SchemaTypes[]): Record<string, any> {
|
|
232
|
+
const record: Record<string, any> = {}
|
|
233
|
+
|
|
234
|
+
schema.forEach(field => {
|
|
235
|
+
const fieldtype = 'fieldtype' in field ? (field.fieldtype as string) : 'Data'
|
|
236
|
+
|
|
237
|
+
switch (fieldtype) {
|
|
238
|
+
case 'Data':
|
|
239
|
+
case 'Text':
|
|
240
|
+
case 'Code':
|
|
241
|
+
record[field.fieldname] = ''
|
|
242
|
+
break
|
|
243
|
+
case 'Check':
|
|
244
|
+
record[field.fieldname] = false
|
|
245
|
+
break
|
|
246
|
+
case 'Int':
|
|
247
|
+
case 'Float':
|
|
248
|
+
case 'Decimal':
|
|
249
|
+
case 'Currency':
|
|
250
|
+
case 'Quantity':
|
|
251
|
+
record[field.fieldname] = 0
|
|
252
|
+
break
|
|
253
|
+
case 'Table':
|
|
254
|
+
record[field.fieldname] = []
|
|
255
|
+
break
|
|
256
|
+
case 'JSON':
|
|
257
|
+
record[field.fieldname] = {}
|
|
258
|
+
break
|
|
259
|
+
case 'Doctype':
|
|
260
|
+
// If nested schema is resolved, recursively initialize
|
|
261
|
+
if ('schema' in field && Array.isArray(field.schema)) {
|
|
262
|
+
record[field.fieldname] = this.initializeRecord(field.schema)
|
|
263
|
+
} else {
|
|
264
|
+
record[field.fieldname] = {}
|
|
265
|
+
}
|
|
266
|
+
break
|
|
267
|
+
default:
|
|
268
|
+
record[field.fieldname] = null
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
return record
|
|
273
|
+
}
|
|
274
|
+
|
|
86
275
|
// TODO: should we allow clearing the registry at all?
|
|
87
276
|
// clear() {
|
|
88
277
|
// this.registry = {}
|
package/src/stonecrop.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reactive } from 'vue'
|
|
1
2
|
import DoctypeMeta from './doctype'
|
|
2
3
|
import Registry from './registry'
|
|
3
4
|
import { createHST, type HSTNode } from './stores/hst'
|
|
@@ -58,7 +59,9 @@ export class Stonecrop {
|
|
|
58
59
|
initialStoreStructure[doctypeSlug] = {}
|
|
59
60
|
})
|
|
60
61
|
|
|
61
|
-
|
|
62
|
+
// Wrap the store in Vue's reactive() for automatic change detection
|
|
63
|
+
// This enables Vue computed properties to track HST store changes
|
|
64
|
+
this.hstStore = createHST(reactive(initialStoreStructure), 'StonecropStore')
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
/**
|
|
@@ -235,10 +238,10 @@ export class Stonecrop {
|
|
|
235
238
|
*/
|
|
236
239
|
async getRecords(doctype: DoctypeMeta): Promise<void> {
|
|
237
240
|
const response = await fetch(`/${doctype.slug}`)
|
|
238
|
-
const records = await response.json()
|
|
241
|
+
const records = (await response.json()) as { id: string }[]
|
|
239
242
|
|
|
240
243
|
// Store each record in HST
|
|
241
|
-
records.forEach(
|
|
244
|
+
records.forEach(record => {
|
|
242
245
|
if (record.id) {
|
|
243
246
|
this.addRecord(doctype, record.id, record)
|
|
244
247
|
}
|
package/src/stores/hst.ts
CHANGED
|
@@ -138,6 +138,7 @@ interface PropertyAccessible {
|
|
|
138
138
|
|
|
139
139
|
// Extend global interfaces
|
|
140
140
|
declare global {
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
|
141
142
|
interface Window extends RegistryGlobal {}
|
|
142
143
|
const global: RegistryGlobal | undefined
|
|
143
144
|
}
|
|
@@ -291,7 +292,6 @@ class HSTProxy implements HSTNode {
|
|
|
291
292
|
const isDelete = value === undefined && beforeValue !== undefined
|
|
292
293
|
const operationType: 'set' | 'delete' = isDelete ? 'delete' : 'set'
|
|
293
294
|
|
|
294
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
295
295
|
logStore.addOperation(
|
|
296
296
|
{
|
|
297
297
|
type: operationType,
|
|
@@ -429,7 +429,6 @@ class HSTProxy implements HSTNode {
|
|
|
429
429
|
// Log FSM transition operation
|
|
430
430
|
const logStore = getOperationLogStore()
|
|
431
431
|
if (logStore && typeof logStore.addOperation === 'function') {
|
|
432
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
433
432
|
logStore.addOperation(
|
|
434
433
|
{
|
|
435
434
|
type: 'transition' as const,
|
|
@@ -678,9 +677,20 @@ class HSTProxy implements HSTNode {
|
|
|
678
677
|
)
|
|
679
678
|
}
|
|
680
679
|
|
|
680
|
+
/**
|
|
681
|
+
* Parse a path string into segments, handling both dot notation and array bracket notation
|
|
682
|
+
* @param path - The path string to parse (e.g., "order.456.line_items[0].product")
|
|
683
|
+
* @returns Array of path segments (e.g., ['order', '456', 'line_items', '0', 'product'])
|
|
684
|
+
*/
|
|
681
685
|
private parsePath(path: string): string[] {
|
|
682
686
|
if (!path) return []
|
|
683
|
-
|
|
687
|
+
|
|
688
|
+
// Replace array bracket notation with dot notation
|
|
689
|
+
// items[0] → items.0
|
|
690
|
+
// items[0][1] → items.0.1
|
|
691
|
+
const normalizedPath = path.replace(/\[(\d+)\]/g, '.$1')
|
|
692
|
+
|
|
693
|
+
return normalizedPath.split('.').filter(segment => segment.length > 0)
|
|
684
694
|
}
|
|
685
695
|
}
|
|
686
696
|
|