@stonecrop/stonecrop 0.10.6 → 0.10.8

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.6",
3
+ "version": "0.10.8",
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.6"
37
+ "@stonecrop/schema": "0.10.8"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "pinia": "^3.0.4",
@@ -60,8 +60,8 @@
60
60
  "vue-router": "^5.0.2",
61
61
  "vite": "^7.3.1",
62
62
  "vitest": "^4.0.18",
63
- "@stonecrop/aform": "0.10.6",
64
- "@stonecrop/atable": "0.10.6",
63
+ "@stonecrop/aform": "0.10.8",
64
+ "@stonecrop/atable": "0.10.8",
65
65
  "stonecrop-rig": "0.7.0"
66
66
  },
67
67
  "description": "Schema-driven framework with XState workflows and HST state management",
@@ -74,6 +74,9 @@ export type HSTStonecropReturn = BaseStonecropReturn & {
74
74
  provideHSTPath: (fieldname: string) => string
75
75
  handleHSTChange: (changeData: HSTChangeData) => void
76
76
  }
77
+ isLoading: Ref<boolean>
78
+ error: Ref<Error | null>
79
+ resolvedDoctype: Ref<Doctype | undefined>
77
80
  }
78
81
 
79
82
  /**
@@ -98,17 +101,21 @@ export function useStonecrop(): BaseStonecropReturn | HSTStonecropReturn
98
101
  /**
99
102
  * Unified Stonecrop composable with HST integration for a specific doctype and record
100
103
  *
101
- * @param options - Configuration with doctype and optional recordId
104
+ * @param options - Configuration with doctype (string slug or Doctype instance) and optional recordId
102
105
  * @returns Stonecrop instance with full HST integration utilities
103
106
  * @public
104
107
  */
105
- export function useStonecrop(options: { registry?: Registry; doctype: Doctype; recordId?: string }): HSTStonecropReturn
108
+ export function useStonecrop(options: {
109
+ registry?: Registry
110
+ doctype: Doctype | string
111
+ recordId?: string
112
+ }): HSTStonecropReturn
106
113
  /**
107
114
  * @public
108
115
  */
