@stonecrop/stonecrop 0.12.8 → 0.13.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/dist/composable.js +1 -0
- package/dist/composables/lazy-link.js +125 -0
- package/dist/composables/operation-log.js +224 -0
- package/dist/composables/stonecrop.js +504 -0
- package/dist/composables/use-lazy-link-state.js +125 -0
- package/dist/composables/use-stonecrop.js +476 -0
- package/dist/doctype.js +242 -0
- package/dist/exceptions.js +16 -0
- package/dist/field-triggers.js +575 -0
- package/dist/index.js +27 -0
- package/dist/operation-log-DB-dGNT9.js +593 -0
- package/dist/operation-log-DB-dGNT9.js.map +1 -0
- package/dist/plugins/index.js +99 -0
- package/dist/registry.js +423 -0
- package/dist/schema-validator.js +407 -0
- 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/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/stores/data.d.ts +11 -0
- package/dist/src/stores/data.d.ts.map +1 -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/utils.d.ts +24 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/stonecrop.css +1 -0
- 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 +496 -0
- package/dist/stores/index.js +12 -0
- package/dist/stores/operation-log.js +580 -0
- 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/composable.js +0 -0
- package/dist/types/doctype.js +0 -0
- package/dist/types/field-triggers.js +4 -0
- package/dist/types/hst.js +0 -0
- package/dist/types/index.js +10 -0
- package/dist/types/operation-log.js +0 -0
- package/dist/types/plugin.js +0 -0
- package/dist/types/registry.js +0 -0
- package/dist/types/schema-validator.js +13 -0
- package/dist/types/stonecrop.js +0 -0
- package/dist/utils.js +46 -0
- package/package.json +4 -4
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { useMagicKeys, whenever } from '@vueuse/core';
|
|
2
|
+
import { storeToRefs } from 'pinia';
|
|
3
|
+
import { getCurrentInstance, inject } from 'vue';
|
|
4
|
+
import { useOperationLogStore } from '../stores/operation-log';
|
|
5
|
+
/**
|
|
6
|
+
* Composable for operation log management
|
|
7
|
+
* Provides easy access to undo/redo functionality and operation history
|
|
8
|
+
*
|
|
9
|
+
* @param config - Optional configuration for the operation log
|
|
10
|
+
* @returns Operation log interface
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const { undo, redo, canUndo, canRedo, operations, configure } = useOperationLog()
|
|
15
|
+
*
|
|
16
|
+
* // Configure the log
|
|
17
|
+
* configure({
|
|
18
|
+
* maxOperations: 50,
|
|
19
|
+
* enableCrossTabSync: true,
|
|
20
|
+
* enablePersistence: true
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* // Undo/redo
|
|
24
|
+
* await undo(hstStore)
|
|
25
|
+
* await redo(hstStore)
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @public
|
|
29
|
+
*/
|
|
30
|
+
export function useOperationLog(config) {
|
|
31
|
+
// inject() is only valid inside a component setup() context. When this
|
|
32
|
+
// composable is called outside one (e.g. directly in test bodies or plain
|
|
33
|
+
// scripts) skip the injection entirely and fall back to the Pinia store.
|
|
34
|
+
const injectedStore = getCurrentInstance()
|
|
35
|
+
? inject('$operationLogStore', undefined)
|
|
36
|
+
: undefined;
|
|
37
|
+
const store = injectedStore || useOperationLogStore();
|
|
38
|
+
// Apply configuration if provided
|
|
39
|
+
if (config) {
|
|
40
|
+
store.configure(config);
|
|
41
|
+
}
|
|
42
|
+
// Extract reactive state
|
|
43
|
+
const { operations, currentIndex, undoRedoState, canUndo, canRedo, undoCount, redoCount } = storeToRefs(store);
|
|
44
|
+
/**
|
|
45
|
+
* Undo the last operation
|
|
46
|
+
*/
|
|
47
|
+
function undo(hstStore) {
|
|
48
|
+
return store.undo(hstStore);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Redo the next operation
|
|
52
|
+
*/
|
|
53
|
+
function redo(hstStore) {
|
|
54
|
+
return store.redo(hstStore);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Start a batch operation
|
|
58
|
+
*/
|
|
59
|
+
function startBatch() {
|
|
60
|
+
store.startBatch();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Commit the current batch
|
|
64
|
+
*/
|
|
65
|
+
function commitBatch(description) {
|
|
66
|
+
return store.commitBatch(description);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Cancel the current batch
|
|
70
|
+
*/
|
|
71
|
+
function cancelBatch() {
|
|
72
|
+
store.cancelBatch();
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Clear all operations
|
|
76
|
+
*/
|
|
77
|
+
function clear() {
|
|
78
|
+
store.clear();
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Get operations for a specific doctype/record
|
|
82
|
+
*/
|
|
83
|
+
function getOperationsFor(doctype, recordId) {
|
|
84
|
+
return store.getOperationsFor(doctype, recordId);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get a snapshot of the operation log
|
|
88
|
+
*/
|
|
89
|
+
function getSnapshot() {
|
|
90
|
+
return store.getSnapshot();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Mark an operation as irreversible
|
|
94
|
+
* @param operationId - The ID of the operation to mark
|
|
95
|
+
* @param reason - The reason why the operation is irreversible
|
|
96
|
+
*/
|
|
97
|
+
function markIrreversible(operationId, reason) {
|
|
98
|
+
store.markIrreversible(operationId, reason);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Log an action execution (stateless actions like print, email, etc.)
|
|
102
|
+
* @param doctype - The doctype the action was executed on
|
|
103
|
+
* @param actionName - The name of the action that was executed
|
|
104
|
+
* @param recordIds - Optional array of record IDs the action was executed on
|
|
105
|
+
* @param result - The result of the action execution
|
|
106
|
+
* @param error - Optional error message if action failed
|
|
107
|
+
* @returns The operation ID
|
|
108
|
+
*/
|
|
109
|
+
function logAction(doctype, actionName, recordIds, result = 'success', error) {
|
|
110
|
+
return store.logAction(doctype, actionName, recordIds, result, error);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Update configuration
|
|
114
|
+
* @param options - Configuration options to update
|
|
115
|
+
*/
|
|
116
|
+
function configure(options) {
|
|
117
|
+
store.configure(options);
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
// State
|
|
121
|
+
operations,
|
|
122
|
+
currentIndex,
|
|
123
|
+
undoRedoState,
|
|
124
|
+
canUndo,
|
|
125
|
+
canRedo,
|
|
126
|
+
undoCount,
|
|
127
|
+
redoCount,
|
|
128
|
+
// Methods
|
|
129
|
+
undo,
|
|
130
|
+
redo,
|
|
131
|
+
startBatch,
|
|
132
|
+
commitBatch,
|
|
133
|
+
cancelBatch,
|
|
134
|
+
clear,
|
|
135
|
+
getOperationsFor,
|
|
136
|
+
getSnapshot,
|
|
137
|
+
markIrreversible,
|
|
138
|
+
logAction,
|
|
139
|
+
configure,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Keyboard shortcut handler for undo/redo
|
|
144
|
+
* Automatically binds Ctrl+Z (undo) and Ctrl+Shift+Z/Ctrl+Y (redo) using VueUse
|
|
145
|
+
*
|
|
146
|
+
* @param hstStore - The HST store to operate on
|
|
147
|
+
* @param enabled - Whether shortcuts are enabled (default: true)
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* import { onMounted } from 'vue'
|
|
152
|
+
*
|
|
153
|
+
* const stonecrop = useStonecrop({ doctype, recordId })
|
|
154
|
+
* useUndoRedoShortcuts(stonecrop.hstStore)
|
|
155
|
+
* ```
|
|
156
|
+
*
|
|
157
|
+
* @public
|
|
158
|
+
*/
|
|
159
|
+
export function useUndoRedoShortcuts(hstStore, enabled = true) {
|
|
160
|
+
if (!enabled)
|
|
161
|
+
return;
|
|
162
|
+
const { undo, redo, canUndo, canRedo } = useOperationLog();
|
|
163
|
+
const keys = useMagicKeys();
|
|
164
|
+
// Undo shortcuts: Ctrl+Z or Cmd+Z (Mac)
|
|
165
|
+
whenever(keys['Ctrl+Z'], () => {
|
|
166
|
+
if (canUndo.value) {
|
|
167
|
+
undo(hstStore);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
whenever(keys['Meta+Z'], () => {
|
|
171
|
+
if (canUndo.value) {
|
|
172
|
+
undo(hstStore);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
// Redo shortcuts: Ctrl+Shift+Z, Cmd+Shift+Z (Mac), or Ctrl+Y
|
|
176
|
+
whenever(keys['Ctrl+Shift+Z'], () => {
|
|
177
|
+
if (canRedo.value) {
|
|
178
|
+
redo(hstStore);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
whenever(keys['Meta+Shift+Z'], () => {
|
|
182
|
+
if (canRedo.value) {
|
|
183
|
+
redo(hstStore);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
whenever(keys['Ctrl+Y'], () => {
|
|
187
|
+
if (canRedo.value) {
|
|
188
|
+
redo(hstStore);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Batch operation helper
|
|
194
|
+
* Wraps a function execution in a batch operation
|
|
195
|
+
*
|
|
196
|
+
* @param fn - The function to execute within a batch
|
|
197
|
+
* @param description - Optional description for the batch
|
|
198
|
+
* @returns The batch operation ID
|
|
199
|
+
*
|
|
200
|
+
* @example
|
|
201
|
+
* ```typescript
|
|
202
|
+
* const { withBatch } = useOperationLog()
|
|
203
|
+
*
|
|
204
|
+
* const batchId = await withBatch(() => {
|
|
205
|
+
* hstStore.set('task.123.title', 'New Title')
|
|
206
|
+
* hstStore.set('task.123.status', 'active')
|
|
207
|
+
* hstStore.set('task.123.priority', 'high')
|
|
208
|
+
* }, 'Update task details')
|
|
209
|
+
* ```
|
|
210
|
+
*
|
|
211
|
+
* @public
|
|
212
|
+
*/
|
|
213
|
+
export async function withBatch(fn, description) {
|
|
214
|
+
const { startBatch, commitBatch, cancelBatch } = useOperationLog();
|
|
215
|
+
startBatch();
|
|
216
|
+
try {
|
|
217
|
+
await fn();
|
|
218
|
+
return commitBatch(description);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
cancelBatch();
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
}
|