@stonecrop/stonecrop 0.4.37 → 0.5.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.
Files changed (78) hide show
  1. package/README.md +92 -3
  2. package/dist/src/composable.d.ts +74 -8
  3. package/dist/src/composable.d.ts.map +1 -1
  4. package/dist/src/composable.js +348 -0
  5. package/dist/src/composables/operation-log.d.ts +136 -0
  6. package/dist/src/composables/operation-log.d.ts.map +1 -0
  7. package/dist/src/composables/operation-log.js +221 -0
  8. package/dist/src/doctype.d.ts +9 -1
  9. package/dist/src/doctype.d.ts.map +1 -1
  10. package/dist/{doctype.js → src/doctype.js} +9 -3
  11. package/dist/src/field-triggers.d.ts +178 -0
  12. package/dist/src/field-triggers.d.ts.map +1 -0
  13. package/dist/src/field-triggers.js +564 -0
  14. package/dist/src/index.d.ts +12 -4
  15. package/dist/src/index.d.ts.map +1 -1
  16. package/dist/src/index.js +18 -0
  17. package/dist/src/plugins/index.d.ts +11 -13
  18. package/dist/src/plugins/index.d.ts.map +1 -1
  19. package/dist/src/plugins/index.js +90 -0
  20. package/dist/src/registry.d.ts +9 -3
  21. package/dist/src/registry.d.ts.map +1 -1
  22. package/dist/{registry.js → src/registry.js} +14 -1
  23. package/dist/src/stonecrop.d.ts +350 -114
  24. package/dist/src/stonecrop.d.ts.map +1 -1
  25. package/dist/src/stonecrop.js +251 -0
  26. package/dist/src/stores/hst.d.ts +157 -0
  27. package/dist/src/stores/hst.d.ts.map +1 -0
  28. package/dist/src/stores/hst.js +483 -0
  29. package/dist/src/stores/index.d.ts +5 -1
  30. package/dist/src/stores/index.d.ts.map +1 -1
  31. package/dist/{stores → src/stores}/index.js +4 -1
  32. package/dist/src/stores/operation-log.d.ts +268 -0
  33. package/dist/src/stores/operation-log.d.ts.map +1 -0
  34. package/dist/src/stores/operation-log.js +571 -0
  35. package/dist/src/types/field-triggers.d.ts +186 -0
  36. package/dist/src/types/field-triggers.d.ts.map +1 -0
  37. package/dist/src/types/field-triggers.js +4 -0
  38. package/dist/src/types/index.d.ts +13 -2
  39. package/dist/src/types/index.d.ts.map +1 -1
  40. package/dist/src/types/index.js +4 -0
  41. package/dist/src/types/operation-log.d.ts +165 -0
  42. package/dist/src/types/operation-log.d.ts.map +1 -0
  43. package/dist/src/types/registry.d.ts +11 -0
  44. package/dist/src/types/registry.d.ts.map +1 -0
  45. package/dist/src/types/registry.js +0 -0
  46. package/dist/stonecrop.d.ts +1555 -159
  47. package/dist/stonecrop.js +1974 -7028
  48. package/dist/stonecrop.js.map +1 -1
  49. package/dist/stonecrop.umd.cjs +4 -8
  50. package/dist/stonecrop.umd.cjs.map +1 -1
  51. package/dist/tests/setup.d.ts +5 -0
  52. package/dist/tests/setup.d.ts.map +1 -0
  53. package/dist/tests/setup.js +15 -0
  54. package/package.json +5 -4
  55. package/src/composable.ts +481 -31
  56. package/src/composables/operation-log.ts +254 -0
  57. package/src/doctype.ts +9 -3
  58. package/src/field-triggers.ts +671 -0
  59. package/src/index.ts +50 -4
  60. package/src/plugins/index.ts +70 -22
  61. package/src/registry.ts +18 -3
  62. package/src/stonecrop.ts +246 -155
  63. package/src/stores/hst.ts +703 -0
  64. package/src/stores/index.ts +6 -1
  65. package/src/stores/operation-log.ts +671 -0
  66. package/src/types/field-triggers.ts +201 -0
  67. package/src/types/index.ts +17 -6
  68. package/src/types/operation-log.ts +205 -0
  69. package/src/types/registry.ts +10 -0
  70. package/dist/composable.js +0 -50
  71. package/dist/index.js +0 -6
  72. package/dist/plugins/index.js +0 -49
  73. package/dist/src/stores/data.d.ts +0 -11
  74. package/dist/src/stores/data.d.ts.map +0 -1
  75. package/dist/stores/data.js +0 -7
  76. package/src/stores/data.ts +0 -8
  77. /package/dist/{exceptions.js → src/exceptions.js} +0 -0
  78. /package/dist/{types/index.js → src/types/operation-log.js} +0 -0
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Vitest setup file
3
+ * Runs before all test files
4
+ */
5
+ //# sourceMappingURL=setup.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"","sources":["../../tests/setup.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Vitest setup file
3
+ * Runs before all test files
4
+ */
5
+ // Remove Node.js's native BroadcastChannel to avoid conflicts with mocks
6
+ // Tests that need BroadcastChannel will mock it themselves
7
+ if (typeof global !== 'undefined' && 'BroadcastChannel' in global) {
8
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
9
+ delete global.BroadcastChannel;
10
+ }
11
+ // Also remove from globalThis if it exists there
12
+ if (typeof globalThis !== 'undefined' && 'BroadcastChannel' in globalThis) {
13
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
14
+ delete globalThis.BroadcastChannel;
15
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stonecrop/stonecrop",
3
- "version": "0.4.37",
3
+ "version": "0.5.0",
4
4
  "description": "schema helper",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,6 +35,7 @@
35
35
  "src/*"
36
36
  ],
