@stonecrop/stonecrop 0.10.16 → 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.
- package/README.md +72 -29
- package/dist/composable.js +1 -0
- package/dist/composables/lazy-link.js +125 -0
- package/dist/composables/stonecrop.js +123 -68
- package/dist/composables/use-lazy-link-state.js +125 -0
- package/dist/composables/use-stonecrop.js +476 -0
- package/dist/doctype.js +10 -2
- package/dist/field-triggers.js +15 -3
- package/dist/index.js +4 -3
- package/dist/operation-log-DB-dGNT9.js +593 -0
- package/dist/operation-log-DB-dGNT9.js.map +1 -0
- package/dist/registry.js +261 -101
- package/dist/schema-validator.js +105 -1
- package/dist/src/composable.d.ts +11 -0
- package/dist/src/composable.d.ts.map +1 -0
- package/dist/src/composable.js +477 -0
- package/dist/src/composables/lazy-link.d.ts +25 -0
- package/dist/src/composables/lazy-link.d.ts.map +1 -0
- package/dist/src/composables/operation-log.d.ts +5 -5
- package/dist/src/composables/operation-log.d.ts.map +1 -1
- package/dist/src/composables/operation-log.js +224 -0
- package/dist/src/composables/stonecrop.d.ts +11 -1
- package/dist/src/composables/stonecrop.d.ts.map +1 -1
- package/dist/src/composables/stonecrop.js +574 -0
- package/dist/src/composables/use-lazy-link-state.d.ts +25 -0
- package/dist/src/composables/use-lazy-link-state.d.ts.map +1 -0
- package/dist/src/composables/use-stonecrop.d.ts +93 -0
- package/dist/src/composables/use-stonecrop.d.ts.map +1 -0
- package/dist/src/composables/useNestedSchema.d.ts +110 -0
- package/dist/src/composables/useNestedSchema.d.ts.map +1 -0
- package/dist/src/composables/useNestedSchema.js +155 -0
- package/dist/src/doctype.d.ts +9 -1
- package/dist/src/doctype.d.ts.map +1 -1
- package/dist/src/doctype.js +234 -0
- package/dist/src/exceptions.js +16 -0
- package/dist/src/field-triggers.d.ts +6 -0
- package/dist/src/field-triggers.d.ts.map +1 -1
- package/dist/src/field-triggers.js +567 -0
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +23 -0
- package/dist/src/plugins/index.js +96 -0
- package/dist/src/registry.d.ts +102 -23
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/registry.js +246 -0
- package/dist/src/schema-validator.d.ts +8 -1
- package/dist/src/schema-validator.d.ts.map +1 -1
- package/dist/src/schema-validator.js +315 -0
- package/dist/src/stonecrop.d.ts +73 -28
- package/dist/src/stonecrop.d.ts.map +1 -1
- package/dist/src/stonecrop.js +339 -0
- package/dist/src/stores/data.d.ts +11 -0
- package/dist/src/stores/data.d.ts.map +1 -0
- package/dist/src/stores/hst.d.ts +5 -75
- package/dist/src/stores/hst.d.ts.map +1 -1
- package/dist/src/stores/hst.js +495 -0
- package/dist/src/stores/index.js +12 -0
- package/dist/src/stores/operation-log.d.ts +14 -14
- package/dist/src/stores/operation-log.d.ts.map +1 -1
- package/dist/src/stores/operation-log.js +568 -0
- package/dist/src/stores/xstate.d.ts +31 -0
- package/dist/src/stores/xstate.d.ts.map +1 -0
- package/dist/src/tsdoc-metadata.json +11 -0
- package/dist/src/types/composable.d.ts +50 -12
- package/dist/src/types/composable.d.ts.map +1 -1
- package/dist/src/types/doctype.d.ts +6 -7
- package/dist/src/types/doctype.d.ts.map +1 -1
- package/dist/src/types/field-triggers.d.ts +1 -1
- package/dist/src/types/field-triggers.d.ts.map +1 -1
- package/dist/src/types/field-triggers.js +4 -0
- package/dist/src/types/hst.d.ts +70 -0
- package/dist/src/types/hst.d.ts.map +1 -0
- package/dist/src/types/index.d.ts +1 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/types/index.js +4 -0
- package/dist/src/types/operation-log.d.ts +4 -4
- package/dist/src/types/operation-log.d.ts.map +1 -1
- package/dist/src/types/operation-log.js +0 -0
- package/dist/src/types/registry.js +0 -0
- package/dist/src/types/schema-validator.d.ts +2 -0
- package/dist/src/types/schema-validator.d.ts.map +1 -1
- package/dist/src/utils.d.ts +24 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/stonecrop.d.ts +317 -99
- package/dist/stonecrop.js +2191 -1897
- package/dist/stonecrop.js.map +1 -1
- package/dist/stonecrop.umd.cjs +6 -0
- package/dist/stonecrop.umd.cjs.map +1 -0
- package/dist/stores/data.js +7 -0
- package/dist/stores/hst.js +27 -25
- package/dist/stores/operation-log.js +59 -47
- package/dist/stores/xstate.js +29 -0
- package/dist/tests/setup.d.ts +5 -0
- package/dist/tests/setup.d.ts.map +1 -0
- package/dist/tests/setup.js +15 -0
- package/dist/types/hst.js +0 -0
- package/dist/types/index.js +1 -0
- package/dist/utils.js +46 -0
- package/package.json +5 -5
- package/src/composables/lazy-link.ts +146 -0
- package/src/composables/operation-log.ts +1 -1
- package/src/composables/stonecrop.ts +142 -73
- package/src/doctype.ts +13 -4
- package/src/field-triggers.ts +18 -4
- package/src/index.ts +4 -2
- package/src/registry.ts +289 -111
- package/src/schema-validator.ts +120 -1
- package/src/stonecrop.ts +230 -106
- package/src/stores/hst.ts +29 -104
- package/src/stores/operation-log.ts +64 -50
- package/src/types/composable.ts +55 -12
- package/src/types/doctype.ts +6 -7
- package/src/types/field-triggers.ts +1 -1
- package/src/types/hst.ts +77 -0
- package/src/types/index.ts +1 -0
- package/src/types/operation-log.ts +4 -4
- package/src/types/schema-validator.ts +2 -0
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# Stonecrop
|
|
2
|
+
|
|
2
3
|
_This package is under active development / design._
|
|
3
4
|
|
|
4
5
|
## Features
|
|
5
6
|
|
|
7
|
+
- **Schema-Driven Relationships**: `links` on doctype schemas declare relationships with cardinality and direction
|
|
6
8
|
- **Hierarchical State Tree (HST)**: Advanced state management with tree navigation
|
|
7
9
|
- **Operation Log**: Global undo/redo with time-travel debugging, automatic FSM transition tracking, and action execution tracking
|
|
8
10
|
- **Action Tracking**: Audit trail for stateless action executions (print, email, archive, etc.)
|
|
9
11
|
- **Field Triggers**: Event-driven field actions integrated with XState
|
|
10
|
-
- **VueUse Integration**: Leverages battle-tested VueUse composables for keyboard shortcuts and persistence
|
|
11
12
|
|
|
12
13
|
## Installation & Usage
|
|
13
14
|
|
|
@@ -15,7 +16,8 @@ _This package is under active development / design._
|
|
|
15
16
|
|
|
16
17
|
```typescript
|
|
17
18
|
import { createApp } from 'vue'
|
|
18
|
-
import Stonecrop from '@stonecrop/stonecrop'
|
|
19
|
+
import Stonecrop, { Stonecrop as StonecropClass } from '@stonecrop/stonecrop'
|
|
20
|
+
import { StonecropClient } from '@stonecrop/graphql-client'
|
|
19
21
|
import router from './router'
|
|
20
22
|
|
|
21
23
|
const app = createApp(App)
|
|
@@ -30,32 +32,66 @@ app.use(Stonecrop, {
|
|
|
30
32
|
return await fetchDoctypeMeta(segments[0])
|
|
31
33
|
},
|
|
32
34
|
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
// Wire up the client after plugin initialization.
|
|
36
|
+
// The callback receives registry and stonecrop instances directly.
|
|
37
|
+
onRouterInitialized: (registry, stonecrop) => {
|
|
38
|
+
const client = new StonecropClient({
|
|
39
|
+
endpoint: 'http://localhost:4000/graphql',
|
|
40
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
41
|
+
registry: buildMetaMap(registry),
|
|
42
|
+
})
|
|
43
|
+
stonecrop.setClient(client)
|
|
42
44
|
},
|
|
43
45
|
})
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Accessing Stonecrop Outside Vue Components
|
|
44
49
|
|
|
45
|
-
|
|
50
|
+
Inside a component, use `useStonecrop()`. Outside a component (e.g., workflow action handlers, utilities), use `getStonecrop()`:
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { getStonecrop } from '@stonecrop/stonecrop'
|
|
54
|
+
|
|
55
|
+
// In a workflow action handler or non-component utility:
|
|
56
|
+
const stonecrop = getStonecrop()
|
|
57
|
+
if (stonecrop) {
|
|
58
|
+
const payload = stonecrop.collectRecordPayload(doctype, recordId)
|
|
59
|
+
// ...
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Building the DoctypeMeta Map
|
|
64
|
+
|
|
65
|
+
`StonecropClient` expects a `Map<string, DoctypeMeta>`, but the Registry stores `Doctype` instances. Convert between them:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
import type { DoctypeMeta } from '@stonecrop/schema'
|
|
69
|
+
import type { Registry, Doctype } from '@stonecrop/stonecrop'
|
|
70
|
+
|
|
71
|
+
function buildMetaMap(registry: Registry): Map<string, DoctypeMeta> {
|
|
72
|
+
const metaMap = new Map<string, DoctypeMeta>()
|
|
73
|
+
for (const [slug, doctype] of Object.entries(registry.registry)) {
|
|
74
|
+
metaMap.set(slug, {
|
|
75
|
+
name: doctype.doctype,
|
|
76
|
+
slug,
|
|
77
|
+
tableName: slug.replace(/-/g, '_'),
|
|
78
|
+
fields: doctype.getSchemaArray(),
|
|
79
|
+
links: doctype.links || {},
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
return metaMap
|
|
83
|
+
}
|
|
46
84
|
```
|
|
47
85
|
|
|
48
86
|
### Plugin Options
|
|
49
87
|
|
|
50
|
-
| Option
|
|
51
|
-
|
|
52
|
-
| `router`
|
|
53
|
-
| `getMeta`
|
|
54
|
-
| `
|
|
55
|
-
| `
|
|
56
|
-
| `
|
|
57
|
-
| `autoInitializeRouter` | `boolean` | Call `onRouterInitialized` automatically after mount. Default: `false`. |
|
|
58
|
-
| `onRouterInitialized` | `(registry, stonecrop) => void` | Callback invoked after plugin install + mount. Receives the Registry and Stonecrop instances. |
|
|
88
|
+
| Option | Type | Description |
|
|
89
|
+
| ---------------------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
90
|
+
| `router` | `Router` | Vue Router instance. Required for route-based doctype resolution. |
|
|
91
|
+
| `getMeta` | `(ctx: RouteContext) => Doctype \| Promise<Doctype>` | Lazy-loads doctype metadata for the current route. `ctx` has `path` and `segments`. |
|
|
92
|
+
| `components` | `Record<string, Component>` | Additional Vue components to register globally. |
|
|
93
|
+
| `autoInitializeRouter` | `boolean` | Call `onRouterInitialized` automatically after mount. Default: `false`. |
|
|
94
|
+
| `onRouterInitialized` | `(registry, stonecrop) => void` | Callback invoked after plugin install + mount. Receives the Registry and Stonecrop instances. |
|
|
59
95
|
|
|
60
96
|
### Available Imports
|
|
61
97
|
|
|
@@ -69,6 +105,7 @@ import {
|
|
|
69
105
|
Registry, // Doctype registry (singleton)
|
|
70
106
|
Doctype, // Doctype definition class
|
|
71
107
|
useStonecrop, // Vue composable — primary integration point
|
|
108
|
+
getStonecrop, // Access singleton outside Vue components
|
|
72
109
|
HST, // HST store class
|
|
73
110
|
createHST, // HST factory function
|
|
74
111
|
} from '@stonecrop/stonecrop'
|
|
@@ -118,7 +155,7 @@ When you pass a string doctype slug instead of a `Doctype` instance, `useStonecr
|
|
|
118
155
|
|
|
119
156
|
```typescript
|
|
120
157
|
const { isLoading, error, resolvedDoctype, formData } = useStonecrop({
|
|
121
|
-
doctype: 'plan',
|
|
158
|
+
doctype: 'plan', // string slug - triggers lazy-loading
|
|
122
159
|
recordId: '123',
|
|
123
160
|
})
|
|
124
161
|
|
|
@@ -131,12 +168,15 @@ const { isLoading, error, resolvedDoctype, formData } = useStonecrop({
|
|
|
131
168
|
This pattern eliminates the timing mismatch when loading doctypes asynchronously in Nuxt plugins.
|
|
132
169
|
|
|
133
170
|
## Design
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
171
|
+
|
|
172
|
+
A Doctype defines schema, links, workflow, and actions.
|
|
173
|
+
|
|
174
|
+
- **Schema** describes the data model and field layout — used by AForm for rendering.
|
|
175
|
+
- **Links** declare relationships to other doctypes with cardinality and direction (`noneOrMany`, `atMostOne`, etc.).
|
|
176
|
+
- **Workflow** is an XState machine config expressing the states and transitions a record can go through.
|
|
177
|
+
- **Actions** are an ordered map of named functions, triggered by field changes (lowercase keys) or FSM transitions (UPPERCASE keys).
|
|
178
|
+
- **Registry** is the singleton catalog — all doctypes live here. Optional Vue Router integration allows automatic route creation per doctype.
|
|
179
|
+
- **`useStonecrop()`** is the Vue composable that wires components to HST and provides `formData`, `provideHSTPath`, `handleHSTChange`, and the operation log API.
|
|
140
180
|
|
|
141
181
|
The data model is **two operations**: get data and run actions. There is no CRUD. Records change state through FSM transitions; those transitions have side effects (persistence, notifications, etc.) defined in action handlers registered by the application. The framework provides the pipeline; applications define what actions exist and what they do.
|
|
142
182
|
|
|
@@ -152,16 +192,19 @@ doctype.recordId.nested.field // deep nesting supported
|
|
|
152
192
|
## Core Requirements
|
|
153
193
|
|
|
154
194
|
### 1. Data Structure Compatibility
|
|
195
|
+
|
|
155
196
|
- **Vue Reactive Objects**: Must work seamlessly with `reactive()`, `ref()`, and `computed()` primitives
|
|
156
197
|
- **Pinia Store Integration**: Compatible with both Options API and Composition API Pinia stores
|
|
157
198
|
- **Immutable Objects**: Support for frozen/immutable configuration objects without breaking reactivity
|
|
158
199
|
|
|
159
200
|
### 2. Path-Based Addressing System
|
|
201
|
+
|
|
160
202
|
- **Dot Notation**: Full support for dot-notation paths (e.g., `"users.123.profile.settings"`)
|
|
161
203
|
- **Dynamic Paths**: Support for programmatically generated path strings (particularly component to HST)
|
|
162
204
|
|
|
163
205
|
### 3. Hierarchical Navigation
|
|
164
|
-
|
|
206
|
+
|
|
207
|
+
- **Ancestor/Descendant Relationships**: Maintain bidirectional ancestor-descendant references
|
|
165
208
|
- **Sibling Access**: Efficient navigation between sibling nodes
|
|
166
209
|
- **Root Access**: Always accessible reference to tree root from any node
|
|
167
210
|
- **Depth Tracking**: Know the depth level of any node in the hierarchy
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useStonecrop } from './composables/stonecrop';
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { computed, inject, ref } from 'vue';
|
|
2
|
+
import { Stonecrop } from '../stonecrop';
|
|
3
|
+
/**
|
|
4
|
+
* Get the lazy link state for a specific link field on a doctype record.
|
|
5
|
+
*
|
|
6
|
+
* This composable provides reactive state for lazy-loaded links:
|
|
7
|
+
* - `loading`: true while fetching
|
|
8
|
+
* - `loaded`: true after successful fetch (permanent until reload)
|
|
9
|
+
* - `error`: error state if any
|
|
10
|
+
* - `reload()`: explicitly trigger a fetch
|
|
11
|
+
* - `data`: computed from HST, or undefined if not loaded
|
|
12
|
+
*
|
|
13
|
+
* The reload() function respects the link's fetch strategy:
|
|
14
|
+
* - `sync`: fetches via GraphQL query through fetchNestedData
|
|
15
|
+
* - `lazy`: fetches via GraphQL query through fetchNestedData
|
|
16
|
+
* - `custom`: invokes the serialized handler function directly
|
|
17
|
+
*
|
|
18
|
+
* @param doctype - The doctype instance
|
|
19
|
+
* @param recordId - The record ID
|
|
20
|
+
* @param linkFieldname - The link fieldname to load
|
|
21
|
+
* @returns LazyLink with loading, loaded, error, reload, and data
|
|
22
|
+
* @public
|
|
23
|
+
*/
|
|
24
|
+
export function useLazyLink(doctype, recordId, linkFieldname) {
|
|
25
|
+
const stonecropInstance = inject('$stonecrop') || Stonecrop._root;
|
|
26
|
+
if (!stonecropInstance) {
|
|
27
|
+
throw new Error('Stonecrop instance not available. Ensure useStonecrop() has been called first.');
|
|
28
|
+
}
|
|
29
|
+
const loading = ref(false);
|
|
30
|
+
const loaded = ref(false);
|
|
31
|
+
const error = ref(null);
|
|
32
|
+
const hstStore = stonecropInstance.getStore();
|
|
33
|
+
/**
|
|
34
|
+
* Build the HST path for a lazy link field
|
|
35
|
+
*/
|
|
36
|
+
const getLinkPath = () => {
|
|
37
|
+
const slug = doctype.slug || doctype.doctype;
|
|
38
|
+
return `${slug}.${recordId}.${linkFieldname}`;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Get the link declaration from the doctype schema
|
|
42
|
+
*/
|
|
43
|
+
const getLinkDeclaration = () => {
|
|
44
|
+
return doctype.links?.[linkFieldname]?.fetch;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Invoke a custom fetch handler
|
|
48
|
+
* The handler is a serialized function string that we execute via new Function()
|
|
49
|
+
*/
|
|
50
|
+
const invokeCustomHandler = async (handler) => {
|
|
51
|
+
try {
|
|
52
|
+
// Create function from serialized string and invoke it
|
|
53
|
+
// The function receives the stonecrop instance and path as parameters
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
55
|
+
const fn = new Function('stonecrop', 'path', 'hst', `
|
|
56
|
+
return (${handler})(stonecrop, path, hst)
|
|
57
|
+
`);
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
59
|
+
return await fn(stonecropInstance, getLinkPath(), hstStore);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
throw new Error(`Custom handler failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Fetch the link data using the appropriate strategy
|
|
67
|
+
*/
|
|
68
|
+
const fetchLinkData = async () => {
|
|
69
|
+
const linkFetch = getLinkDeclaration();
|
|
70
|
+
const ancestorPath = `${doctype.slug || doctype.doctype}.${recordId}`;
|
|
71
|
+
if (linkFetch?.method === 'custom') {
|
|
72
|
+
// Ensure ancestor path exists before invoking custom handler
|
|
73
|
+
if (!hstStore.has(ancestorPath)) {
|
|
74
|
+
hstStore.set(ancestorPath, {}, 'system');
|
|
75
|
+
}
|
|
76
|
+
// Custom handler - invoke directly
|
|
77
|
+
const result = await invokeCustomHandler(linkFetch.handler);
|
|
78
|
+
// Store result in HST at the link path
|
|
79
|
+
hstStore.set(getLinkPath(), result, 'system');
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
// sync or lazy (both use fetchNestedData but with different includeNested)
|
|
83
|
+
// For lazy links, we still use fetchNestedData but only for this specific link
|
|
84
|
+
await stonecropInstance.fetchNestedData(ancestorPath, doctype, recordId, { includeNested: [linkFieldname] });
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Explicitly reload the lazy link data
|
|
89
|
+
*/
|
|
90
|
+
const reload = async () => {
|
|
91
|
+
if (loading.value)
|
|
92
|
+
return;
|
|
93
|
+
loading.value = true;
|
|
94
|
+
error.value = null;
|
|
95
|
+
try {
|
|
96
|
+
await fetchLinkData();
|
|
97
|
+
loaded.value = true;
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
error.value = err instanceof Error ? err : new Error(String(err));
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
loading.value = false;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Computed property that returns the loaded data from HST
|
|
109
|
+
*/
|
|
110
|
+
const data = computed(() => {
|
|
111
|
+
if (!loaded.value)
|
|
112
|
+
return undefined;
|
|
113
|
+
return hstStore.get(getLinkPath());
|
|
114
|
+
});
|
|
115
|
+
return {
|
|
116
|
+
// State
|
|
117
|
+
loading,
|
|
118
|
+
loaded,
|
|
119
|
+
error,
|
|
120
|
+
// Computed
|
|
121
|
+
data,
|
|
122
|
+
// Actions
|
|
123
|
+
reload,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -21,6 +21,21 @@ export function useStonecrop(options) {
|
|
|
21
21
|
const isLoading = ref(false);
|
|
22
22
|
const error = ref(null);
|
|
23
23
|
const resolvedDoctype = ref();
|
|
24
|
+
// Workflow readiness computed properties
|
|
25
|
+
const isWorkflowReady = computed(() => {
|
|
26
|
+
if (!stonecrop.value || !resolvedDoctype.value || !options.recordId || options.recordId === 'new') {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
const status = stonecrop.value.isWorkflowReady(resolvedDoctype.value, options.recordId);
|
|
30
|
+
return status.ready;
|
|
31
|
+
});
|
|
32
|
+
const blockedLinks = computed(() => {
|
|
33
|
+
if (!stonecrop.value || !resolvedDoctype.value || !options.recordId || options.recordId === 'new') {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const status = stonecrop.value.isWorkflowReady(resolvedDoctype.value, options.recordId);
|
|
37
|
+
return status.blockedLinks ?? [];
|
|
38
|
+
});
|
|
24
39
|
// Initialize stonecrop instance synchronously using singleton pattern
|
|
25
40
|
// Use injected instance if available, otherwise fall back to the singleton root
|
|
26
41
|
const stonecropInstance = providedStonecrop || Stonecrop._root;
|
|
@@ -31,7 +46,7 @@ export function useStonecrop(options) {
|
|
|
31
46
|
if (options?.doctype && typeof options.doctype !== 'string') {
|
|
32
47
|
resolvedDoctype.value = options.doctype;
|
|
33
48
|
}
|
|
34
|
-
// Operation log state and methods
|
|
49
|
+
// Operation log state and methods
|
|
35
50
|
const operations = ref([]);
|
|
36
51
|
const currentIndex = ref(-1);
|
|
37
52
|
const canUndo = computed(() => stonecrop.value?.getOperationLogStore().canUndo ?? false);
|
|
@@ -85,12 +100,9 @@ export function useStonecrop(options) {
|
|
|
85
100
|
const configure = (config) => {
|
|
86
101
|
stonecrop.value?.getOperationLogStore().configure(config);
|
|
87
102
|
};
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
// Set up reactive refs from operation log store - only if Pinia is available
|
|
103
|
+
// Wire operation log reactive state synchronously — no lifecycle hook needed.
|
|
104
|
+
// storeToRefs and watch are both safe to call in setup() body.
|
|
105
|
+
if (registry && stonecrop.value) {
|
|
94
106
|
try {
|
|
95
107
|
const opLogStore = stonecrop.value.getOperationLogStore();
|
|
96
108
|
const opLogRefs = storeToRefs(opLogStore);
|
|
@@ -105,8 +117,29 @@ export function useStonecrop(options) {
|
|
|
105
117
|
});
|
|
106
118
|
}
|
|
107
119
|
catch {
|
|
108
|
-
// Pinia not available
|
|
109
|
-
|
|
120
|
+
// Pinia not available — operation log is optional, silently skip
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Synchronous HST initialisation for an explicit Doctype instance.
|
|
124
|
+
// When the caller passes a Doctype object (not a slug string), every piece of
|
|
125
|
+
// setup that doesn't require network I/O runs here during setup() so that
|
|
126
|
+
// hstStore, resolvedSchema, and formData are populated before the first render
|
|
127
|
+
// and are immediately available to callers without any await.
|
|
128
|
+
if (options.doctype && typeof options.doctype !== 'string' && registry && stonecrop.value) {
|
|
129
|
+
hstStore.value = stonecrop.value.getStore();
|
|
130
|
+
resolvedSchema.value = registry.resolveSchema(options.doctype);
|
|
131
|
+
if (!options.recordId || options.recordId === 'new') {
|
|
132
|
+
formData.value = registry.initializeRecord(resolvedSchema.value);
|
|
133
|
+
}
|
|
134
|
+
if (hstStore.value) {
|
|
135
|
+
setupDeepReactivity(options.doctype, options.recordId || 'new', formData, hstStore.value);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// onMounted handles only work that is genuinely async: lazy-loading a doctype
|
|
139
|
+
// by slug, fetching an existing record from the server, and router-based setup.
|
|
140
|
+
onMounted(async () => {
|
|
141
|
+
if (!registry || !stonecrop.value) {
|
|
142
|
+
return;
|
|
110
143
|
}
|
|
111
144
|
// Handle router-based setup if no specific doctype provided
|
|
112
145
|
if (!options.doctype && registry.router) {
|
|
@@ -132,12 +165,7 @@ export function useStonecrop(options) {
|
|
|
132
165
|
hstStore.value = stonecrop.value.getStore();
|
|
133
166
|
// Resolve schema for router-loaded doctype
|
|
134
167
|
if (registry) {
|
|
135
|
-
|
|
136
|
-
? Array.isArray(doctype.schema)
|
|
137
|
-
? doctype.schema
|
|
138
|
-
: Array.from(doctype.schema)
|
|
139
|
-
: [];
|
|
140
|
-
resolvedSchema.value = registry.resolveSchema(schemaArray);
|
|
168
|
+
resolvedSchema.value = registry.resolveSchema(doctype);
|
|
141
169
|
}
|
|
142
170
|
if (recordId && recordId !== 'new') {
|
|
143
171
|
const existingRecord = stonecrop.value.getRecordById(doctype, recordId);
|
|
@@ -169,15 +197,14 @@ export function useStonecrop(options) {
|
|
|
169
197
|
}
|
|
170
198
|
// Handle HST integration if doctype is provided explicitly
|
|
171
199
|
if (options.doctype) {
|
|
172
|
-
hstStore.value = stonecrop.value.getStore();
|
|
173
200
|
const recordId = options.recordId;
|
|
174
|
-
// Resolve doctype - handle string (lazy-load) or Doctype instance
|
|
175
|
-
let doctype;
|
|
176
201
|
if (typeof options.doctype === 'string') {
|
|
177
|
-
// String doctype
|
|
202
|
+
// String doctype — resolve lazily, then do full sync-equivalent setup here.
|
|
178
203
|
const doctypeSlug = options.doctype;
|
|
204
|
+
hstStore.value = stonecrop.value.getStore();
|
|
179
205
|
isLoading.value = true;
|
|
180
206
|
error.value = null;
|
|
207
|
+
let doctype;
|
|
181
208
|
try {
|
|
182
209
|
// Check if already in registry
|
|
183
210
|
doctype = registry.getDoctype(doctypeSlug);
|
|
@@ -202,47 +229,57 @@ export function useStonecrop(options) {
|
|
|
202
229
|
finally {
|
|
203
230
|
isLoading.value = false;
|
|
204
231
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
// Error already set above, just return
|
|
214
|
-
return;
|
|
215
|
-
}
|
|
216
|
-
// Resolve schema for the doctype
|
|
217
|
-
const schemaArray = doctype.schema
|
|
218
|
-
? Array.isArray(doctype.schema)
|
|
219
|
-
? doctype.schema
|
|
220
|
-
: Array.from(doctype.schema)
|
|
221
|
-
: [];
|
|
222
|
-
resolvedSchema.value = registry.resolveSchema(schemaArray);
|
|
223
|
-
if (recordId && recordId !== 'new') {
|
|
224
|
-
const existingRecord = stonecrop.value.getRecordById(doctype, recordId);
|
|
225
|
-
if (existingRecord) {
|
|
226
|
-
formData.value = existingRecord.get('') || {};
|
|
227
|
-
}
|
|
228
|
-
else {
|
|
229
|
-
try {
|
|
230
|
-
await stonecrop.value.getRecord(doctype, recordId);
|
|
231
|
-
const loadedRecord = stonecrop.value.getRecordById(doctype, recordId);
|
|
232
|
-
if (loadedRecord) {
|
|
233
|
-
formData.value = loadedRecord.get('') || {};
|
|
234
|
-
}
|
|
232
|
+
resolvedDoctype.value = doctype;
|
|
233
|
+
if (!doctype)
|
|
234
|
+
return;
|
|
235
|
+
resolvedSchema.value = registry.resolveSchema(doctype);
|
|
236
|
+
if (recordId && recordId !== 'new') {
|
|
237
|
+
const existingRecord = stonecrop.value.getRecordById(doctype, recordId);
|
|
238
|
+
if (existingRecord) {
|
|
239
|
+
formData.value = existingRecord.get('') || {};
|
|
235
240
|
}
|
|
236
|
-
|
|
237
|
-
|
|
241
|
+
else {
|
|
242
|
+
try {
|
|
243
|
+
await stonecrop.value.getRecord(doctype, recordId);
|
|
244
|
+
const loadedRecord = stonecrop.value.getRecordById(doctype, recordId);
|
|
245
|
+
if (loadedRecord) {
|
|
246
|
+
formData.value = loadedRecord.get('') || {};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
formData.value = registry.initializeRecord(resolvedSchema.value);
|
|
251
|
+
}
|
|
238
252
|
}
|
|
239
253
|
}
|
|
254
|
+
else {
|
|
255
|
+
formData.value = registry.initializeRecord(resolvedSchema.value);
|
|
256
|
+
}
|
|
257
|
+
if (hstStore.value) {
|
|
258
|
+
setupDeepReactivity(doctype, recordId || 'new', formData, hstStore.value);
|
|
259
|
+
}
|
|
240
260
|
}
|
|
241
261
|
else {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
262
|
+
// Doctype instance — sync init was done during setup().
|
|
263
|
+
// Only handle the async path: fetching an existing record from the server.
|
|
264
|
+
if (recordId && recordId !== 'new') {
|
|
265
|
+
const doctype = options.doctype;
|
|
266
|
+
const existingRecord = stonecrop.value.getRecordById(doctype, recordId);
|
|
267
|
+
if (existingRecord) {
|
|
268
|
+
formData.value = existingRecord.get('') || {};
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
try {
|
|
272
|
+
await stonecrop.value.getRecord(doctype, recordId);
|
|
273
|
+
const loadedRecord = stonecrop.value.getRecordById(doctype, recordId);
|
|
274
|
+
if (loadedRecord) {
|
|
275
|
+
formData.value = loadedRecord.get('') || {};
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
formData.value = registry.initializeRecord(resolvedSchema.value);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
246
283
|
}
|
|
247
284
|
}
|
|
248
285
|
});
|
|
@@ -302,18 +339,30 @@ export function useStonecrop(options) {
|
|
|
302
339
|
provide('hstChangeHandler', handleHSTChange);
|
|
303
340
|
}
|
|
304
341
|
/**
|
|
305
|
-
*
|
|
306
|
-
* Delegates to Stonecrop.
|
|
307
|
-
* @param
|
|
308
|
-
* @param
|
|
309
|
-
|
|
310
|
-
|
|
342
|
+
* Scaffold empty descendant records from defaults for all descendant links.
|
|
343
|
+
* Delegates to Stonecrop.initializeNestedData method.
|
|
344
|
+
* @param path - The HST path where initialized data should be stored
|
|
345
|
+
* @param doctype - The doctype to initialize
|
|
346
|
+
*/
|
|
347
|
+
const initializeNestedData = (path, doctype) => {
|
|
348
|
+
if (!stonecrop.value) {
|
|
349
|
+
throw new Error('Stonecrop instance not available');
|
|
350
|
+
}
|
|
351
|
+
return stonecrop.value.initializeNestedData(path, doctype);
|
|
352
|
+
};
|
|
353
|
+
/**
|
|
354
|
+
* Fetch a record and its nested data from the server.
|
|
355
|
+
* Delegates to Stonecrop.fetchNestedData method.
|
|
356
|
+
* @param path - The HST path (e.g., "recipe.r1")
|
|
357
|
+
* @param doctype - The doctype to fetch
|
|
358
|
+
* @param recordId - Record ID to fetch
|
|
359
|
+
* @param options - Query options (includeNested to control which links are fetched)
|
|
311
360
|
*/
|
|
312
|
-
const
|
|
361
|
+
const fetchNestedData = async (path, doctype, recordId, options) => {
|
|
313
362
|
if (!stonecrop.value) {
|
|
314
363
|
throw new Error('Stonecrop instance not available');
|
|
315
364
|
}
|
|
316
|
-
return stonecrop.value.
|
|
365
|
+
return stonecrop.value.fetchNestedData(path, doctype, recordId, options);
|
|
317
366
|
};
|
|
318
367
|
/**
|
|
319
368
|
* Collect a record payload with all nested doctype fields from HST
|
|
@@ -329,12 +378,12 @@ export function useStonecrop(options) {
|
|
|
329
378
|
return stonecrop.value.collectRecordPayload(doctype, recordId);
|
|
330
379
|
};
|
|
331
380
|
/**
|
|
332
|
-
* Create a nested context for
|
|
381
|
+
* Create a nested context for descendant forms
|
|
333
382
|
* @param basePath - The base path for the nested context (e.g., "customer.123.address")
|
|
334
|
-
* @param
|
|
383
|
+
* @param _descendantDoctype - The descendant doctype metadata (unused but kept for API consistency)
|
|
335
384
|
* @returns Object with scoped provideHSTPath and handleHSTChange
|
|
336
385
|
*/
|
|
337
|
-
const createNestedContext = (basePath,
|
|
386
|
+
const createNestedContext = (basePath, _descendantDoctype) => {
|
|
338
387
|
const nestedProvideHSTPath = (fieldname) => {
|
|
339
388
|
return `${basePath}.${fieldname}`;
|
|
340
389
|
};
|
|
@@ -383,12 +432,15 @@ export function useStonecrop(options) {
|
|
|
383
432
|
hstStore,
|
|
384
433
|
formData,
|
|
385
434
|
resolvedSchema,
|
|
386
|
-
|
|
435
|
+
initializeNestedData,
|
|
436
|
+
fetchNestedData,
|
|
387
437
|
collectRecordPayload,
|
|
388
438
|
createNestedContext,
|
|
389
439
|
isLoading,
|
|
390
440
|
error,
|
|
391
441
|
resolvedDoctype,
|
|
442
|
+
isWorkflowReady,
|
|
443
|
+
blockedLinks,
|
|
392
444
|
};
|
|
393
445
|
}
|
|
394
446
|
else if (!options.doctype && registry?.router) {
|
|
@@ -401,12 +453,15 @@ export function useStonecrop(options) {
|
|
|
401
453
|
hstStore,
|
|
402
454
|
formData,
|
|
403
455
|
resolvedSchema,
|
|
404
|
-
|
|
456
|
+
initializeNestedData,
|
|
457
|
+
fetchNestedData,
|
|
405
458
|
collectRecordPayload,
|
|
406
459
|
createNestedContext,
|
|
407
460
|
isLoading,
|
|
408
461
|
error,
|
|
409
462
|
resolvedDoctype,
|
|
463
|
+
isWorkflowReady,
|
|
464
|
+
blockedLinks,
|
|
410
465
|
};
|
|
411
466
|
}
|
|
412
467
|
// No doctype and no router - basic mode
|