@stonecrop/stonecrop 0.10.15 → 0.11.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 (117) hide show
  1. package/README.md +72 -29
  2. package/dist/composable.js +1 -0
  3. package/dist/composables/lazy-link.js +125 -0
  4. package/dist/composables/stonecrop.js +123 -68
  5. package/dist/composables/use-lazy-link-state.js +125 -0
  6. package/dist/composables/use-stonecrop.js +476 -0
  7. package/dist/doctype.js +10 -2
  8. package/dist/field-triggers.js +15 -3
  9. package/dist/index.js +4 -3
  10. package/dist/operation-log-DB-dGNT9.js +593 -0
  11. package/dist/operation-log-DB-dGNT9.js.map +1 -0
  12. package/dist/registry.js +261 -101
  13. package/dist/schema-validator.js +105 -1
  14. package/dist/src/composable.d.ts +11 -0
  15. package/dist/src/composable.d.ts.map +1 -0
  16. package/dist/src/composable.js +477 -0
  17. package/dist/src/composables/lazy-link.d.ts +25 -0
  18. package/dist/src/composables/lazy-link.d.ts.map +1 -0
  19. package/dist/src/composables/operation-log.d.ts +5 -5
  20. package/dist/src/composables/operation-log.d.ts.map +1 -1
  21. package/dist/src/composables/operation-log.js +224 -0
  22. package/dist/src/composables/stonecrop.d.ts +11 -1
  23. package/dist/src/composables/stonecrop.d.ts.map +1 -1
  24. package/dist/src/composables/stonecrop.js +574 -0
  25. package/dist/src/composables/use-lazy-link-state.d.ts +25 -0
  26. package/dist/src/composables/use-lazy-link-state.d.ts.map +1 -0
  27. package/dist/src/composables/use-stonecrop.d.ts +93 -0
  28. package/dist/src/composables/use-stonecrop.d.ts.map +1 -0
  29. package/dist/src/composables/useNestedSchema.d.ts +110 -0
  30. package/dist/src/composables/useNestedSchema.d.ts.map +1 -0
  31. package/dist/src/composables/useNestedSchema.js +155 -0
  32. package/dist/src/doctype.d.ts +9 -1
  33. package/dist/src/doctype.d.ts.map +1 -1
  34. package/dist/src/doctype.js +234 -0
  35. package/dist/src/exceptions.js +16 -0
  36. package/dist/src/field-triggers.d.ts +6 -0
  37. package/dist/src/field-triggers.d.ts.map +1 -1
  38. package/dist/src/field-triggers.js +567 -0
  39. package/dist/src/index.d.ts +3 -2
  40. package/dist/src/index.d.ts.map +1 -1
  41. package/dist/src/index.js +23 -0
  42. package/dist/src/plugins/index.js +96 -0
  43. package/dist/src/registry.d.ts +102 -23
  44. package/dist/src/registry.d.ts.map +1 -1
  45. package/dist/src/registry.js +246 -0
  46. package/dist/src/schema-validator.d.ts +8 -1
  47. package/dist/src/schema-validator.d.ts.map +1 -1
  48. package/dist/src/schema-validator.js +315 -0
  49. package/dist/src/stonecrop.d.ts +73 -28
  50. package/dist/src/stonecrop.d.ts.map +1 -1
  51. package/dist/src/stonecrop.js +339 -0
  52. package/dist/src/stores/data.d.ts +11 -0
  53. package/dist/src/stores/data.d.ts.map +1 -0
  54. package/dist/src/stores/hst.d.ts +5 -75
  55. package/dist/src/stores/hst.d.ts.map +1 -1
  56. package/dist/src/stores/hst.js +495 -0
  57. package/dist/src/stores/index.js +12 -0
  58. package/dist/src/stores/operation-log.d.ts +14 -14
  59. package/dist/src/stores/operation-log.d.ts.map +1 -1
  60. package/dist/src/stores/operation-log.js +568 -0
  61. package/dist/src/stores/xstate.d.ts +31 -0
  62. package/dist/src/stores/xstate.d.ts.map +1 -0
  63. package/dist/src/tsdoc-metadata.json +11 -0
  64. package/dist/src/types/composable.d.ts +50 -12
  65. package/dist/src/types/composable.d.ts.map +1 -1
  66. package/dist/src/types/doctype.d.ts +6 -7
  67. package/dist/src/types/doctype.d.ts.map +1 -1
  68. package/dist/src/types/field-triggers.d.ts +1 -1
  69. package/dist/src/types/field-triggers.d.ts.map +1 -1
  70. package/dist/src/types/field-triggers.js +4 -0
  71. package/dist/src/types/hst.d.ts +70 -0
  72. package/dist/src/types/hst.d.ts.map +1 -0
  73. package/dist/src/types/index.d.ts +1 -0
  74. package/dist/src/types/index.d.ts.map +1 -1
  75. package/dist/src/types/index.js +4 -0
  76. package/dist/src/types/operation-log.d.ts +4 -4
  77. package/dist/src/types/operation-log.d.ts.map +1 -1
  78. package/dist/src/types/operation-log.js +0 -0
  79. package/dist/src/types/registry.js +0 -0
  80. package/dist/src/types/schema-validator.d.ts +2 -0
  81. package/dist/src/types/schema-validator.d.ts.map +1 -1
  82. package/dist/src/utils.d.ts +24 -0
  83. package/dist/src/utils.d.ts.map +1 -0
  84. package/dist/stonecrop.d.ts +317 -99
  85. package/dist/stonecrop.js +2191 -1897
  86. package/dist/stonecrop.js.map +1 -1
  87. package/dist/stonecrop.umd.cjs +6 -0
  88. package/dist/stonecrop.umd.cjs.map +1 -0
  89. package/dist/stores/data.js +7 -0
  90. package/dist/stores/hst.js +27 -25
  91. package/dist/stores/operation-log.js +59 -47
  92. package/dist/stores/xstate.js +29 -0
  93. package/dist/tests/setup.d.ts +5 -0
  94. package/dist/tests/setup.d.ts.map +1 -0
  95. package/dist/tests/setup.js +15 -0
  96. package/dist/types/hst.js +0 -0
  97. package/dist/types/index.js +1 -0
  98. package/dist/utils.js +46 -0
  99. package/package.json +4 -4
  100. package/src/composables/lazy-link.ts +146 -0
  101. package/src/composables/operation-log.ts +1 -1
  102. package/src/composables/stonecrop.ts +142 -73
  103. package/src/doctype.ts +13 -4
  104. package/src/field-triggers.ts +18 -4
  105. package/src/index.ts +4 -2
  106. package/src/registry.ts +289 -111
  107. package/src/schema-validator.ts +120 -1
  108. package/src/stonecrop.ts +230 -106
  109. package/src/stores/hst.ts +29 -104
  110. package/src/stores/operation-log.ts +64 -50
  111. package/src/types/composable.ts +55 -12
  112. package/src/types/doctype.ts +6 -7
  113. package/src/types/field-triggers.ts +1 -1
  114. package/src/types/hst.ts +77 -0
  115. package/src/types/index.ts +1 -0
  116. package/src/types/operation-log.ts +4 -4
  117. package/src/types/schema-validator.ts +2 -0
