@stonecrop/stonecrop 0.11.0 → 0.11.2
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/registry.js +0 -37
- package/dist/src/registry.d.ts +1 -24
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/stonecrop.d.ts +0 -24
- package/dist/stonecrop.js +0 -36
- package/dist/stonecrop.js.map +1 -1
- package/package.json +5 -5
- package/src/registry.ts +1 -39
- package/dist/composable.js +0 -1
- package/dist/composables/use-lazy-link-state.js +0 -125
- package/dist/composables/use-stonecrop.js +0 -476
- package/dist/operation-log-DB-dGNT9.js +0 -593
- package/dist/operation-log-DB-dGNT9.js.map +0 -1
- package/dist/src/composable.d.ts +0 -11
- package/dist/src/composable.d.ts.map +0 -1
- package/dist/src/composable.js +0 -477
- package/dist/src/composables/operation-log.js +0 -224
- package/dist/src/composables/stonecrop.js +0 -574
- package/dist/src/composables/use-lazy-link-state.d.ts +0 -25
- package/dist/src/composables/use-lazy-link-state.d.ts.map +0 -1
- package/dist/src/composables/use-stonecrop.d.ts +0 -93
- package/dist/src/composables/use-stonecrop.d.ts.map +0 -1
- package/dist/src/composables/useNestedSchema.d.ts +0 -110
- package/dist/src/composables/useNestedSchema.d.ts.map +0 -1
- package/dist/src/composables/useNestedSchema.js +0 -155
- package/dist/src/doctype.js +0 -234
- package/dist/src/exceptions.js +0 -16
- package/dist/src/field-triggers.js +0 -567
- package/dist/src/index.js +0 -23
- package/dist/src/plugins/index.js +0 -96
- package/dist/src/registry.js +0 -246
- package/dist/src/schema-validator.js +0 -315
- package/dist/src/stonecrop.js +0 -339
- package/dist/src/stores/data.d.ts +0 -11
- package/dist/src/stores/data.d.ts.map +0 -1
- package/dist/src/stores/hst.js +0 -495
- package/dist/src/stores/index.js +0 -12
- package/dist/src/stores/operation-log.js +0 -568
- package/dist/src/stores/xstate.d.ts +0 -31
- package/dist/src/stores/xstate.d.ts.map +0 -1
- package/dist/src/tsdoc-metadata.json +0 -11
- package/dist/src/types/field-triggers.js +0 -4
- package/dist/src/types/index.js +0 -4
- package/dist/src/types/operation-log.js +0 -0
- package/dist/src/types/registry.js +0 -0
- package/dist/src/utils.d.ts +0 -24
- package/dist/src/utils.d.ts.map +0 -1
- package/dist/stonecrop.css +0 -1
- package/dist/stonecrop.umd.cjs +0 -6
- package/dist/stonecrop.umd.cjs.map +0 -1
- package/dist/stores/data.js +0 -7
- package/dist/stores/xstate.js +0 -29
- package/dist/tests/setup.d.ts +0 -5
- package/dist/tests/setup.d.ts.map +0 -1
- package/dist/tests/setup.js +0 -15
- package/dist/utils.js +0 -46
package/dist/src/composable.js
DELETED
|
@@ -1,477 +0,0 @@
|
|
|
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
|
-
// Resolved schema with nested Doctype fields expanded
|
|
20
|
-
const resolvedSchema = ref([]);
|
|
21
|
-
// Auto-resolve schema when doctype is available
|
|
22
|
-
if (options.doctype && registry) {
|
|
23
|
-
const schemaArray = options.doctype.schema
|
|
24
|
-
? Array.isArray(options.doctype.schema)
|
|
25
|
-
? options.doctype.schema
|
|
26
|
-
: Array.from(options.doctype.schema)
|
|
27
|
-
: [];
|
|
28
|
-
resolvedSchema.value = registry.resolveSchema(schemaArray);
|
|
29
|
-
}
|
|
30
|
-
// Operation log state and methods - will be populated after stonecrop instance is created
|
|
31
|
-
const operations = ref([]);
|
|
32
|
-
const currentIndex = ref(-1);
|
|
33
|
-
const canUndo = computed(() => stonecrop.value?.getOperationLogStore().canUndo ?? false);
|
|
34
|
-
const canRedo = computed(() => stonecrop.value?.getOperationLogStore().canRedo ?? false);
|
|
35
|
-
const undoCount = computed(() => stonecrop.value?.getOperationLogStore().undoCount ?? 0);
|
|
36
|
-
const redoCount = computed(() => stonecrop.value?.getOperationLogStore().redoCount ?? 0);
|
|
37
|
-
const undoRedoState = computed(() => stonecrop.value?.getOperationLogStore().undoRedoState ?? {
|
|
38
|
-
canUndo: false,
|
|
39
|
-
canRedo: false,
|
|
40
|
-
undoCount: 0,
|
|
41
|
-
redoCount: 0,
|
|
42
|
-
currentIndex: -1,
|
|
43
|
-
});
|
|
44
|
-
// Operation log methods
|
|
45
|
-
const undo = (hstStore) => {
|
|
46
|
-
return stonecrop.value?.getOperationLogStore().undo(hstStore) ?? false;
|
|
47
|
-
};
|
|
48
|
-
const redo = (hstStore) => {
|
|
49
|
-
return stonecrop.value?.getOperationLogStore().redo(hstStore) ?? false;
|
|
50
|
-
};
|
|
51
|
-
const startBatch = () => {
|
|
52
|
-
stonecrop.value?.getOperationLogStore().startBatch();
|
|
53
|
-
};
|
|
54
|
-
const commitBatch = (description) => {
|
|
55
|
-
return stonecrop.value?.getOperationLogStore().commitBatch(description) ?? null;
|
|
56
|
-
};
|
|
57
|
-
const cancelBatch = () => {
|
|
58
|
-
stonecrop.value?.getOperationLogStore().cancelBatch();
|
|
59
|
-
};
|
|
60
|
-
const clear = () => {
|
|
61
|
-
stonecrop.value?.getOperationLogStore().clear();
|
|
62
|
-
};
|
|
63
|
-
const getOperationsFor = (doctype, recordId) => {
|
|
64
|
-
return stonecrop.value?.getOperationLogStore().getOperationsFor(doctype, recordId) ?? [];
|
|
65
|
-
};
|
|
66
|
-
const getSnapshot = () => {
|
|
67
|
-
return (stonecrop.value?.getOperationLogStore().getSnapshot() ?? {
|
|
68
|
-
operations: [],
|
|
69
|
-
currentIndex: -1,
|
|
70
|
-
totalOperations: 0,
|
|
71
|
-
reversibleOperations: 0,
|
|
72
|
-
irreversibleOperations: 0,
|
|
73
|
-
});
|
|
74
|
-
};
|
|
75
|
-
const markIrreversible = (operationId, reason) => {
|
|
76
|
-
stonecrop.value?.getOperationLogStore().markIrreversible(operationId, reason);
|
|
77
|
-
};
|
|
78
|
-
const logAction = (doctype, actionName, recordIds, result = 'success', error) => {
|
|
79
|
-
return stonecrop.value?.getOperationLogStore().logAction(doctype, actionName, recordIds, result, error) ?? '';
|
|
80
|
-
};
|
|
81
|
-
const configure = (config) => {
|
|
82
|
-
stonecrop.value?.getOperationLogStore().configure(config);
|
|
83
|
-
};
|
|
84
|
-
// Initialize Stonecrop instance
|
|
85
|
-
onMounted(async () => {
|
|
86
|
-
if (!registry) {
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
stonecrop.value = providedStonecrop || new Stonecrop(registry);
|
|
90
|
-
// Set up reactive refs from operation log store - only if Pinia is available
|
|
91
|
-
try {
|
|
92
|
-
const opLogStore = stonecrop.value.getOperationLogStore();
|
|
93
|
-
const opLogRefs = storeToRefs(opLogStore);
|
|
94
|
-
operations.value = opLogRefs.operations.value;
|
|
95
|
-
currentIndex.value = opLogRefs.currentIndex.value;
|
|
96
|
-
// Watch for changes in operation log state
|
|
97
|
-
watch(() => opLogRefs.operations.value, newOps => {
|
|
98
|
-
operations.value = newOps;
|
|
99
|
-
});
|
|
100
|
-
watch(() => opLogRefs.currentIndex.value, newIndex => {
|
|
101
|
-
currentIndex.value = newIndex;
|
|
102
|
-
});
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
// Pinia not available (e.g., in tests) - operation log features will not be available
|
|
106
|
-
// Silently fail - operation log is optional
|
|
107
|
-
}
|
|
108
|
-
// Handle router-based setup if no specific doctype provided
|
|
109
|
-
if (!options.doctype && registry.router) {
|
|
110
|
-
const route = registry.router.currentRoute.value;
|
|
111
|
-
// Parse route path - let the application determine the doctype from the route
|
|
112
|
-
if (!route.path)
|
|
113
|
-
return; // Early return if no path available
|
|
114
|
-
const pathSegments = route.path.split('/').filter(segment => segment.length > 0);
|
|
115
|
-
const recordId = pathSegments[1]?.toLowerCase();
|
|
116
|
-
if (pathSegments.length > 0) {
|
|
117
|
-
// Create route context for getMeta function
|
|
118
|
-
const routeContext = {
|
|
119
|
-
path: route.path,
|
|
120
|
-
segments: pathSegments,
|
|
121
|
-
};
|
|
122
|
-
const doctype = await registry.getMeta?.(routeContext);
|
|
123
|
-
if (doctype) {
|
|
124
|
-
registry.addDoctype(doctype);
|
|
125
|
-
stonecrop.value.setup(doctype);
|
|
126
|
-
// Set reactive refs for router-based doctype
|
|
127
|
-
routerDoctype.value = doctype;
|
|
128
|
-
routerRecordId.value = recordId;
|
|
129
|
-
hstStore.value = stonecrop.value.getStore();
|
|
130
|
-
// Resolve schema for router-loaded doctype
|
|
131
|
-
if (registry) {
|
|
132
|
-
const schemaArray = doctype.schema
|
|
133
|
-
? Array.isArray(doctype.schema)
|
|
134
|
-
? doctype.schema
|
|
135
|
-
: Array.from(doctype.schema)
|
|
136
|
-
: [];
|
|
137
|
-
resolvedSchema.value = registry.resolveSchema(schemaArray);
|
|
138
|
-
}
|
|
139
|
-
if (recordId && recordId !== 'new') {
|
|
140
|
-
const existingRecord = stonecrop.value.getRecordById(doctype, recordId);
|
|
141
|
-
if (existingRecord) {
|
|
142
|
-
formData.value = existingRecord.get('') || {};
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
try {
|
|
146
|
-
await stonecrop.value.getRecord(doctype, recordId);
|
|
147
|
-
const loadedRecord = stonecrop.value.getRecordById(doctype, recordId);
|
|
148
|
-
if (loadedRecord) {
|
|
149
|
-
formData.value = loadedRecord.get('') || {};
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
formData.value = initializeNewRecord(doctype);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
formData.value = initializeNewRecord(doctype);
|
|
159
|
-
}
|
|
160
|
-
if (hstStore.value) {
|
|
161
|
-
setupDeepReactivity(doctype, recordId || 'new', formData, hstStore.value);
|
|
162
|
-
}
|
|
163
|
-
stonecrop.value.runAction(doctype, 'load', recordId ? [recordId] : undefined);
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
// Handle HST integration if doctype is provided explicitly
|
|
168
|
-
if (options.doctype) {
|
|
169
|
-
hstStore.value = stonecrop.value.getStore();
|
|
170
|
-
const doctype = options.doctype;
|
|
171
|
-
const recordId = options.recordId;
|
|
172
|
-
if (recordId && recordId !== 'new') {
|
|
173
|
-
const existingRecord = stonecrop.value.getRecordById(doctype, recordId);
|
|
174
|
-
if (existingRecord) {
|
|
175
|
-
formData.value = existingRecord.get('') || {};
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
try {
|
|
179
|
-
await stonecrop.value.getRecord(doctype, recordId);
|
|
180
|
-
const loadedRecord = stonecrop.value.getRecordById(doctype, recordId);
|
|
181
|
-
if (loadedRecord) {
|
|
182
|
-
formData.value = loadedRecord.get('') || {};
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
catch {
|
|
186
|
-
formData.value = initializeNewRecord(doctype);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
else {
|
|
191
|
-
formData.value = initializeNewRecord(doctype);
|
|
192
|
-
}
|
|
193
|
-
if (hstStore.value) {
|
|
194
|
-
setupDeepReactivity(doctype, recordId || 'new', formData, hstStore.value);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
// HST integration functions - always created but only populated when HST is available
|
|
199
|
-
const provideHSTPath = (fieldname, customRecordId) => {
|
|
200
|
-
const doctype = options.doctype || routerDoctype.value;
|
|
201
|
-
if (!doctype)
|
|
202
|
-
return '';
|
|
203
|
-
const actualRecordId = customRecordId || options.recordId || routerRecordId.value || 'new';
|
|
204
|
-
return `${doctype.slug}.${actualRecordId}.${fieldname}`;
|
|
205
|
-
};
|
|
206
|
-
const handleHSTChange = (changeData) => {
|
|
207
|
-
const doctype = options.doctype || routerDoctype.value;
|
|
208
|
-
if (!hstStore.value || !stonecrop.value || !doctype) {
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
try {
|
|
212
|
-
const pathParts = changeData.path.split('.');
|
|
213
|
-
if (pathParts.length >= 2) {
|
|
214
|
-
const doctypeSlug = pathParts[0];
|
|
215
|
-
const recordId = pathParts[1];
|
|
216
|
-
if (!hstStore.value.has(`${doctypeSlug}.${recordId}`)) {
|
|
217
|
-
stonecrop.value.addRecord(doctype, recordId, { ...formData.value });
|
|
218
|
-
}
|
|
219
|
-
if (pathParts.length > 3) {
|
|
220
|
-
const recordPath = `${doctypeSlug}.${recordId}`;
|
|
221
|
-
const nestedParts = pathParts.slice(2);
|
|
222
|
-
let currentPath = recordPath;
|
|
223
|
-
for (let i = 0; i < nestedParts.length - 1; i++) {
|
|
224
|
-
currentPath += `.${nestedParts[i]}`;
|
|
225
|
-
if (!hstStore.value.has(currentPath)) {
|
|
226
|
-
const nextPart = nestedParts[i + 1];
|
|
227
|
-
const isArray = !isNaN(Number(nextPart));
|
|
228
|
-
hstStore.value.set(currentPath, isArray ? [] : {});
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
hstStore.value.set(changeData.path, changeData.value);
|
|
234
|
-
const fieldParts = changeData.fieldname.split('.');
|
|
235
|
-
const newFormData = { ...formData.value };
|
|
236
|
-
if (fieldParts.length === 1) {
|
|
237
|
-
newFormData[fieldParts[0]] = changeData.value;
|
|
238
|
-
}
|
|
239
|
-
else {
|
|
240
|
-
updateNestedObject(newFormData, fieldParts, changeData.value);
|
|
241
|
-
}
|
|
242
|
-
formData.value = newFormData;
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
// Silently handle errors
|
|
246
|
-
}
|
|
247
|
-
};
|
|
248
|
-
// Provide injection tokens if HST will be available
|
|
249
|
-
if (options.doctype || registry?.router) {
|
|
250
|
-
provide('hstPathProvider', provideHSTPath);
|
|
251
|
-
provide('hstChangeHandler', handleHSTChange);
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Load nested doctype data from API or initialize empty structure
|
|
255
|
-
* @param parentPath - The parent path (e.g., "customer.123.address")
|
|
256
|
-
* @param childDoctype - The child doctype metadata
|
|
257
|
-
* @param recordId - Optional record ID to load
|
|
258
|
-
* @returns Promise resolving to the loaded or initialized data
|
|
259
|
-
*/
|
|
260
|
-
const loadNestedData = (parentPath, childDoctype, recordId) => {
|
|
261
|
-
if (!stonecrop.value) {
|
|
262
|
-
return initializeNewRecord(childDoctype);
|
|
263
|
-
}
|
|
264
|
-
// If recordId provided, try to load existing data
|
|
265
|
-
if (recordId) {
|
|
266
|
-
try {
|
|
267
|
-
// Check if data already exists in HST
|
|
268
|
-
const existingData = hstStore.value?.get(parentPath);
|
|
269
|
-
if (existingData && typeof existingData === 'object') {
|
|
270
|
-
return existingData;
|
|
271
|
-
}
|
|
272
|
-
// TODO: Add API fetch logic here if needed
|
|
273
|
-
// For now, initialize new record
|
|
274
|
-
return initializeNewRecord(childDoctype);
|
|
275
|
-
}
|
|
276
|
-
catch {
|
|
277
|
-
return initializeNewRecord(childDoctype);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
// Initialize new record
|
|
281
|
-
return initializeNewRecord(childDoctype);
|
|
282
|
-
};
|
|
283
|
-
/**
|
|
284
|
-
* Recursively save a record with all nested doctype fields
|
|
285
|
-
* @param doctype - The doctype metadata
|
|
286
|
-
* @param recordId - The record ID to save
|
|
287
|
-
* @returns Promise resolving to the complete save payload
|
|
288
|
-
*/
|
|
289
|
-
const saveRecursive = async (doctype, recordId) => {
|
|
290
|
-
if (!hstStore.value || !stonecrop.value) {
|
|
291
|
-
throw new Error('HST store not initialized');
|
|
292
|
-
}
|
|
293
|
-
const recordPath = `${doctype.slug}.${recordId}`;
|
|
294
|
-
const recordData = hstStore.value.get(recordPath) || {};
|
|
295
|
-
// Build the save payload using resolved schema
|
|
296
|
-
const payload = { ...recordData };
|
|
297
|
-
// Use resolveSchema to get the full resolved tree, then walk Doctype fields
|
|
298
|
-
const schemaArray = (doctype.schema ? (Array.isArray(doctype.schema) ? doctype.schema : Array.from(doctype.schema)) : []);
|
|
299
|
-
const resolved = registry ? registry.resolveSchema(schemaArray) : schemaArray;
|
|
300
|
-
const doctypeFields = resolved.filter(field => 'fieldtype' in field && field.fieldtype === 'Doctype' && 'schema' in field && Array.isArray(field.schema));
|
|
301
|
-
// Recursively collect nested data from HST using resolved schemas
|
|
302
|
-
for (const field of doctypeFields) {
|
|
303
|
-
const doctypeField = field;
|
|
304
|
-
const fieldPath = `${recordPath}.${doctypeField.fieldname}`;
|
|
305
|
-
const nestedData = collectNestedData(doctypeField.schema, fieldPath, hstStore.value);
|
|
306
|
-
payload[doctypeField.fieldname] = nestedData;
|
|
307
|
-
}
|
|
308
|
-
return payload;
|
|
309
|
-
};
|
|
310
|
-
/**
|
|
311
|
-
* Create a nested context for child forms
|
|
312
|
-
* @param basePath - The base path for the nested context (e.g., "customer.123.address")
|
|
313
|
-
* @param _childDoctype - The child doctype metadata (unused but kept for API consistency)
|
|
314
|
-
* @returns Object with scoped provideHSTPath and handleHSTChange
|
|
315
|
-
*/
|
|
316
|
-
const createNestedContext = (basePath, _childDoctype) => {
|
|
317
|
-
const nestedProvideHSTPath = (fieldname) => {
|
|
318
|
-
return `${basePath}.${fieldname}`;
|
|
319
|
-
};
|
|
320
|
-
const nestedHandleHSTChange = (changeData) => {
|
|
321
|
-
// Update the path to be relative to the nested base path
|
|
322
|
-
const nestedPath = changeData.path.startsWith(basePath) ? changeData.path : `${basePath}.${changeData.fieldname}`;
|
|
323
|
-
handleHSTChange({
|
|
324
|
-
...changeData,
|
|
325
|
-
path: nestedPath,
|
|
326
|
-
});
|
|
327
|
-
};
|
|
328
|
-
return {
|
|
329
|
-
provideHSTPath: nestedProvideHSTPath,
|
|
330
|
-
handleHSTChange: nestedHandleHSTChange,
|
|
331
|
-
};
|
|
332
|
-
};
|
|
333
|
-
// Create operation log API object
|
|
334
|
-
const operationLog = {
|
|
335
|
-
operations,
|
|
336
|
-
currentIndex,
|
|
337
|
-
undoRedoState,
|
|
338
|
-
canUndo,
|
|
339
|
-
canRedo,
|
|
340
|
-
undoCount,
|
|
341
|
-
redoCount,
|
|
342
|
-
undo,
|
|
343
|
-
redo,
|
|
344
|
-
startBatch,
|
|
345
|
-
commitBatch,
|
|
346
|
-
cancelBatch,
|
|
347
|
-
clear,
|
|
348
|
-
getOperationsFor,
|
|
349
|
-
getSnapshot,
|
|
350
|
-
markIrreversible,
|
|
351
|
-
logAction,
|
|
352
|
-
configure,
|
|
353
|
-
};
|
|
354
|
-
// Always return HST functions if doctype is provided or will be loaded from router
|
|
355
|
-
if (options.doctype) {
|
|
356
|
-
// Explicit doctype - return HST immediately
|
|
357
|
-
return {
|
|
358
|
-
stonecrop,
|
|
359
|
-
operationLog,
|
|
360
|
-
provideHSTPath,
|
|
361
|
-
handleHSTChange,
|
|
362
|
-
hstStore,
|
|
363
|
-
formData,
|
|
364
|
-
resolvedSchema,
|
|
365
|
-
loadNestedData,
|
|
366
|
-
saveRecursive,
|
|
367
|
-
createNestedContext,
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
else if (!options.doctype && registry?.router) {
|
|
371
|
-
// Router-based - return HST (will be populated after mount)
|
|
372
|
-
return {
|
|
373
|
-
stonecrop,
|
|
374
|
-
operationLog,
|
|
375
|
-
provideHSTPath,
|
|
376
|
-
handleHSTChange,
|
|
377
|
-
hstStore,
|
|
378
|
-
formData,
|
|
379
|
-
resolvedSchema,
|
|
380
|
-
loadNestedData,
|
|
381
|
-
saveRecursive,
|
|
382
|
-
createNestedContext,
|
|
383
|
-
};
|
|
384
|
-
}
|
|
385
|
-
// No doctype and no router - basic mode
|
|
386
|
-
return {
|
|
387
|
-
stonecrop,
|
|
388
|
-
operationLog,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
* Initialize new record structure based on doctype schema
|
|
393
|
-
*/
|
|
394
|
-
function initializeNewRecord(doctype) {
|
|
395
|
-
const initialData = {};
|
|
396
|
-
if (!doctype.schema) {
|
|
397
|
-
return initialData;
|
|
398
|
-
}
|
|
399
|
-
doctype.schema.forEach(field => {
|
|
400
|
-
const fieldtype = 'fieldtype' in field ? field.fieldtype : 'Data';
|
|
401
|
-
switch (fieldtype) {
|
|
402
|
-
case 'Data':
|
|
403
|
-
case 'Text':
|
|
404
|
-
initialData[field.fieldname] = '';
|
|
405
|
-
break;
|
|
406
|
-
case 'Check':
|
|
407
|
-
initialData[field.fieldname] = false;
|
|
408
|
-
break;
|
|
409
|
-
case 'Int':
|
|
410
|
-
case 'Float':
|
|
411
|
-
initialData[field.fieldname] = 0;
|
|
412
|
-
break;
|
|
413
|
-
case 'Table':
|
|
414
|
-
initialData[field.fieldname] = [];
|
|
415
|
-
break;
|
|
416
|
-
case 'JSON':
|
|
417
|
-
initialData[field.fieldname] = {};
|
|
418
|
-
break;
|
|
419
|
-
default:
|
|
420
|
-
initialData[field.fieldname] = null;
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
return initialData;
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Setup deep reactivity between form data and HST store
|
|
427
|
-
*/
|
|
428
|
-
function setupDeepReactivity(doctype, recordId, formData, hstStore) {
|
|
429
|
-
watch(formData, newData => {
|
|
430
|
-
const recordPath = `${doctype.slug}.${recordId}`;
|
|
431
|
-
Object.keys(newData).forEach(fieldname => {
|
|
432
|
-
const path = `${recordPath}.${fieldname}`;
|
|
433
|
-
try {
|
|
434
|
-
hstStore.set(path, newData[fieldname]);
|
|
435
|
-
}
|
|
436
|
-
catch {
|
|
437
|
-
// Silently handle errors
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
}, { deep: true });
|
|
441
|
-
}
|
|
442
|
-
/**
|
|
443
|
-
* Update nested object with dot-notation path
|
|
444
|
-
*/
|
|
445
|
-
function updateNestedObject(obj, path, value) {
|
|
446
|
-
let current = obj;
|
|
447
|
-
for (let i = 0; i < path.length - 1; i++) {
|
|
448
|
-
const key = path[i];
|
|
449
|
-
if (!(key in current) || typeof current[key] !== 'object') {
|
|
450
|
-
current[key] = isNaN(Number(path[i + 1])) ? {} : [];
|
|
451
|
-
}
|
|
452
|
-
current = current[key];
|
|
453
|
-
}
|
|
454
|
-
const finalKey = path[path.length - 1];
|
|
455
|
-
current[finalKey] = value;
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* Recursively collect nested data from HST using pre-resolved schemas
|
|
459
|
-
* @param resolvedSchema - The already-resolved schema (with nested schemas embedded)
|
|
460
|
-
* @param basePath - The base path in HST (e.g., "customer.123.address")
|
|
461
|
-
* @param hstStore - The HST store instance
|
|
462
|
-
* @returns The collected data object
|
|
463
|
-
*/
|
|
464
|
-
function collectNestedData(resolvedSchema, basePath, hstStore) {
|
|
465
|
-
const data = hstStore.get(basePath) || {};
|
|
466
|
-
const payload = { ...data };
|
|
467
|
-
// Find Doctype fields that have resolved child schemas
|
|
468
|
-
const doctypeFields = resolvedSchema.filter(field => 'fieldtype' in field && field.fieldtype === 'Doctype' && 'schema' in field && Array.isArray(field.schema));
|
|
469
|
-
// Recursively collect nested data
|
|
470
|
-
for (const field of doctypeFields) {
|
|
471
|
-
const doctypeField = field;
|
|
472
|
-
const fieldPath = `${basePath}.${doctypeField.fieldname}`;
|
|
473
|
-
const nestedData = collectNestedData(doctypeField.schema, fieldPath, hstStore);
|
|
474
|
-
payload[doctypeField.fieldname] = nestedData;
|
|
475
|
-
}
|
|
476
|
-
return payload;
|
|
477
|
-
}
|
|
@@ -1,224 +0,0 @@
|
|
|
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
|
-
}
|