@stonecrop/stonecrop 0.4.37 → 0.6.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 +92 -3
- package/dist/src/composable.d.ts +74 -8
- package/dist/src/composable.d.ts.map +1 -1
- package/dist/src/composable.js +348 -0
- package/dist/src/composables/operation-log.d.ts +136 -0
- package/dist/src/composables/operation-log.d.ts.map +1 -0
- package/dist/src/composables/operation-log.js +221 -0
- package/dist/src/doctype.d.ts +9 -1
- package/dist/src/doctype.d.ts.map +1 -1
- package/dist/{doctype.js → src/doctype.js} +9 -3
- package/dist/src/field-triggers.d.ts +178 -0
- package/dist/src/field-triggers.d.ts.map +1 -0
- package/dist/src/field-triggers.js +564 -0
- package/dist/src/index.d.ts +12 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +18 -0
- package/dist/src/plugins/index.d.ts +11 -13
- package/dist/src/plugins/index.d.ts.map +1 -1
- package/dist/src/plugins/index.js +90 -0
- package/dist/src/registry.d.ts +9 -3
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/{registry.js → src/registry.js} +14 -1
- package/dist/src/stonecrop.d.ts +350 -114
- package/dist/src/stonecrop.d.ts.map +1 -1
- package/dist/src/stonecrop.js +251 -0
- package/dist/src/stores/hst.d.ts +157 -0
- package/dist/src/stores/hst.d.ts.map +1 -0
- package/dist/src/stores/hst.js +483 -0
- package/dist/src/stores/index.d.ts +5 -1
- package/dist/src/stores/index.d.ts.map +1 -1
- package/dist/{stores → src/stores}/index.js +4 -1
- package/dist/src/stores/operation-log.d.ts +268 -0
- package/dist/src/stores/operation-log.d.ts.map +1 -0
- package/dist/src/stores/operation-log.js +571 -0
- package/dist/src/types/field-triggers.d.ts +186 -0
- package/dist/src/types/field-triggers.d.ts.map +1 -0
- package/dist/src/types/field-triggers.js +4 -0
- package/dist/src/types/index.d.ts +13 -2
- 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 +165 -0
- package/dist/src/types/operation-log.d.ts.map +1 -0
- package/dist/src/types/registry.d.ts +11 -0
- package/dist/src/types/registry.d.ts.map +1 -0
- package/dist/src/types/registry.js +0 -0
- package/dist/stonecrop.d.ts +1555 -159
- package/dist/stonecrop.js +1974 -7028
- package/dist/stonecrop.js.map +1 -1
- package/dist/stonecrop.umd.cjs +4 -8
- package/dist/stonecrop.umd.cjs.map +1 -1
- 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/package.json +6 -5
- package/src/composable.ts +481 -31
- package/src/composables/operation-log.ts +254 -0
- package/src/doctype.ts +9 -3
- package/src/field-triggers.ts +671 -0
- package/src/index.ts +50 -4
- package/src/plugins/index.ts +70 -22
- package/src/registry.ts +18 -3
- package/src/stonecrop.ts +246 -155
- package/src/stores/hst.ts +703 -0
- package/src/stores/index.ts +6 -1
- package/src/stores/operation-log.ts +671 -0
- package/src/types/field-triggers.ts +201 -0
- package/src/types/index.ts +17 -6
- package/src/types/operation-log.ts +205 -0
- package/src/types/registry.ts +10 -0
- package/dist/composable.js +0 -50
- package/dist/index.js +0 -6
- package/dist/plugins/index.js +0 -49
- package/dist/src/stores/data.d.ts +0 -11
- package/dist/src/stores/data.d.ts.map +0 -1
- package/dist/stores/data.js +0 -7
- package/src/stores/data.ts +0 -8
- /package/dist/{exceptions.js → src/exceptions.js} +0 -0
- /package/dist/{types/index.js → src/types/operation-log.js} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
# Stonecrop
|
|
2
2
|
_This package is under active development / design._
|
|
3
3
|
|
|
4
|
+
## Features
|
|
5
|
+
|
|
6
|
+
- **Hierarchical State Tree (HST)**: Advanced state management with tree navigation
|
|
7
|
+
- **Operation Log**: Global undo/redo with time-travel debugging, automatic FSM transition tracking, and action execution tracking
|
|
8
|
+
- **Action Tracking**: Audit trail for stateless action executions (print, email, archive, etc.)
|
|
9
|
+
- **Field Triggers**: Event-driven field actions integrated with XState
|
|
10
|
+
- **VueUse Integration**: Leverages battle-tested VueUse composables for keyboard shortcuts and persistence
|
|
11
|
+
|
|
12
|
+
## Installation & Usage
|
|
13
|
+
|
|
14
|
+
### Vue Plugin Installation
|
|
15
|
+
|
|
16
|
+
```typescript
|
|
17
|
+
import { createApp } from 'vue'
|
|
18
|
+
import Stonecrop from '@stonecrop/stonecrop'
|
|
19
|
+
|
|
20
|
+
const app = createApp(App)
|
|
21
|
+
|
|
22
|
+
// Install the Stonecrop plugin
|
|
23
|
+
app.use(Stonecrop, {
|
|
24
|
+
router,
|
|
25
|
+
components: {
|
|
26
|
+
// Register custom components
|
|
27
|
+
},
|
|
28
|
+
getMeta: async (doctype: string) => {
|
|
29
|
+
// Fetch doctype metadata from your API
|
|
30
|
+
return await fetchDoctypeMeta(doctype)
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
app.mount('#app')
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Available Imports
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
// Default export - Vue plugin
|
|
41
|
+
import Stonecrop from '@stonecrop/stonecrop'
|
|
42
|
+
|
|
43
|
+
// Named exports - utilities and classes
|
|
44
|
+
import {
|
|
45
|
+
Stonecrop as StonecropClass, // Core class
|
|
46
|
+
Registry, // Doctype registry
|
|
47
|
+
useStonecrop, // Vue composable
|
|
48
|
+
HST, // Hierarchical State Tree
|
|
49
|
+
createHST, // HST factory
|
|
50
|
+
DoctypeMeta // Doctype metadata class
|
|
51
|
+
} from '@stonecrop/stonecrop'
|
|
52
|
+
```### Using the Composable
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { useStonecrop } from '@stonecrop/stonecrop'
|
|
56
|
+
|
|
57
|
+
export default {
|
|
58
|
+
setup() {
|
|
59
|
+
const { stonecrop } = useStonecrop()
|
|
60
|
+
|
|
61
|
+
// Access HST store
|
|
62
|
+
const store = stonecrop.value?.getStore()
|
|
63
|
+
|
|
64
|
+
// Work with records
|
|
65
|
+
const records = stonecrop.value?.records('doctype')
|
|
66
|
+
const record = stonecrop.value?.getRecordById('doctype', recordId)
|
|
67
|
+
|
|
68
|
+
return { stonecrop, records, record }
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
4
73
|
## Design
|
|
5
74
|
A context will define schema, workflow and actions.
|
|
6
75
|
- Schema describes the data model and layout of the document.
|
|
@@ -27,11 +96,9 @@ app.doctype.schema.field.workflow <FSM>
|
|
|
27
96
|
app.doctype.schema.field.actions <OrderedSet>
|
|
28
97
|
app.doctype.schema.field.value <Store>
|
|
29
98
|
app.doctype.schema.field.value.field.value <Store> // a "sub-form"
|
|
30
|
-
app.doctype.schema.field.value.field[
|
|
99
|
+
app.doctype.schema.field.value.field['a:1'].value <Store> // also a "sub-form", representing a table
|
|
31
100
|
```
|
|
32
101
|
|
|
33
|
-
It may make sense to use [automatic injection aliasing](https://vuejs.org/guide/components/provide-inject.html#inject) at the doctype level
|
|
34
|
-
|
|
35
102
|
## Base Classes
|
|
36
103
|
The Doctype aligns with a row, record or object in a database. It is required to specify its schema, a Finite State Machine that informs its workflow and a set of functions that are triggered by that FSM's state transitions.
|
|
37
104
|
|
|
@@ -60,3 +127,25 @@ Stem is a composable singleton that wraps Registry and provides application leve
|
|
|
60
127
|
- User can define `doctype` and schema from UI
|
|
61
128
|
- Fields are shown as rows in a table
|
|
62
129
|
- FSM is shown as an editable diagram that validates itself
|
|
130
|
+
|
|
131
|
+
___
|
|
132
|
+
|
|
133
|
+
# Hierarchical State Tree (HST) Interface Requirements
|
|
134
|
+
|
|
135
|
+
## Core Requirements
|
|
136
|
+
|
|
137
|
+
### 1. Data Structure Compatibility
|
|
138
|
+
- **Vue Reactive Objects**: Must work seamlessly with `reactive()`, `ref()`, and `computed()` primitives
|
|
139
|
+
- **Pinia Store Integration**: Compatible with both Options API and Composition API Pinia stores
|
|
140
|
+
- **Immutable Objects**: Support for frozen/immutable configuration objects without breaking reactivity
|
|
141
|
+
|
|
142
|
+
### 2. Path-Based Addressing System
|
|
143
|
+
- **Dot Notation**: Full support for dot-notation paths (e.g., `"users.123.profile.settings"`)
|
|
144
|
+
- **Dynamic Paths**: Support for programmatically generated path strings (particularly component to HST)
|
|
145
|
+
|
|
146
|
+
### 3. Hierarchical Navigation
|
|
147
|
+
- **Parent/Child Relationships**: Maintain bidirectional parent-child references
|
|
148
|
+
- **Sibling Access**: Efficient navigation between sibling nodes
|
|
149
|
+
- **Root Access**: Always accessible reference to tree root from any node
|
|
150
|
+
- **Depth Tracking**: Know the depth level of any node in the hierarchy
|
|
151
|
+
- **Breadcrumb Generation**: Generate full path breadcrumbs for any node
|
package/dist/src/composable.d.ts
CHANGED
|
@@ -1,19 +1,85 @@
|
|
|
1
|
-
import { Ref } from 'vue';
|
|
1
|
+
import { Ref, ComputedRef } from 'vue';
|
|
2
2
|
import Registry from './registry';
|
|
3
3
|
import { Stonecrop } from './stonecrop';
|
|
4
|
+
import DoctypeMeta from './doctype';
|
|
5
|
+
import type { HSTNode } from './stores/hst';
|
|
6
|
+
import type { HSTOperation, OperationLogConfig, OperationLogSnapshot } from './types/operation-log';
|
|
4
7
|
/**
|
|
5
|
-
*
|
|
8
|
+
* Operation Log API - nested object containing all operation log functionality
|
|
6
9
|
* @public
|
|
7
10
|
*/
|
|
8
|
-
export type
|
|
11
|
+
export type OperationLogAPI = {
|
|
12
|
+
operations: Ref<HSTOperation[]>;
|
|
13
|
+
currentIndex: Ref<number>;
|
|
14
|
+
undoRedoState: ComputedRef<{
|
|
15
|
+
canUndo: boolean;
|
|
16
|
+
canRedo: boolean;
|
|
17
|
+
undoCount: number;
|
|
18
|
+
redoCount: number;
|
|
19
|
+
currentIndex: number;
|
|
20
|
+
}>;
|
|
21
|
+
canUndo: ComputedRef<boolean>;
|
|
22
|
+
canRedo: ComputedRef<boolean>;
|
|
23
|
+
undoCount: ComputedRef<number>;
|
|
24
|
+
redoCount: ComputedRef<number>;
|
|
25
|
+
undo: (hstStore: HSTNode) => boolean;
|
|
26
|
+
redo: (hstStore: HSTNode) => boolean;
|
|
27
|
+
startBatch: () => void;
|
|
28
|
+
commitBatch: (description?: string) => string | null;
|
|
29
|
+
cancelBatch: () => void;
|
|
30
|
+
clear: () => void;
|
|
31
|
+
getOperationsFor: (doctype: string, recordId?: string) => HSTOperation[];
|
|
32
|
+
getSnapshot: () => OperationLogSnapshot;
|
|
33
|
+
markIrreversible: (operationId: string, reason: string) => void;
|
|
34
|
+
logAction: (doctype: string, actionName: string, recordIds?: string[], result?: 'success' | 'failure' | 'pending', error?: string) => string;
|
|
35
|
+
configure: (options: Partial<OperationLogConfig>) => void;
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Base Stonecrop composable return type - includes operation log functionality
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
41
|
+
export type BaseStonecropReturn = {
|
|
9
42
|
stonecrop: Ref<Stonecrop | undefined>;
|
|
43
|
+
operationLog: OperationLogAPI;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* HST-enabled Stonecrop composable return type
|
|
47
|
+
* @public
|
|
48
|
+
*/
|
|
49
|
+
export type HSTStonecropReturn = BaseStonecropReturn & {
|
|
50
|
+
provideHSTPath: (fieldname: string, recordId?: string) => string;
|
|
51
|
+
handleHSTChange: (changeData: HSTChangeData) => void;
|
|
52
|
+
hstStore: Ref<HSTNode | undefined>;
|
|
53
|
+
formData: Ref<Record<string, any>>;
|
|
10
54
|
};
|
|
11
55
|
/**
|
|
12
|
-
*
|
|
13
|
-
* @
|
|
14
|
-
|
|
15
|
-
|
|
56
|
+
* HST Change data structure
|
|
57
|
+
* @public
|
|
58
|
+
*/
|
|
59
|
+
export type HSTChangeData = {
|
|
60
|
+
path: string;
|
|
61
|
+
value: any;
|
|
62
|
+
fieldname: string;
|
|
63
|
+
recordId?: string;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Unified Stonecrop composable - handles both general operations and HST reactive integration
|
|
67
|
+
*
|
|
68
|
+
* @param options - Configuration options for the composable
|
|
69
|
+
* @returns Stonecrop instance and optional HST integration utilities
|
|
70
|
+
* @public
|
|
71
|
+
*/
|
|
72
|
+
export declare function useStonecrop(): BaseStonecropReturn | HSTStonecropReturn;
|
|
73
|
+
/**
|
|
74
|
+
* Unified Stonecrop composable with HST integration for a specific doctype and record
|
|
75
|
+
*
|
|
76
|
+
* @param options - Configuration with doctype and optional recordId
|
|
77
|
+
* @returns Stonecrop instance with full HST integration utilities
|
|
16
78
|
* @public
|
|
17
79
|
*/
|
|
18
|
-
export declare function useStonecrop(
|
|
80
|
+
export declare function useStonecrop(options: {
|
|
81
|
+
registry?: Registry;
|
|
82
|
+
doctype: DoctypeMeta;
|
|
83
|
+
recordId?: string;
|
|
84
|
+
}): HSTStonecropReturn;
|
|
19
85
|
//# sourceMappingURL=composable.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composable.d.ts","sourceRoot":"","sources":["../../src/composable.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"composable.d.ts","sourceRoot":"","sources":["../../src/composable.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// src/composable.ts
|
|
2
|
+
import { inject, onMounted, ref, watch, provide, computed } from 'vue';
|
|
3
|
+
import { Stonecrop } from './stonecrop';
|
|
4
|
+
import { storeToRefs } from 'pinia';
|
|
5
|
+
/**
|
|
6
|
+
* @public
|
|
7
|
+
*/
|
|
8
|
+
export function useStonecrop(options) {
|
|
9
|
+
if (!options)
|
|
10
|
+
options = {};
|
|
11
|
+
const registry = options.registry || inject('$registry');
|
|
12
|
+
const providedStonecrop = inject('$stonecrop');
|
|
13
|
+
const stonecrop = ref();
|
|
14
|
+
const hstStore = ref();
|
|
15
|
+
const formData = ref({});
|
|
16
|
+
// Use refs for router-loaded doctype to maintain reactivity
|
|
17
|
+
const routerDoctype = ref();
|
|
18
|
+
const routerRecordId = ref();
|
|
19
|
+
// Operation log state and methods - will be populated after stonecrop instance is created
|
|
20
|
+
const operations = ref([]);
|
|
21
|
+
const currentIndex = ref(-1);
|
|
22
|
+
const canUndo = computed(() => stonecrop.value?.getOperationLogStore().canUndo ?? false);
|
|
23
|
+
const canRedo = computed(() => stonecrop.value?.getOperationLogStore().canRedo ?? false);
|
|
24
|
+
const undoCount = computed(() => stonecrop.value?.getOperationLogStore().undoCount ?? 0);
|
|
25
|
+
const redoCount = computed(() => stonecrop.value?.getOperationLogStore().redoCount ?? 0);
|
|
26
|
+
const undoRedoState = computed(() => stonecrop.value?.getOperationLogStore().undoRedoState ?? {
|
|
27
|
+
canUndo: false,
|
|
28
|
+
canRedo: false,
|
|
29
|
+
undoCount: 0,
|
|
30
|
+
redoCount: 0,
|
|
31
|
+
currentIndex: -1,
|
|
32
|
+
});
|
|
33
|
+
// Operation log methods
|
|
34
|
+
const undo = (hstStore) => {
|
|
35
|
+
return stonecrop.value?.getOperationLogStore().undo(hstStore) ?? false;
|
|
36
|
+
};
|
|
37
|
+
const redo = (hstStore) => {
|
|
38
|
+
return stonecrop.value?.getOperationLogStore().redo(hstStore) ?? false;
|
|
39
|
+
};
|
|
40
|
+
const startBatch = () => {
|
|
41
|
+
stonecrop.value?.getOperationLogStore().startBatch();
|
|
42
|
+
};
|
|
43
|
+
const commitBatch = (description) => {
|
|
44
|
+
return stonecrop.value?.getOperationLogStore().commitBatch(description) ?? null;
|
|
45
|
+
};
|
|
46
|
+
const cancelBatch = () => {
|
|
47
|
+
stonecrop.value?.getOperationLogStore().cancelBatch();
|
|
48
|
+
};
|
|
49
|
+
const clear = () => {
|
|
50
|
+
stonecrop.value?.getOperationLogStore().clear();
|
|
51
|
+
};
|
|
52
|
+
const getOperationsFor = (doctype, recordId) => {
|
|
53
|
+
return stonecrop.value?.getOperationLogStore().getOperationsFor(doctype, recordId) ?? [];
|
|
54
|
+
};
|
|
55
|
+
const getSnapshot = () => {
|
|
56
|
+
return (stonecrop.value?.getOperationLogStore().getSnapshot() ?? {
|
|
57
|
+
operations: [],
|
|
58
|
+
currentIndex: -1,
|
|
59
|
+
totalOperations: 0,
|
|
60
|
+
reversibleOperations: 0,
|
|
61
|
+
irreversibleOperations: 0,
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
const markIrreversible = (operationId, reason) => {
|
|
65
|
+
stonecrop.value?.getOperationLogStore().markIrreversible(operationId, reason);
|
|
66
|
+
};
|
|
67
|
+
const logAction = (doctype, actionName, recordIds, result = 'success', error) => {
|
|
68
|
+
return stonecrop.value?.getOperationLogStore().logAction(doctype, actionName, recordIds, result, error) ?? '';
|
|
69
|
+
};
|
|
70
|
+
const configure = (config) => {
|
|
71
|
+
stonecrop.value?.getOperationLogStore().configure(config);
|
|
72
|
+
};
|
|
73
|
+
// Initialize Stonecrop instance
|
|
74
|
+
onMounted(async () => {
|
|
75
|
+
if (!registry) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
stonecrop.value = providedStonecrop || new Stonecrop(registry);
|
|
79
|
+
// Set up reactive refs from operation log store - only if Pinia is available
|
|
80
|
+
try {
|
|
81
|
+
const opLogStore = stonecrop.value.getOperationLogStore();
|
|
82
|
+
const opLogRefs = storeToRefs(opLogStore);
|
|
83
|
+
operations.value = opLogRefs.operations.value;
|
|
84
|
+
currentIndex.value = opLogRefs.currentIndex.value;
|
|
85
|
+
// Watch for changes in operation log state
|
|
86
|
+
watch(() => opLogRefs.operations.value, newOps => {
|
|
87
|
+
operations.value = newOps;
|
|
88
|
+
});
|
|
89
|
+
watch(() => opLogRefs.currentIndex.value, newIndex => {
|
|
90
|
+
currentIndex.value = newIndex;
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Pinia not available (e.g., in tests) - operation log features will not be available
|
|
95
|
+
// Silently fail - operation log is optional
|
|
96
|
+
}
|
|
97
|
+
// Handle router-based setup if no specific doctype provided
|
|
98
|
+
if (!options.doctype && registry.router) {
|
|
99
|
+
const route = registry.router.currentRoute.value;
|
|
100
|
+
// Parse route path - let the application determine the doctype from the route
|
|
101
|
+
if (!route.path)
|
|
102
|
+
return; // Early return if no path available
|
|
103
|
+
const pathSegments = route.path.split('/').filter(segment => segment.length > 0);
|
|
104
|
+
const recordId = pathSegments[1]?.toLowerCase();
|
|
105
|
+
if (pathSegments.length > 0) {
|
|
106
|
+
// Create route context for getMeta function
|
|
107
|
+
const routeContext = {
|
|
108
|
+
path: route.path,
|
|
109
|
+
segments: pathSegments,
|
|
110
|
+
};
|
|
111
|
+
const doctype = await registry.getMeta?.(routeContext);
|
|
112
|
+
if (doctype) {
|
|
113
|
+
registry.addDoctype(doctype);
|
|
114
|
+
stonecrop.value.setup(doctype);
|
|
115
|
+
// Set reactive refs for router-based doctype
|
|
116
|
+
routerDoctype.value = doctype;
|
|
117
|
+
routerRecordId.value = recordId;
|
|
118
|
+
hstStore.value = stonecrop.value.getStore();
|
|
119
|
+
if (recordId && recordId !== 'new') {
|
|
120
|
+
const existingRecord = stonecrop.value.getRecordById(doctype, recordId);
|
|
121
|
+
if (existingRecord) {
|
|
122
|
+
formData.value = existingRecord.get('') || {};
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
try {
|
|
126
|
+
await stonecrop.value.getRecord(doctype, recordId);
|
|
127
|
+
const loadedRecord = stonecrop.value.getRecordById(doctype, recordId);
|
|
128
|
+
if (loadedRecord) {
|
|
129
|
+
formData.value = loadedRecord.get('') || {};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
formData.value = initializeNewRecord(doctype);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
formData.value = initializeNewRecord(doctype);
|
|
139
|
+
}
|
|
140
|
+
if (hstStore.value) {
|
|
141
|
+
setupDeepReactivity(doctype, recordId || 'new', formData, hstStore.value);
|
|
142
|
+
}
|
|
143
|
+
stonecrop.value.runAction(doctype, 'load', recordId ? [recordId] : undefined);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Handle HST integration if doctype is provided explicitly
|
|
148
|
+
if (options.doctype) {
|
|
149
|
+
hstStore.value = stonecrop.value.getStore();
|
|
150
|
+
const doctype = options.doctype;
|
|
151
|
+
const recordId = options.recordId;
|
|
152
|
+
if (recordId && recordId !== 'new') {
|
|
153
|
+
const existingRecord = stonecrop.value.getRecordById(doctype, recordId);
|
|
154
|
+
if (existingRecord) {
|
|
155
|
+
formData.value = existingRecord.get('') || {};
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
try {
|
|
159
|
+
await stonecrop.value.getRecord(doctype, recordId);
|
|
160
|
+
const loadedRecord = stonecrop.value.getRecordById(doctype, recordId);
|
|
161
|
+
if (loadedRecord) {
|
|
162
|
+
formData.value = loadedRecord.get('') || {};
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
formData.value = initializeNewRecord(doctype);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
formData.value = initializeNewRecord(doctype);
|
|
172
|
+
}
|
|
173
|
+
if (hstStore.value) {
|
|
174
|
+
setupDeepReactivity(doctype, recordId || 'new', formData, hstStore.value);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
// HST integration functions - always created but only populated when HST is available
|
|
179
|
+
const provideHSTPath = (fieldname, customRecordId) => {
|
|
180
|
+
const doctype = options.doctype || routerDoctype.value;
|
|
181
|
+
if (!doctype)
|
|
182
|
+
return '';
|
|
183
|
+
const actualRecordId = customRecordId || options.recordId || routerRecordId.value || 'new';
|
|
184
|
+
return `${doctype.slug}.${actualRecordId}.${fieldname}`;
|
|
185
|
+
};
|
|
186
|
+
const handleHSTChange = (changeData) => {
|
|
187
|
+
const doctype = options.doctype || routerDoctype.value;
|
|
188
|
+
if (!hstStore.value || !stonecrop.value || !doctype) {
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const pathParts = changeData.path.split('.');
|
|
193
|
+
if (pathParts.length >= 2) {
|
|
194
|
+
const doctypeSlug = pathParts[0];
|
|
195
|
+
const recordId = pathParts[1];
|
|
196
|
+
if (!hstStore.value.has(`${doctypeSlug}.${recordId}`)) {
|
|
197
|
+
stonecrop.value.addRecord(doctype, recordId, { ...formData.value });
|
|
198
|
+
}
|
|
199
|
+
if (pathParts.length > 3) {
|
|
200
|
+
const recordPath = `${doctypeSlug}.${recordId}`;
|
|
201
|
+
const nestedParts = pathParts.slice(2);
|
|
202
|
+
let currentPath = recordPath;
|
|
203
|
+
for (let i = 0; i < nestedParts.length - 1; i++) {
|
|
204
|
+
currentPath += `.${nestedParts[i]}`;
|
|
205
|
+
if (!hstStore.value.has(currentPath)) {
|
|
206
|
+
const nextPart = nestedParts[i + 1];
|
|
207
|
+
const isArray = !isNaN(Number(nextPart));
|
|
208
|
+
hstStore.value.set(currentPath, isArray ? [] : {});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
hstStore.value.set(changeData.path, changeData.value);
|
|
214
|
+
const fieldParts = changeData.fieldname.split('.');
|
|
215
|
+
const newFormData = { ...formData.value };
|
|
216
|
+
if (fieldParts.length === 1) {
|
|
217
|
+
newFormData[fieldParts[0]] = changeData.value;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
updateNestedObject(newFormData, fieldParts, changeData.value);
|
|
221
|
+
}
|
|
222
|
+
formData.value = newFormData;
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
// Silently handle errors
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
// Provide injection tokens if HST will be available
|
|
229
|
+
if (options.doctype || registry?.router) {
|
|
230
|
+
provide('hstPathProvider', provideHSTPath);
|
|
231
|
+
provide('hstChangeHandler', handleHSTChange);
|
|
232
|
+
}
|
|
233
|
+
// Create operation log API object
|
|
234
|
+
const operationLog = {
|
|
235
|
+
operations,
|
|
236
|
+
currentIndex,
|
|
237
|
+
undoRedoState,
|
|
238
|
+
canUndo,
|
|
239
|
+
canRedo,
|
|
240
|
+
undoCount,
|
|
241
|
+
redoCount,
|
|
242
|
+
undo,
|
|
243
|
+
redo,
|
|
244
|
+
startBatch,
|
|
245
|
+
commitBatch,
|
|
246
|
+
cancelBatch,
|
|
247
|
+
clear,
|
|
248
|
+
getOperationsFor,
|
|
249
|
+
getSnapshot,
|
|
250
|
+
markIrreversible,
|
|
251
|
+
logAction,
|
|
252
|
+
configure,
|
|
253
|
+
};
|
|
254
|
+
// Always return HST functions if doctype is provided or will be loaded from router
|
|
255
|
+
if (options.doctype) {
|
|
256
|
+
// Explicit doctype - return HST immediately
|
|
257
|
+
return {
|
|
258
|
+
stonecrop,
|
|
259
|
+
operationLog,
|
|
260
|
+
provideHSTPath,
|
|
261
|
+
handleHSTChange,
|
|
262
|
+
hstStore,
|
|
263
|
+
formData,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
else if (!options.doctype && registry?.router) {
|
|
267
|
+
// Router-based - return HST (will be populated after mount)
|
|
268
|
+
return {
|
|
269
|
+
stonecrop,
|
|
270
|
+
operationLog,
|
|
271
|
+
provideHSTPath,
|
|
272
|
+
handleHSTChange,
|
|
273
|
+
hstStore,
|
|
274
|
+
formData,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
// No doctype and no router - basic mode
|
|
278
|
+
return {
|
|
279
|
+
stonecrop,
|
|
280
|
+
operationLog,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Initialize new record structure based on doctype schema
|
|
285
|
+
*/
|
|
286
|
+
function initializeNewRecord(doctype) {
|
|
287
|
+
const initialData = {};
|
|
288
|
+
if (!doctype.schema) {
|
|
289
|
+
return initialData;
|
|
290
|
+
}
|
|
291
|
+
doctype.schema.forEach(field => {
|
|
292
|
+
const fieldtype = 'fieldtype' in field ? field.fieldtype : 'Data';
|
|
293
|
+
switch (fieldtype) {
|
|
294
|
+
case 'Data':
|
|
295
|
+
case 'Text':
|
|
296
|
+
initialData[field.fieldname] = '';
|
|
297
|
+
break;
|
|
298
|
+
case 'Check':
|
|
299
|
+
initialData[field.fieldname] = false;
|
|
300
|
+
break;
|
|
301
|
+
case 'Int':
|
|
302
|
+
case 'Float':
|
|
303
|
+
initialData[field.fieldname] = 0;
|
|
304
|
+
break;
|
|
305
|
+
case 'Table':
|
|
306
|
+
initialData[field.fieldname] = [];
|
|
307
|
+
break;
|
|
308
|
+
case 'JSON':
|
|
309
|
+
initialData[field.fieldname] = {};
|
|
310
|
+
break;
|
|
311
|
+
default:
|
|
312
|
+
initialData[field.fieldname] = null;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
return initialData;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Setup deep reactivity between form data and HST store
|
|
319
|
+
*/
|
|
320
|
+
function setupDeepReactivity(doctype, recordId, formData, hstStore) {
|
|
321
|
+
watch(formData, newData => {
|
|
322
|
+
const recordPath = `${doctype.slug}.${recordId}`;
|
|
323
|
+
Object.keys(newData).forEach(fieldname => {
|
|
324
|
+
const path = `${recordPath}.${fieldname}`;
|
|
325
|
+
try {
|
|
326
|
+
hstStore.set(path, newData[fieldname]);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// Silently handle errors
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
}, { deep: true });
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Update nested object with dot-notation path
|
|
336
|
+
*/
|
|
337
|
+
function updateNestedObject(obj, path, value) {
|
|
338
|
+
let current = obj;
|
|
339
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
340
|
+
const key = path[i];
|
|
341
|
+
if (!(key in current) || typeof current[key] !== 'object') {
|
|
342
|
+
current[key] = isNaN(Number(path[i + 1])) ? {} : [];
|
|
343
|
+
}
|
|
344
|
+
current = current[key];
|
|
345
|
+
}
|
|
346
|
+
const finalKey = path[path.length - 1];
|
|
347
|
+
current[finalKey] = value;
|
|
348
|
+
}
|