@@ -5,7 +5,7 @@ import { inject, onMounted, Ref, ref, watch, provide, computed } from 'vue'
5
5
  import Doctype from '../doctype'
6
6
  import Registry from '../registry'
7
7
  import { Stonecrop } from '../stonecrop'
8
- import type { HSTNode } from '../stores/hst'
8
+ import type { HSTNode } from '../types/hst'
9
9
  import type { BaseStonecropReturn, HSTStonecropReturn, HSTChangeData, OperationLogAPI } from '../types/composable'
10
10
  import type { HSTOperation, OperationLogConfig } from '../types/operation-log'
11
11
  import type { RouteContext } from '../types/registry'
@@ -19,7 +19,17 @@ import type { RouteContext } from '../types/registry'
19
19
  */
20
20
  export function useStonecrop(): BaseStonecropReturn | HSTStonecropReturn
21
21
  /**
22
- * Unified Stonecrop composable with HST integration for a specific doctype and record
22
+ * Unified Stonecrop composable with HST integration for a specific doctype and record.
23
+ *
24
+ * When a `Doctype` instance is passed, all synchronous initialisation (`hstStore`,
25
+ * `resolvedSchema`, `formData`, `handleHSTChange`, operation-log wiring) is performed
26
+ * during `setup()` — before the first render and without awaiting any lifecycle hook.
27
+ * Callers can read `hstStore.value`, `resolvedSchema.value`, and `formData.value`
28
+ * immediately after calling this composable; no `nextTick`, `flushPromises`, or
29
+ * `setTimeout` is required.
30
+ *
31
+ * The only remaining async work in `onMounted` is fetching an existing record from the
32
+ * server when `recordId` is not `'new'`, and lazy-loading a doctype by slug string.
23
33
  *
24
34
  * @param options - Configuration with doctype (string slug or Doctype instance) and optional recordId
25
35
  * @returns Stonecrop instance with full HST integration utilities
@@ -58,6 +68,23 @@ export function useStonecrop(options?: {
58
68
  const error = ref<Error | null>(null)
59
69
  const resolvedDoctype = ref<Doctype | undefined>()
60
70
 
71
+ // Workflow readiness computed properties
72
+ const isWorkflowReady = computed(() => {
73
+ if (!stonecrop.value || !resolvedDoctype.value || !options.recordId || options.recordId === 'new') {
74
+ return true
75
+ }
76
+ const status = stonecrop.value.isWorkflowReady(resolvedDoctype.value, options.recordId)
77
+ return status.ready
78
+ })
79
+
80
+ const blockedLinks = computed(() => {
81
+ if (!stonecrop.value || !resolvedDoctype.value || !options.recordId || options.recordId === 'new') {
82
+ return []
83
+ }
84
+ const status = stonecrop.value.isWorkflowReady(resolvedDoctype.value, options.recordId)
85
+ return status.blockedLinks ?? []
86
+ })
87
+
61
88
  // Initialize stonecrop instance synchronously using singleton pattern
62
89
  // Use injected instance if available, otherwise fall back to the singleton root
63
90
  const stonecropInstance = providedStonecrop || Stonecrop._root
@@ -70,7 +97,7 @@ export function useStonecrop(options?: {
70
97
  resolvedDoctype.value = options.doctype
71
98
  }
72
99
 
73
- // Operation log state and methods - will be populated after stonecrop instance is created
100
+ // Operation log state and methods
74
101
  const operations = ref<HSTOperation[]>([])
75
102
  const currentIndex = ref(-1)
76
103
  const canUndo = computed(() => stonecrop.value?.getOperationLogStore().canUndo ?? false)
@@ -147,13 +174,9 @@ export function useStonecrop(options?: {
147
174
  stonecrop.value?.getOperationLogStore().configure(config)
148
175
  }
149
176
 
150
- // Initialize Stonecrop instance
151
- onMounted(async () => {
152
- if (!registry || !stonecrop.value) {
153
- return
154
- }
155
-
156
- // Set up reactive refs from operation log store - only if Pinia is available
177
+ // Wire operation log reactive state synchronously — no lifecycle hook needed.
178
+ // storeToRefs and watch are both safe to call in setup() body.
179
+ if (registry && stonecrop.value) {
157
180
  try {
158
181
  const opLogStore = stonecrop.value.getOperationLogStore()
159
182
  const opLogRefs = storeToRefs(opLogStore)
@@ -174,8 +197,31 @@ export function useStonecrop(options?: {
174
197
  }
175
198
  )
176
199
  } catch {
177
- // Pinia not available (e.g., in tests) - operation log features will not be available
178
- // Silently fail - operation log is optional
200
+ // Pinia not available operation log is optional, silently skip
201
+ }
202
+ }
203
+
204
+ // Synchronous HST initialisation for an explicit Doctype instance.
205
+ // When the caller passes a Doctype object (not a slug string), every piece of
206
+ // setup that doesn't require network I/O runs here during setup() so that
207
+ // hstStore, resolvedSchema, and formData are populated before the first render
208
+ // and are immediately available to callers without any await.
209
+ if (options.doctype && typeof options.doctype !== 'string' && registry && stonecrop.value) {
210
+ hstStore.value = stonecrop.value.getStore()
211
+ resolvedSchema.value = registry.resolveSchema(options.doctype)
212
+ if (!options.recordId || options.recordId === 'new') {
213
+ formData.value = registry.initializeRecord(resolvedSchema.value)
214
+ }
215
+ if (hstStore.value) {
216
+ setupDeepReactivity(options.doctype, options.recordId || 'new', formData, hstStore.value)
217
+ }
218
+ }
219
+
220
+ // onMounted handles only work that is genuinely async: lazy-loading a doctype
221
+ // by slug, fetching an existing record from the server, and router-based setup.
222
+ onMounted(async () => {
223
+ if (!registry || !stonecrop.value) {
224
+ return
179
225
  }
180
226
 
181
227
  // Handle router-based setup if no specific doctype provided
@@ -207,12 +253,7 @@ export function useStonecrop(options?: {
207
253
 
208
254
  // Resolve schema for router-loaded doctype
209
255
  if (registry) {
210
- const schemaArray = doctype.schema
211
- ? Array.isArray(doctype.schema)
212
- ? doctype.schema
213
- : Array.from(doctype.schema)
214
- : []
215
- resolvedSchema.value = registry.resolveSchema(schemaArray)
256
+ resolvedSchema.value = registry.resolveSchema(doctype)
216
257
  }
217
258
 
218
259
  if (recordId && recordId !== 'new') {
@@ -245,18 +286,16 @@ export function useStonecrop(options?: {
245
286
 
246
287
  // Handle HST integration if doctype is provided explicitly
247
288
  if (options.doctype) {
248
- hstStore.value = stonecrop.value.getStore()
249
289
  const recordId = options.recordId
250
290
 
251
- // Resolve doctype - handle string (lazy-load) or Doctype instance
252
- let doctype: Doctype | undefined
253
-
254
291
  if (typeof options.doctype === 'string') {
255
- // String doctype - check registry first, then lazy-load
292
+ // String doctype resolve lazily, then do full sync-equivalent setup here.
256
293
  const doctypeSlug = options.doctype
294
+ hstStore.value = stonecrop.value.getStore()
257
295
  isLoading.value = true
258
296
  error.value = null
259
297
 
298
+ let doctype: Doctype | undefined
260
299
  try {
261
300
  // Check if already in registry
262
301
  doctype = registry.getDoctype(doctypeSlug)
@@ -281,48 +320,54 @@ export function useStonecrop(options?: {
281
320
  } finally {
282
321
  isLoading.value = false
283
322
  }
284
- } else {
285
- // Doctype instance provided directly
286
- doctype = options.doctype
287
- }
288
323
 
289
- // Set resolved doctype for consumers
290
- resolvedDoctype.value = doctype
324
+ resolvedDoctype.value = doctype
325
+ if (!doctype) return
291
326
 
292
- if (!doctype) {
293
- // Error already set above, just return
294
- return
295
- }
327
+ resolvedSchema.value = registry.resolveSchema(doctype)
296
328
 
297
- // Resolve schema for the doctype
298
- const schemaArray = doctype.schema
299
- ? Array.isArray(doctype.schema)
300
- ? doctype.schema
301
- : Array.from(doctype.schema)
302
- : []
303
- resolvedSchema.value = registry.resolveSchema(schemaArray)
304
-
305
- if (recordId && recordId !== 'new') {
306
- const existingRecord = stonecrop.value.getRecordById(doctype, recordId)
307
- if (existingRecord) {
308
- formData.value = existingRecord.get('') || {}
309
- } else {
310
- try {
311
- await stonecrop.value.getRecord(doctype, recordId)
312
- const loadedRecord = stonecrop.value.getRecordById(doctype, recordId)
313
- if (loadedRecord) {
314
- formData.value = loadedRecord.get('') || {}
329
+ if (recordId && recordId !== 'new') {
330
+ const existingRecord = stonecrop.value.getRecordById(doctype, recordId)
331
+ if (existingRecord) {
332
+ formData.value = existingRecord.get('') || {}
333
+ } else {
334
+ try {
335
+ await stonecrop.value.getRecord(doctype, recordId)
336
+ const loadedRecord = stonecrop.value.getRecordById(doctype, recordId)
337
+ if (loadedRecord) {
338
+ formData.value = loadedRecord.get('') || {}
339
+ }
340
+ } catch {
341
+ formData.value = registry.initializeRecord(resolvedSchema.value)
315
342
  }
316
- } catch {
317
- formData.value = registry.initializeRecord(resolvedSchema.value)
318
343
  }
344
+ } else {
345
+ formData.value = registry.initializeRecord(resolvedSchema.value)
319
346
  }
320
- } else {
321
- formData.value = registry.initializeRecord(resolvedSchema.value)
322
- }
323
347
 
324
- if (hstStore.value) {
325
- setupDeepReactivity(doctype, recordId || 'new', formData, hstStore.value)
348
+ if (hstStore.value) {
349
+ setupDeepReactivity(doctype, recordId || 'new', formData, hstStore.value)
350
+ }
351
+ } else {
352
+ // Doctype instance — sync init was done during setup().
353
+ // Only handle the async path: fetching an existing record from the server.
354
+ if (recordId && recordId !== 'new') {
355
+ const doctype = options.doctype
356
+ const existingRecord = stonecrop.value.getRecordById(doctype, recordId)
357
+ if (existingRecord) {
358
+ formData.value = existingRecord.get('') || {}
359
+ } else {
360
+ try {
361
+ await stonecrop.value.getRecord(doctype, recordId)
362
+ const loadedRecord = stonecrop.value.getRecordById(doctype, recordId)
363
+ if (loadedRecord) {
364
+ formData.value = loadedRecord.get('') || {}
365
+ }
366
+ } catch {
367
+ formData.value = registry.initializeRecord(resolvedSchema.value)
368
+ }
369
+ }
370
+ }
326
371
  }
327
372
  }
328
373
  })
@@ -393,18 +438,36 @@ export function useStonecrop(options?: {
393
438
  }
394
439
 
395
440
  /**
396
- * Load nested doctype data from API or initialize empty structure
397
- * Delegates to Stonecrop.loadNestedData method
398
- * @param parentPath - The parent path (e.g., "customer.123.address")
399
- * @param childDoctype - The child doctype metadata
400
- * @param recordId - Optional record ID to load
401
- * @returns The loaded or initialized data
441
+ * Scaffold empty descendant records from defaults for all descendant links.
442
+ * Delegates to Stonecrop.initializeNestedData method.
443
+ * @param path - The HST path where initialized data should be stored
444
+ * @param doctype - The doctype to initialize
445
+ */
446
+ const initializeNestedData = (path: string, doctype: Doctype): void => {
447
+ if (!stonecrop.value) {
448
+ throw new Error('Stonecrop instance not available')
449
+ }
450
+ return stonecrop.value.initializeNestedData(path, doctype)
451
+ }
452
+
453
+ /**
454
+ * Fetch a record and its nested data from the server.
455
+ * Delegates to Stonecrop.fetchNestedData method.
456
+ * @param path - The HST path (e.g., "recipe.r1")
457
+ * @param doctype - The doctype to fetch
458
+ * @param recordId - Record ID to fetch
459
+ * @param options - Query options (includeNested to control which links are fetched)
402
460
  */