37
37
  "dependencies": {
38
+ "@vueuse/core": "^14.0.0",
38
39
  "immutable": "^5.1.4",
39
40
  "pinia-shared-state": "^1.0.1",
40
41
  "pinia-xstate": "^3.0.0",
@@ -59,9 +60,9 @@
59
60
  "typescript-eslint": "^8.46.2",
60
61
  "vite": "^7.1.1",
61
62
  "vitest": "^4.0.5",
62
- "@stonecrop/atable": "0.4.37",
63
- "stonecrop-rig": "0.2.22",
64
- "@stonecrop/aform": "0.4.37"
63
+ "@stonecrop/aform": "0.5.0",
64
+ "@stonecrop/atable": "0.5.0",
65
+ "stonecrop-rig": "0.2.22"
65
66
  },
66
67
  "publishConfig": {
67
68
  "access": "public"
package/src/composable.ts CHANGED
@@ -1,66 +1,516 @@
1
- import { inject, onMounted, Ref, ref } from 'vue'
1
+ // src/composable.ts
2
+ import { inject, onMounted, Ref, ref, watch, provide, computed, ComputedRef } from 'vue'
2
3
 
3
4
  import Registry from './registry'
4
5
  import { Stonecrop } from './stonecrop'
5
- import { useDataStore } from './stores/data'
6
+ import DoctypeMeta from './doctype'
7
+ import type { HSTNode } from './stores/hst'
8
+ import { RouteContext } from './types/registry'
9
+ import { storeToRefs } from 'pinia'
10
+ import type { HSTOperation, OperationLogConfig, OperationLogSnapshot } from './types/operation-log'
6
11
 
7
12
  /**
8
- * Stonecrop composable return type
13
+ * Operation Log API - nested object containing all operation log functionality
9
14
  * @public
10
15
  */
11
- export type StonecropReturn = {
16
+ export type OperationLogAPI = {
17
+ operations: Ref<HSTOperation[]>
18
+ currentIndex: Ref<number>
19
+ undoRedoState: ComputedRef<{
20
+ canUndo: boolean
21
+ canRedo: boolean
22
+ undoCount: number
23
+ redoCount: number
24
+ currentIndex: number
25
+ }>
26
+ canUndo: ComputedRef<boolean>
27
+ canRedo: ComputedRef<boolean>
28
+ undoCount: ComputedRef<number>
29
+ redoCount: ComputedRef<number>
30
+ undo: (hstStore: HSTNode) => boolean
31
+ redo: (hstStore: HSTNode) => boolean
32
+ startBatch: () => void
33
+ commitBatch: (description?: string) => string | null
34
+ cancelBatch: () => void
35
+ clear: () => void
36
+ getOperationsFor: (doctype: string, recordId?: string) => HSTOperation[]
37
+ getSnapshot: () => OperationLogSnapshot
38
+ markIrreversible: (operationId: string, reason: string) => void
39
+ logAction: (
40
+ doctype: string,
41
+ actionName: string,
42
+ recordIds?: string[],
43
+ result?: 'success' | 'failure' | 'pending',
44
+ error?: string
45
+ ) => string
46
+ configure: (options: Partial<OperationLogConfig>) => void
47
+ }
48
+
49
+ /**
50
+ * Base Stonecrop composable return type - includes operation log functionality
51
+ * @public
52
+ */
53
+ export type BaseStonecropReturn = {
12
54
  stonecrop: Ref<Stonecrop | undefined>
55
+ operationLog: OperationLogAPI
56
+ }
57
+
58
+ /**
59
+ * HST-enabled Stonecrop composable return type
60
+ * @public
61
+ */
62
+ export type HSTStonecropReturn = BaseStonecropReturn & {
63
+ provideHSTPath: (fieldname: string, recordId?: string) => string
64
+ handleHSTChange: (changeData: HSTChangeData) => void
65
+ hstStore: Ref<HSTNode | undefined>
66
+ formData: Ref<Record<string, any>>
67
+ }
68
+
69
+ /**
70
+ * HST Change data structure
71
+ * @public
72
+ */
73
+ export type HSTChangeData = {
74
+ path: string
75
+ value: any
76
+ fieldname: string
77
+ recordId?: string
13
78
  }
14
79
 
15
80
  /**
16
- * Stonecrop composable
17
- * @param registry - An existing Stonecrop Registry instance
18
- * @returns The Stonecrop instance and a boolean indicating if Stonecrop is setup and ready
19
- * @throws Error if the Stonecrop plugin is not enabled before using the composable
81
+ * Unified Stonecrop composable - handles both general operations and HST reactive integration
82
+ *
83
+ * @param options - Configuration options for the composable
84
+ * @returns Stonecrop instance and optional HST integration utilities
20
85
  * @public
21
86
  */
22
- export function useStonecrop(registry?: Registry): StonecropReturn {
87
+ export function useStonecrop(): BaseStonecropReturn | HSTStonecropReturn
88
+ /**
89
+ * Unified Stonecrop composable with HST integration for a specific doctype and record
90
+ *
91
+ * @param options - Configuration with doctype and optional recordId
92
+ * @returns Stonecrop instance with full HST integration utilities
93
+ * @public
94
+ */
95
+ export function useStonecrop(options: {
96
+ registry?: Registry
97
+ doctype: DoctypeMeta
98
+ recordId?: string
99
+ }): HSTStonecropReturn
100
+ /**
101
+ * @public
102
+ */
103
+ export function useStonecrop(options?: {
104
+ registry?: Registry
105
+ doctype?: DoctypeMeta
106
+ recordId?: string
107
+ }): BaseStonecropReturn | HSTStonecropReturn {
108
+ if (!options) options = {}
109
+
110
+ const registry = options.registry || inject<Registry>('$registry')
111
+ const providedStonecrop = inject<Stonecrop>('$stonecrop')
23
112
  const stonecrop = ref<Stonecrop>()
113
+ const hstStore = ref<HSTNode>()
114
+ const formData = ref<Record<string, any>>({})
115
+
116
+ // Use refs for router-loaded doctype to maintain reactivity
117
+ const routerDoctype = ref<DoctypeMeta | undefined>()
118
+ const routerRecordId = ref<string | undefined>()
119
+
120
+ // Operation log state and methods - will be populated after stonecrop instance is created
121
+ const operations = ref<HSTOperation[]>([])
122
+ const currentIndex = ref(-1)
123
+ const canUndo = computed(() => stonecrop.value?.getOperationLogStore().canUndo ?? false)
124
+ const canRedo = computed(() => stonecrop.value?.getOperationLogStore().canRedo ?? false)
125
+ const undoCount = computed(() => stonecrop.value?.getOperationLogStore().undoCount ?? 0)
126
+ const redoCount = computed(() => stonecrop.value?.getOperationLogStore().redoCount ?? 0)
127
+ const undoRedoState = computed(
128
+ () =>
129
+ stonecrop.value?.getOperationLogStore().undoRedoState ?? {
130
+ canUndo: false,
131
+ canRedo: false,
132
+ undoCount: 0,
133
+ redoCount: 0,
134
+ currentIndex: -1,
135
+ }
136
+ )
137
+
138
+ // Operation log methods
139
+ const undo = (hstStore: HSTNode): boolean => {
140
+ return stonecrop.value?.getOperationLogStore().undo(hstStore) ?? false
141
+ }
24
142
 
143
+ const redo = (hstStore: HSTNode): boolean => {
144
+ return stonecrop.value?.getOperationLogStore().redo(hstStore) ?? false
145
+ }
146
+
147
+ const startBatch = () => {
148
+ stonecrop.value?.getOperationLogStore().startBatch()
149
+ }
150
+
151
+ const commitBatch = (description?: string): string | null => {
152
+ return stonecrop.value?.getOperationLogStore().commitBatch(description) ?? null
153
+ }
154
+
155
+ const cancelBatch = () => {
156
+ stonecrop.value?.getOperationLogStore().cancelBatch()
157
+ }
158
+
159
+ const clear = () => {
160
+ stonecrop.value?.getOperationLogStore().clear()
161
+ }
162
+
163
+ const getOperationsFor = (doctype: string, recordId?: string) => {
164
+ return stonecrop.value?.getOperationLogStore().getOperationsFor(doctype, recordId) ?? []
165
+ }
166
+
167
+ const getSnapshot = () => {
168
+ return (
169
+ stonecrop.value?.getOperationLogStore().getSnapshot() ?? {
170
+ operations: [],
171
+ currentIndex: -1,
172
+ totalOperations: 0,
173
+ reversibleOperations: 0,
174
+ irreversibleOperations: 0,
175
+ }
176
+ )
177
+ }
178
+
179
+ const markIrreversible = (operationId: string, reason: string) => {
180
+ stonecrop.value?.getOperationLogStore().markIrreversible(operationId, reason)
181
+ }
182
+
183
+ const logAction = (
184
+ doctype: string,
185
+ actionName: string,
186
+ recordIds?: string[],
187
+ result: 'success' | 'failure' | 'pending' = 'success',
188
+ error?: string
189
+ ): string => {
190
+ return stonecrop.value?.getOperationLogStore().logAction(doctype, actionName, recordIds, result, error) ?? ''
191
+ }
192
+
193
+ const configure = (config: Partial<OperationLogConfig>) => {
194
+ stonecrop.value?.getOperationLogStore().configure(config)
195
+ }
196
+
197
+ // Initialize Stonecrop instance
25
198
  onMounted(async () => {
26
199
  if (!registry) {
27
- registry = inject<Registry>('$registry')
200
+ return
28
201
  }
29
202
 
30
- if (!registry || !registry.router) return
203
+ stonecrop.value = providedStonecrop || new Stonecrop(registry)
31
204
 
32
- let store: ReturnType<typeof useDataStore>
205
+ // Set up reactive refs from operation log store - only if Pinia is available
33
206
  try {
34
- store = useDataStore()
207
+ const opLogStore = stonecrop.value.getOperationLogStore()
208
+ const opLogRefs = storeToRefs(opLogStore)
209
+ operations.value = opLogRefs.operations.value
210
+ currentIndex.value = opLogRefs.currentIndex.value
211
+
212
+ // Watch for changes in operation log state
213
+ watch(
214
+ () => opLogRefs.operations.value,
215
+ newOps => {
216
+ operations.value = newOps
217
+ }
218
+ )
219
+ watch(
220
+ () => opLogRefs.currentIndex.value,
221
+ newIndex => {
222
+ currentIndex.value = newIndex
223
+ }
224
+ )
35
225
  } catch {
36
- throw new Error('Please enable the Stonecrop plugin before using the Stonecrop composable')
226
+ // Pinia not available (e.g., in tests) - operation log features will not be available
227
+ // Silently fail - operation log is optional
37
228
  }
38
229
 
39
- stonecrop.value = new Stonecrop(registry, store)
40
- const route = registry.router.currentRoute.value
41
- const doctypeSlug = route.params.records?.toString().toLowerCase()
42
- const recordId = route.params.record?.toString().toLowerCase() // TODO: handle views other than list and form views?
43
- if (!doctypeSlug && !recordId) {
44
- return
230
+ // Handle router-based setup if no specific doctype provided
231
+ if (!options.doctype && registry.router) {
232
+ const route = registry.router.currentRoute.value
233
+
234
+ // Parse route path - let the application determine the doctype from the route
235
+ if (!route.path) return // Early return if no path available
236
+
237
+ const pathSegments = route.path.split('/').filter(segment => segment.length > 0)
238
+ const recordId = pathSegments[1]?.toLowerCase()
239
+
240
+ if (pathSegments.length > 0) {
241
+ // Create route context for getMeta function
242
+ const routeContext: RouteContext = {
243
+ path: route.path,
244
+ segments: pathSegments,
245
+ }
246
+
247
+ const doctype = await registry.getMeta?.(routeContext)
248
+ if (doctype) {
249
+ registry.addDoctype(doctype)
250
+ stonecrop.value.setup(doctype)
251
+
252
+ // Set reactive refs for router-based doctype
253
+ routerDoctype.value = doctype
254
+ routerRecordId.value = recordId
255
+ hstStore.value = stonecrop.value.getStore()
256
+
257
+ if (recordId && recordId !== 'new') {
258
+ const existingRecord = stonecrop.value.getRecordById(doctype, recordId)
259
+ if (existingRecord) {
260
+ formData.value = existingRecord.get('') || {}
261
+ } else {
262
+ try {
263
+ await stonecrop.value.getRecord(doctype, recordId)
264
+ const loadedRecord = stonecrop.value.getRecordById(doctype, recordId)
265
+ if (loadedRecord) {
266
+ formData.value = loadedRecord.get('') || {}
267
+ }
268
+ } catch {
269
+ formData.value = initializeNewRecord(doctype)
270
+ }
271
+ }
272
+ } else {
273
+ formData.value = initializeNewRecord(doctype)
274
+ }
275
+
276
+ if (hstStore.value) {
277
+ setupDeepReactivity(doctype, recordId || 'new', formData, hstStore.value)
278
+ }
279
+
280
+ stonecrop.value.runAction(doctype, 'load', recordId ? [recordId] : undefined)
281
+ }
282
+ }
45
283
  }
46
284
 
47
- // setup doctype via registry
48
- const doctype = await registry.getMeta?.(doctypeSlug)
49
- if (doctype) {
50
- registry.addDoctype(doctype)
51
- stonecrop.value.setup(doctype)
285
+ // Handle HST integration if doctype is provided explicitly
286
+ if (options.doctype) {
287
+ hstStore.value = stonecrop.value.getStore()
288
+ const doctype = options.doctype
289
+ const recordId = options.recordId
52
290
 
53
- if (doctypeSlug) {
54
- if (recordId) {
55
- await stonecrop.value.getRecord(doctype, recordId)
291
+ if (recordId && recordId !== 'new') {
292
+ const existingRecord = stonecrop.value.getRecordById(doctype, recordId)
293
+ if (existingRecord) {
294
+ formData.value = existingRecord.get('') || {}
56
295
  } else {
57
- await stonecrop.value.getRecords(doctype)
296
+ try {
297
+ await stonecrop.value.getRecord(doctype, recordId)
298
+ const loadedRecord = stonecrop.value.getRecordById(doctype, recordId)
299
+ if (loadedRecord) {
300
+ formData.value = loadedRecord.get('') || {}
301
+ }
302
+ } catch {
303
+ formData.value = initializeNewRecord(doctype)
304
+ }
58
305
  }
306
+ } else {
307
+ formData.value = initializeNewRecord(doctype)
59
308
  }
60
309
 
61
- stonecrop.value.runAction(doctype, 'load', recordId ? [recordId] : undefined)
310
+ if (hstStore.value) {
311
+ setupDeepReactivity(doctype, recordId || 'new', formData, hstStore.value)
312
+ }
62
313
  }
63
314
  })
64
315
 
65
- return { stonecrop }
316
+ // HST integration functions - always created but only populated when HST is available
317
+ const provideHSTPath = (fieldname: string, customRecordId?: string): string => {
318
+ const doctype = options.doctype || routerDoctype.value
319
+ if (!doctype) return ''
320
+
321
+ const actualRecordId = customRecordId || options.recordId || routerRecordId.value || 'new'
322
+ return `${doctype.slug}.${actualRecordId}.${fieldname}`
323
+ }
324
+
325
+ const handleHSTChange = (changeData: HSTChangeData): void => {
326
+ const doctype = options.doctype || routerDoctype.value
327
+ if (!hstStore.value || !stonecrop.value || !doctype) {
328
+ return
329
+ }
330
+
331
+ try {
332
+ const pathParts = changeData.path.split('.')
333
+ if (pathParts.length >= 2) {
334
+ const doctypeSlug = pathParts[0]
335
+ const recordId = pathParts[1]
336
+
337
+ if (!hstStore.value.has(`${doctypeSlug}.${recordId}`)) {
338
+ stonecrop.value.addRecord(doctype, recordId, { ...formData.value })
339
+ }
340
+
341
+ if (pathParts.length > 3) {
342
+ const recordPath = `${doctypeSlug}.${recordId}`
343
+ const nestedParts = pathParts.slice(2)
344
+
345
+ let currentPath = recordPath
346
+ for (let i = 0; i < nestedParts.length - 1; i++) {
347
+ currentPath += `.${nestedParts[i]}`
348
+
349
+ if (!hstStore.value.has(currentPath)) {
350
+ const nextPart = nestedParts[i + 1]
351
+ const isArray = !isNaN(Number(nextPart))
352
+ hstStore.value.set(currentPath, isArray ? [] : {})
353
+ }
354
+ }
355
+ }
356
+ }
357
+
358
+ hstStore.value.set(changeData.path, changeData.value)
359
+
360
+ const fieldParts = changeData.fieldname.split('.')
361
+ const newFormData = { ...formData.value }
362
+
363
+ if (fieldParts.length === 1) {
364
+ newFormData[fieldParts[0]] = changeData.value
365
+ } else {
366
+ updateNestedObject(newFormData, fieldParts, changeData.value)
367
+ }
368
+
369
+ formData.value = newFormData
370
+ } catch {
371
+ // Silently handle errors
372
+ }
373
+ }
374
+
375
+ // Provide injection tokens if HST will be available
376
+ if (options.doctype || registry?.router) {
377
+ provide('hstPathProvider', provideHSTPath)
378
+ provide('hstChangeHandler', handleHSTChange)
379
+ }
380
+
381
+ // Create operation log API object
382
+ const operationLog: OperationLogAPI = {
383
+ operations,
384
+ currentIndex,
385
+ undoRedoState,
386
+ canUndo,
387
+ canRedo,
388
+ undoCount,
389
+ redoCount,
390
+ undo,
391
+ redo,
392
+ startBatch,
393
+ commitBatch,
394
+ cancelBatch,
395
+ clear,
396
+ getOperationsFor,
397
+ getSnapshot,
398
+ markIrreversible,
399
+ logAction,
400
+ configure,
401
+ }
402
+ // Always return HST functions if doctype is provided or will be loaded from router
403
+ if (options.doctype) {
404
+ // Explicit doctype - return HST immediately
405
+ return {
406
+ stonecrop,
407
+ operationLog,
408
+ provideHSTPath,
409
+ handleHSTChange,
410
+ hstStore,
411
+ formData,
412
+ } as HSTStonecropReturn
413
+ } else if (!options.doctype && registry?.router) {
414
+ // Router-based - return HST (will be populated after mount)
415
+ return {
416
+ stonecrop,
417
+ operationLog,
418
+ provideHSTPath,
419
+ handleHSTChange,
420
+ hstStore,
421
+ formData,
422
+ } as HSTStonecropReturn
423
+ }
424
+
425
+ // No doctype and no router - basic mode
426
+ return {
427
+ stonecrop,
428
+ operationLog,
429
+ } as BaseStonecropReturn
430
+ }
431
+
432
+ /**
433
+ * Initialize new record structure based on doctype schema
434
+ */
435
+ function initializeNewRecord(doctype: DoctypeMeta): Record<string, any> {
436
+ const initialData: Record<string, any> = {}
437
+
438
+ if (!doctype.schema) {
439
+ return initialData
440
+ }
441
+
442
+ doctype.schema.forEach(field => {
443
+ const fieldtype = 'fieldtype' in field ? field.fieldtype : 'Data'
444
+
445
+ switch (fieldtype) {
446
+ case 'Data':
447
+ case 'Text':
448
+ initialData[field.fieldname] = ''
449
+ break
450
+ case 'Check':
451
+ initialData[field.fieldname] = false
452
+ break
453
+ case 'Int':
454
+ case 'Float':
455
+ initialData[field.fieldname] = 0
456
+ break
457
+ case 'Table':
458
+ initialData[field.fieldname] = []
459
+ break
460
+ case 'JSON':
461
+ initialData[field.fieldname] = {}
462
+ break
463
+ default:
464
+ initialData[field.fieldname] = null
465
+ }
466
+ })
467
+
468
+ return initialData
469
+ }
470
+
471
+ /**
472
+ * Setup deep reactivity between form data and HST store
473
+ */
474
+ function setupDeepReactivity(
475
+ doctype: DoctypeMeta,
476
+ recordId: string,
477
+ formData: Ref<Record<string, any>>,
478
+ hstStore: HSTNode
479
+ ): void {
480
+ watch(
481
+ formData,
482
+ newData => {
483
+ const recordPath = `${doctype.slug}.${recordId}`
484
+
485
+ Object.keys(newData).forEach(fieldname => {
486
+ const path = `${recordPath}.${fieldname}`
487
+ try {
488
+ hstStore.set(path, newData[fieldname])
489
+ } catch {
490
+ // Silently handle errors
491
+ }
492
+ })
493
+ },
494
+ { deep: true }
495
+ )
496
+ }
497
+
498
+ /**
499
+ * Update nested object with dot-notation path
500
+ */
501
+ function updateNestedObject(obj: any, path: string[], value: any): void {
502
+ let current = obj as Record<string, any>
503
+
504
+ for (let i = 0; i < path.length - 1; i++) {
505
+ const key = path[i]
506
+
507
+ if (!(key in current) || typeof current[key] !== 'object') {
508
+ current[key] = isNaN(Number(path[i + 1])) ? {} : []
509
+ }
510
+
511
+ current = current[key] as Record<string, any>
512
+ }
513
+
514
+ const finalKey = path[path.length - 1]
515
+ current[finalKey] = value
66
516
  }