109
116
  export function useStonecrop(options?: {
110
117
  registry?: Registry
111
- doctype?: Doctype
118
+ doctype?: Doctype | string
112
119
  recordId?: string
113
120
  }): BaseStonecropReturn | HSTStonecropReturn {
114
121
  if (!options) options = {}
@@ -126,14 +133,14 @@ export function useStonecrop(options?: {
126
133
  // Resolved schema with nested Doctype fields expanded
127
134
  const resolvedSchema = ref<SchemaTypes[]>([])
128
135
 
129
- // Auto-resolve schema when doctype is available
130
- if (options.doctype && registry) {
131
- const schemaArray = options.doctype.schema
132
- ? Array.isArray(options.doctype.schema)
133
- ? options.doctype.schema
134
- : Array.from(options.doctype.schema)
135
- : []
136
- resolvedSchema.value = registry.resolveSchema(schemaArray as SchemaTypes[])
136
+ // Loading state for lazy-loaded doctypes
137
+ const isLoading = ref(false)
138
+ const error = ref<Error | null>(null)
139
+ const resolvedDoctype = ref<Doctype | undefined>()
140
+
141
+ // If doctype is a Doctype instance (not string), set resolved immediately
142
+ if (options?.doctype && typeof options.doctype !== 'string') {
143
+ resolvedDoctype.value = options.doctype
137
144
  }
138
145
 
139
146
  // Operation log state and methods - will be populated after stonecrop instance is created
@@ -280,7 +287,7 @@ export function useStonecrop(options?: {
280
287
  ? doctype.schema
281
288
  : Array.from(doctype.schema)
282
289
  : []
283
- resolvedSchema.value = registry.resolveSchema(schemaArray as SchemaTypes[])
290
+ resolvedSchema.value = registry.resolveSchema(schemaArray)
284
291
  }
285
292
 
286
293
  if (recordId && recordId !== 'new') {
@@ -314,9 +321,62 @@ export function useStonecrop(options?: {
314
321
  // Handle HST integration if doctype is provided explicitly
315
322
  if (options.doctype) {
316
323
  hstStore.value = stonecrop.value.getStore()
317
- const doctype = options.doctype
318
324
  const recordId = options.recordId
319
325
 
326
+ // Resolve doctype - handle string (lazy-load) or Doctype instance
327
+ let doctype: Doctype | undefined
328
+
329
+ if (typeof options.doctype === 'string') {
330
+ // String doctype - check registry first, then lazy-load
331
+ const doctypeSlug = options.doctype
332
+ isLoading.value = true
333
+ error.value = null
334
+
335
+ try {
336
+ // Check if already in registry
337
+ doctype = registry.getDoctype(doctypeSlug)
338
+
339
+ if (!doctype && registry.getMeta) {
340
+ // Lazy-load via getMeta
341
+ const routeContext: RouteContext = {
342
+ path: `/${doctypeSlug}`,
343
+ segments: [doctypeSlug],
344
+ }
345
+ doctype = await registry.getMeta(routeContext)
346
+ if (doctype) {
347
+ registry.addDoctype(doctype)
348
+ }
349
+ }
350
+
351
+ if (!doctype) {
352
+ error.value = new Error(`Doctype '${doctypeSlug}' not found in registry and getMeta returned no result`)
353
+ }
354
+ } catch (e) {
355
+ error.value = e instanceof Error ? e : new Error(String(e))
356
+ } finally {
357
+ isLoading.value = false
358
+ }
359
+ } else {
360
+ // Doctype instance provided directly
361
+ doctype = options.doctype
362
+ }
363
+
364
+ // Set resolved doctype for consumers
365
+ resolvedDoctype.value = doctype
366
+
367
+ if (!doctype) {
368
+ // Error already set above, just return
369
+ return
370
+ }
371
+
372
+ // Resolve schema for the doctype
373
+ const schemaArray = doctype.schema
374
+ ? Array.isArray(doctype.schema)
375
+ ? doctype.schema
376
+ : Array.from(doctype.schema)
377
+ : []
378
+ resolvedSchema.value = registry.resolveSchema(schemaArray)
379
+
320
380
  if (recordId && recordId !== 'new') {
321
381
  const existingRecord = stonecrop.value.getRecordById(doctype, recordId)
322
382
  if (existingRecord) {
@@ -344,7 +404,7 @@ export function useStonecrop(options?: {
344
404
 
345
405
  // HST integration functions - always created but only populated when HST is available
346
406
  const provideHSTPath = (fieldname: string, customRecordId?: string): string => {
347
- const doctype = options.doctype || routerDoctype.value
407
+ const doctype = resolvedDoctype.value || routerDoctype.value
348
408
  if (!doctype) return ''
349
409
 
350
410
  const actualRecordId = customRecordId || options.recordId || routerRecordId.value || 'new'
@@ -352,7 +412,7 @@ export function useStonecrop(options?: {
352
412
  }
353
413
 
354
414
  const handleHSTChange = (changeData: HSTChangeData): void => {
355
- const doctype = options.doctype || routerDoctype.value
415
+ const doctype = resolvedDoctype.value || routerDoctype.value
356
416
  if (!hstStore.value || !stonecrop.value || !doctype) {
357
417
  return
358
418
  }
@@ -458,9 +518,11 @@ export function useStonecrop(options?: {
458
518
  const payload: Record<string, any> = { ...recordData }
459
519
 
460
520
  // Use resolveSchema to get the full resolved tree, then walk Doctype fields
461
- const schemaArray = (
462
- doctype.schema ? (Array.isArray(doctype.schema) ? doctype.schema : Array.from(doctype.schema)) : []
463
- ) as SchemaTypes[]
521
+ const schemaArray = doctype.schema
522
+ ? Array.isArray(doctype.schema)
523
+ ? doctype.schema
524
+ : Array.from(doctype.schema)
525
+ : []
464
526
  const resolved = registry ? registry.resolveSchema(schemaArray) : schemaArray
465
527
  const doctypeFields = resolved.filter(
466
528
  field => 'fieldtype' in field && field.fieldtype === 'Doctype' && 'schema' in field && Array.isArray(field.schema)
@@ -539,6 +601,9 @@ export function useStonecrop(options?: {
539
601
  loadNestedData,
540
602
  saveRecursive,
541
603
  createNestedContext,
604
+ isLoading,
605
+ error,
606
+ resolvedDoctype,
542
607
  } as HSTStonecropReturn
543
608
  } else if (!options.doctype && registry?.router) {
544
609
  // Router-based - return HST (will be populated after mount)
@@ -553,6 +618,9 @@ export function useStonecrop(options?: {
553
618
  loadNestedData,
554
619
  saveRecursive,
555
620
  createNestedContext,
621
+ isLoading,
622
+ error,
623
+ resolvedDoctype,
556
624
  } as HSTStonecropReturn
557
625
  }
558
626
 
package/src/doctype.ts CHANGED
@@ -1,7 +1,7 @@
1
+ import type { SchemaTypes } from '@stonecrop/aform'
2
+ import type { WorkflowMeta } from '@stonecrop/schema'
1
3
  import { List, Map } from 'immutable'
2
4
  import { Component } from 'vue'
3
-
4
- import type { SchemaTypes } from '@stonecrop/aform'
5
5
  import type { UnknownMachineConfig } from 'xstate'
6
6
 
7
7
  import type { ImmutableDoctype } from './types'
@@ -20,8 +20,8 @@ export type DoctypeConfig = {
20
20
  tableName?: string
21
21
  /** Field definitions */
22
22
  fields?: SchemaTypes[]
23
- /** Workflow configuration */
24
- workflow?: UnknownMachineConfig
23
+ /** Workflow configuration (XState format or simple WorkflowMeta) */
24
+ workflow?: UnknownMachineConfig | WorkflowMeta
25
25
  /** Actions and their field triggers */
26
26
  actions?: Record<string, string[]>
27
27
  /** Parent doctype for inheritance */
@@ -183,7 +183,7 @@ export default class Doctype {
183
183
 
184
184
  /**
185
185
  * Returns the transitions available from a given workflow state, derived from the
186
- * doctype's XState workflow configuration.
186
+ * doctype's workflow configuration. Supports both XState format and WorkflowMeta format.
187
187
  *
188
188
  * @param currentState - The state name to read transitions from
189
189
  * @returns Array of transition descriptors with `name` and `targetState`
@@ -197,7 +197,34 @@ export default class Doctype {
197
197
  * @public
198
198
  */
199
199
  getAvailableTransitions(currentState: string): Array<{ name: string; targetState: string }> {
200
- const states = this.workflow?.states
200
+ const workflow = this.workflow
201
+ if (!workflow) return []
202
+
203
+ // Check if this is WorkflowMeta format (states is an array) or XState format (states is an object)
204
+ if (Array.isArray(workflow.states)) {
205
+ // WorkflowMeta format: validate state exists and filter actions by allowedStates
206
+ const states = workflow.states
207
+ if (!states.includes(currentState)) return []
208
+
209
+ const actions = (workflow as WorkflowMeta).actions
210
+ if (!actions) return []
211
+
212
+ return Object.entries(actions)
213
+ .filter(([, actionDef]) => {
214
+ const allowedStates = actionDef.allowedStates
215
+ // If no allowedStates specified, action is available in all valid states
216
+ if (!allowedStates || allowedStates.length === 0) return true
217
+ return allowedStates.includes(currentState)
218
+ })
219
+ .map(([name]) => ({
220
+ name,
221
+ // WorkflowMeta doesn't define target states - transitions are handled server-side
222
+ targetState: currentState,
223
+ }))
224
+ }
225
+
226
+ // XState format: use the on property of the state
227
+ const states = workflow.states
201
228
  if (!states) return []
202
229
  const stateConfig = states[currentState]
203
230
  if (!stateConfig?.on) return []
@@ -207,6 +234,40 @@ export default class Doctype {
207
234
  }))
208
235
  }
209
236
 
237
+ /**
238
+ * Returns metadata for a specific action, if available.
239
+ * Only works with WorkflowMeta format; returns undefined for XState format.
240
+ *
241
+ * @param actionName - The action name to get metadata for
242
+ * @returns Action metadata or undefined
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * const actionMeta = doctype.getActionMeta('submit')
247
+ * // { label: 'Submit', handler: 'plan:submit', allowedStates: ['draft'] }
248
+ * ```
249
+ *
250
+ * @public
251
+ */
252
+ getActionMeta(
253
+ actionName: string
254
+ ):
255
+ | {
256
+ label: string
257
+ handler: string
258
+ requiredFields?: string[]
259
+ allowedStates?: string[]
260
+ confirm?: boolean
261
+ args?: Record<string, unknown>
262
+ }
263
+ | undefined {
264
+ const workflow = this.workflow
265
+ if (!workflow || !Array.isArray(workflow.states)) return undefined
266
+
267
+ const actions = (workflow as WorkflowMeta).actions
268
+ return actions?.[actionName]
269
+ }
270
+
210
271
  /**
211
272
  * Converts the registered doctype string to a slug (kebab-case). The following conversions are made:
212
273
  * - It replaces camelCase and PascalCase with kebab-case strings
package/src/stonecrop.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { DataClient } from '@stonecrop/schema'
1
+ import type { DataClient, WorkflowMeta } from '@stonecrop/schema'
2
2
  import { reactive } from 'vue'
3
3
 
4
4
  import Doctype from './doctype'
@@ -400,10 +400,20 @@ export class Stonecrop {
400
400
  const record = this.getRecordById(slug, recordId)
401
401
  const status = record?.get('status') as string | undefined
402
402
 
403
- const initialState =
404
- typeof meta.workflow.initial === 'string'
405
- ? meta.workflow.initial
406
- : Object.keys(meta.workflow.states ?? {})[0] ?? ''
403
+ // Handle both XState format and WorkflowMeta format
404
+ const workflow = meta.workflow
405
+ let initialState: string
406
+
407
+ if (Array.isArray(workflow.states)) {
408
+ // WorkflowMeta format: states is a string array
409
+ initialState = workflow.states[0] ?? ''
410
+ } else {
411
+ // XState format: states is an object, use initial or first key
412
+ initialState =
413
+ typeof (workflow as { initial?: unknown }).initial === 'string'
414
+ ? (workflow as { initial: string }).initial
415
+ : Object.keys(workflow.states ?? {})[0] ?? ''
416
+ }
407
417
 
408
418
  return status || initialState
409
419
  }
@@ -1,4 +1,4 @@
1
- import type { DataClient } from '@stonecrop/schema'
1
+ import type { DataClient, WorkflowMeta } from '@stonecrop/schema'
2
2
  import type { SchemaTypes } from '@stonecrop/aform'
3
3
  import { List, Map } from 'immutable'
4
4
  import type { Component } from 'vue'
@@ -16,7 +16,7 @@ import type { RouteContext } from './registry'
16
16
  */
17
17
  export type ImmutableDoctype = {
18
18
  readonly schema?: List<SchemaTypes> // TODO: allow schema to be a function
19
- readonly workflow?: UnknownMachineConfig | AnyStateNodeConfig
19
+ readonly workflow?: UnknownMachineConfig | AnyStateNodeConfig | WorkflowMeta
20
20
  readonly actions?: Map<string, string[]>
21
21
  }
22
22
 
@@ -27,7 +27,7 @@ export type ImmutableDoctype = {
27
27
  export type MutableDoctype = {
28
28
  doctype?: string
29
29
  schema?: SchemaTypes[] // TODO: allow schema to be a function
30
- workflow?: UnknownMachineConfig | AnyStateNodeConfig
30
+ workflow?: UnknownMachineConfig | AnyStateNodeConfig | WorkflowMeta
31
31
  actions?: Record<string, string[]>
32
32
  }
33
33