403
- const loadNestedData = (parentPath: string, childDoctype: Doctype, recordId?: string): Record<string, any> => {
461
+ const fetchNestedData = async (
462
+ path: string,
463
+ doctype: Doctype,
464
+ recordId: string,
465
+ options?: { includeNested?: boolean | string[] }
466
+ ): Promise<void> => {
404
467
  if (!stonecrop.value) {
405
468
  throw new Error('Stonecrop instance not available')
406
469
  }
407
- return stonecrop.value.loadNestedData(parentPath, childDoctype, recordId)
470
+ return stonecrop.value.fetchNestedData(path, doctype, recordId, options)
408
471
  }
409
472
 
410
473
  /**
@@ -422,12 +485,12 @@ export function useStonecrop(options?: {
422
485
  }
423
486
 
424
487
  /**
425
- * Create a nested context for child forms
488
+ * Create a nested context for descendant forms
426
489
  * @param basePath - The base path for the nested context (e.g., "customer.123.address")
427
- * @param _childDoctype - The child doctype metadata (unused but kept for API consistency)
490
+ * @param _descendantDoctype - The descendant doctype metadata (unused but kept for API consistency)
428
491
  * @returns Object with scoped provideHSTPath and handleHSTChange
429
492
  */
430
- const createNestedContext = (basePath: string, _childDoctype: Doctype) => {
493
+ const createNestedContext = (basePath: string, _descendantDoctype: Doctype) => {
431
494
  const nestedProvideHSTPath = (fieldname: string): string => {
432
495
  return `${basePath}.${fieldname}`
433
496
  }
@@ -480,13 +543,16 @@ export function useStonecrop(options?: {
480
543
  hstStore,
481
544
  formData,
482
545
  resolvedSchema,
483
- loadNestedData,
546
+ initializeNestedData,
547
+ fetchNestedData,
484
548
  collectRecordPayload,
485
549
  createNestedContext,
486
550
  isLoading,
487
551
  error,
488
552
  resolvedDoctype,
489
- } as HSTStonecropReturn
553
+ isWorkflowReady,
554
+ blockedLinks,
555
+ } satisfies HSTStonecropReturn
490
556
  } else if (!options.doctype && registry?.router) {
491
557
  // Router-based - return HST (will be populated after mount)
492
558
  return {
@@ -497,13 +563,16 @@ export function useStonecrop(options?: {
497
563
  hstStore,
498
564
  formData,
499
565
  resolvedSchema,
500
- loadNestedData,
566
+ initializeNestedData,
567
+ fetchNestedData,
501
568
  collectRecordPayload,
502
569
  createNestedContext,
503
570
  isLoading,
504
571
  error,
505
572
  resolvedDoctype,
506
- } as HSTStonecropReturn
573
+ isWorkflowReady,
574
+ blockedLinks,
575
+ } satisfies HSTStonecropReturn
507
576
  }
508
577
 
509
578
  // No doctype and no router - basic mode
package/src/doctype.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import type { SchemaTypes } from '@stonecrop/aform'
2
- import type { WorkflowMeta } from '@stonecrop/schema'
2
+ import type { LinkDeclaration, WorkflowMeta } from '@stonecrop/schema'
3
3
  import { List, Map } from 'immutable'
4
4
  import { Component } from 'vue'
5
- import type { UnknownMachineConfig } from 'xstate'
6
5
 
7
6
  import type { ImmutableDoctype } from './types'
8
7
  import type { DoctypeConfig } from './types/doctype'
@@ -56,6 +55,13 @@ export default class Doctype {
56
55
  */
57
56
  readonly component?: Component
58
57
 
58
+ /**
59
+ * Relationship links to other doctypes
60
+ * @public
61
+ * @readonly
62
+ */
63
+ readonly links?: Record<string, LinkDeclaration>
64
+
59
65
  /**
60
66
  * Creates a new Doctype instance
61
67
  * @param doctype - The doctype name
@@ -63,19 +69,22 @@ export default class Doctype {
63
69
  * @param workflow - The doctype workflow configuration (XState machine)
64
70
  * @param actions - The doctype actions and field triggers
65
71
  * @param component - Optional Vue component for rendering the doctype
72
+ * @param links - Optional relationship links to other doctypes
66
73
  */
67
74
  constructor(
68
75
  doctype: string,
69
76
  schema: ImmutableDoctype['schema'],
70
77
  workflow: ImmutableDoctype['workflow'],
71
78
  actions: ImmutableDoctype['actions'],
72
- component?: Component
79
+ component?: Component,
80
+ links?: Record<string, LinkDeclaration>
73
81
  ) {
74
82
  this.doctype = doctype
75
83
  this.schema = schema
76
84
  this.workflow = workflow
77
85
  this.actions = actions
78
86
  this.component = component
87
+ this.links = links
79
88
  }
80
89
 
81
90
  /**
@@ -120,7 +129,7 @@ export default class Doctype {
120
129
  const schema = config.fields ? List(config.fields) : List<SchemaTypes>()
121
130
  const actions = config.actions ? Map(config.actions) : Map<string, string[]>()
122
131
 
123
- return new Doctype(config.name, schema, config.workflow, actions)
132
+ return new Doctype(config.name, schema, config.workflow, actions, undefined, config.links)
124
133
  }
125
134
 
126
135
  /**
@@ -1,6 +1,7 @@
1
1
  import type { Map as ImmutableMap } from 'immutable'
2
2
  import { useOperationLogStore } from './stores/operation-log'
3
3
  import type {
4
+ FieldAction,
4
5
  FieldActionFunction,
5
6
  FieldChangeContext,
6
7
  FieldTriggerExecutionResult,
@@ -22,7 +23,7 @@ export class FieldTriggerEngine {
22
23
  */
23
24
  static _root: FieldTriggerEngine
24
25
 
25
- private options: FieldTriggerOptions & { defaultTimeout: number; debug: boolean; enableRollback: boolean }
26
+ private options!: FieldTriggerOptions & { defaultTimeout: number; debug: boolean; enableRollback: boolean }
26
27
  private doctypeActions = new Map<string, Map<string, string[]>>() // doctype -> action/field -> functions
27
28
  private doctypeTransitions = new Map<string, Map<string, string[]>>() // doctype -> transition -> functions
28
29
  private fieldRollbackConfig = new Map<string, Map<string, boolean>>() // doctype -> field -> rollback enabled
@@ -55,6 +56,15 @@ export class FieldTriggerEngine {
55
56
  this.globalActions.set(name, fn)
56
57
  }
57
58
 
59
+ /**
60
+ * Look up a registered action function by name.
61
+ * Returns `undefined` if the action has not been registered.
62
+ * @param name - The action name
63
+ */
64
+ getAction(name: string): FieldActionFunction | undefined {
65
+ return this.globalActions.get(name)
66
+ }
67
+
58
68
  /**
59
69
  * Register a global XState transition action function
60
70
  * @param name - The name of the transition action
@@ -227,11 +237,13 @@ export class FieldTriggerEngine {
227
237
  const totalExecutionTime = performance.now() - startTime
228
238
 
229
239
  // Call global error handler if configured and errors occurred
230
- const failedResults = actionResults.filter(r => !r.success)
240
+ const failedResults = actionResults.filter(r => !r.success && r.error != null)
231
241
  if (failedResults.length > 0 && this.options.errorHandler) {
232
242
  for (const failedResult of failedResults) {
233
243
  try {
234
- this.options.errorHandler(failedResult.error!, context, failedResult.action)
244
+ if (failedResult.error) {
245
+ this.options.errorHandler(failedResult.error, context, failedResult.action)
246
+ }
235
247
  } catch (handlerError) {
236
248
  // eslint-disable-next-line no-console
237
249
  console.error('[FieldTriggers] Error in global error handler:', handlerError)
@@ -301,7 +313,9 @@ export class FieldTriggerEngine {
301
313
  for (const failedResult of failedResults) {
302
314
  try {
303
315
  // Call with FieldChangeContext (base context type)
304
- this.options.errorHandler(failedResult.error!, context, failedResult.action)
316
+ if (failedResult.error) {
317
+ this.options.errorHandler(failedResult.error, context, failedResult.action as unknown as FieldAction)
318
+ }
305
319
  } catch (handlerError) {
306
320
  // eslint-disable-next-line no-console
307
321
  console.error('[FieldTriggers] Error in global error handler:', handlerError)
package/src/index.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export type * from '@stonecrop/aform/types'
2
2
  export type * from '@stonecrop/atable/types'
3
3
 
4
+ import { useLazyLink } from './composables/lazy-link'
4
5
  import { useStonecrop } from './composables/stonecrop'
5
6
  import { useOperationLog, useUndoRedoShortcuts, withBatch } from './composables/operation-log'
6
7
  import Doctype from './doctype'
@@ -15,7 +16,7 @@ import {
15
16
  } from './field-triggers'
16
17
  import plugin from './plugins'
17
18
  import Registry from './registry'
18
- import { Stonecrop, collectNestedData } from './stonecrop'
19
+ import { Stonecrop, getStonecrop } from './stonecrop'
19
20
  import { HST, createHST, type HSTNode } from './stores/hst'
20
21
  import { useOperationLogStore } from './stores/operation-log'
21
22
  import { SchemaValidator, createValidator, validateSchema } from './schema-validator'
@@ -31,6 +32,7 @@ export {
31
32
  Doctype,
32
33
  Registry,
33
34
  Stonecrop,
35
+ useLazyLink,
34
36
  useStonecrop,
35
37
  // HST exports for advanced usage
36
38
  HST,
@@ -54,7 +56,7 @@ export {
54
56
  useUndoRedoShortcuts,
55
57
  withBatch,
56
58
  // Utility functions
57
- collectNestedData,
59
+ getStonecrop,
58
60
  }
59
61
 
60
62
  // Default export is the Vue plugin