@stonecrop/stonecrop 0.6.2 → 0.6.3
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 +348 -0
- package/dist/composables/operation-log.js +221 -0
- package/dist/doctype.js +73 -0
- package/dist/exceptions.js +16 -0
- package/dist/field-triggers.js +564 -0
- package/dist/index.js +18 -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 +90 -0
- package/dist/registry.js +72 -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 +1 -1
- package/dist/stonecrop.d.ts +47 -58
- package/dist/stonecrop.js +851 -859
- package/dist/stonecrop.js.map +1 -1
- package/dist/stonecrop.umd.cjs +3 -7
- package/dist/stonecrop.umd.cjs.map +1 -1
- package/dist/stores/data.js +7 -0
- package/dist/stores/hst.js +483 -0
- package/dist/stores/index.js +12 -0
- package/dist/stores/operation-log.js +571 -0
- package/dist/stores/xstate.js +29 -0
- package/dist/types/field-triggers.js +4 -0
- package/dist/types/index.js +4 -0
- package/dist/types/operation-log.js +0 -0
- package/dist/types/registry.js +0 -0
- package/package.json +3 -3
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { useMagicKeys, whenever } from '@vueuse/core';
|
|
2
|
+
import { storeToRefs } from 'pinia';
|
|
3
|
+
import { 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
|
+
// Try to use the injected store from the Stonecrop plugin first
|
|
32
|
+
// This ensures we use the same Pinia instance as the app
|
|
33
|
+
const injectedStore = inject('$operationLogStore', undefined);
|
|
34
|
+
const store = injectedStore || useOperationLogStore();
|
|
35
|
+
// Apply configuration if provided
|
|
36
|
+
if (config) {
|
|
37
|
+
store.configure(config);
|
|
38
|
+
}
|
|
39
|
+
// Extract reactive state
|
|
40
|
+
const { operations, currentIndex, undoRedoState, canUndo, canRedo, undoCount, redoCount } = storeToRefs(store);
|
|
41
|
+
/**
|
|
42
|
+
* Undo the last operation
|
|
43
|
+
*/
|
|
44
|
+
function undo(hstStore) {
|
|
45
|
+
return store.undo(hstStore);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Redo the next operation
|
|
49
|
+
*/
|
|
50
|
+
function redo(hstStore) {
|
|
51
|
+
return store.redo(hstStore);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Start a batch operation
|
|
55
|
+
*/
|
|
56
|
+
function startBatch() {
|
|
57
|
+
store.startBatch();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Commit the current batch
|
|
61
|
+
*/
|
|
62
|
+
function commitBatch(description) {
|
|
63
|
+
return store.commitBatch(description);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Cancel the current batch
|
|
67
|
+
*/
|
|
68
|
+
function cancelBatch() {
|
|
69
|
+
store.cancelBatch();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Clear all operations
|
|
73
|
+
*/
|
|
74
|
+
function clear() {
|
|
75
|
+
store.clear();
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get operations for a specific doctype/record
|
|
79
|
+
*/
|
|
80
|
+
function getOperationsFor(doctype, recordId) {
|
|
81
|
+
return store.getOperationsFor(doctype, recordId);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Get a snapshot of the operation log
|
|
85
|
+
*/
|
|
86
|
+
function getSnapshot() {
|
|
87
|
+
return store.getSnapshot();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Mark an operation as irreversible
|
|
91
|
+
* @param operationId - The ID of the operation to mark
|
|
92
|
+
* @param reason - The reason why the operation is irreversible
|
|
93
|
+
*/
|
|
94
|
+
function markIrreversible(operationId, reason) {
|
|
95
|
+
store.markIrreversible(operationId, reason);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Log an action execution (stateless actions like print, email, etc.)
|
|
99
|
+
* @param doctype - The doctype the action was executed on
|
|
100
|
+
* @param actionName - The name of the action that was executed
|
|
101
|
+
* @param recordIds - Optional array of record IDs the action was executed on
|
|
102
|
+
* @param result - The result of the action execution
|
|
103
|
+
* @param error - Optional error message if action failed
|
|
104
|
+
* @returns The operation ID
|
|
105
|
+
*/
|
|
106
|
+
function logAction(doctype, actionName, recordIds, result = 'success', error) {
|
|
107
|
+
return store.logAction(doctype, actionName, recordIds, result, error);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Update configuration
|
|
111
|
+
* @param options - Configuration options to update
|
|
112
|
+
*/
|
|
113
|
+
function configure(options) {
|
|
114
|
+
store.configure(options);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
// State
|
|
118
|
+
operations,
|
|
119
|
+
currentIndex,
|
|
120
|
+
undoRedoState,
|
|
121
|
+
canUndo,
|
|
122
|
+
canRedo,
|
|
123
|
+
undoCount,
|
|
124
|
+
redoCount,
|
|
125
|
+
// Methods
|
|
126
|
+
undo,
|
|
127
|
+
redo,
|
|
128
|
+
startBatch,
|
|
129
|
+
commitBatch,
|
|
130
|
+
cancelBatch,
|
|
131
|
+
clear,
|
|
132
|
+
getOperationsFor,
|
|
133
|
+
getSnapshot,
|
|
134
|
+
markIrreversible,
|
|
135
|
+
logAction,
|
|
136
|
+
configure,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Keyboard shortcut handler for undo/redo
|
|
141
|
+
* Automatically binds Ctrl+Z (undo) and Ctrl+Shift+Z/Ctrl+Y (redo) using VueUse
|
|
142
|
+
*
|
|
143
|
+
* @param hstStore - The HST store to operate on
|
|
144
|
+
* @param enabled - Whether shortcuts are enabled (default: true)
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* import { onMounted } from 'vue'
|
|
149
|
+
*
|
|
150
|
+
* const stonecrop = useStonecrop({ doctype, recordId })
|
|
151
|
+
* useUndoRedoShortcuts(stonecrop.hstStore)
|
|
152
|
+
* ```
|
|
153
|
+
*
|
|
154
|
+
* @public
|
|
155
|
+
*/
|
|
156
|
+
export function useUndoRedoShortcuts(hstStore, enabled = true) {
|
|
157
|
+
if (!enabled)
|
|
158
|
+
return;
|
|
159
|
+
const { undo, redo, canUndo, canRedo } = useOperationLog();
|
|
160
|
+
const keys = useMagicKeys();
|
|
161
|
+
// Undo shortcuts: Ctrl+Z or Cmd+Z (Mac)
|
|
162
|
+
whenever(keys['Ctrl+Z'], () => {
|
|
163
|
+
if (canUndo.value) {
|
|
164
|
+
undo(hstStore);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
whenever(keys['Meta+Z'], () => {
|
|
168
|
+
if (canUndo.value) {
|
|
169
|
+
undo(hstStore);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
// Redo shortcuts: Ctrl+Shift+Z, Cmd+Shift+Z (Mac), or Ctrl+Y
|
|
173
|
+
whenever(keys['Ctrl+Shift+Z'], () => {
|
|
174
|
+
if (canRedo.value) {
|
|
175
|
+
redo(hstStore);
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
whenever(keys['Meta+Shift+Z'], () => {
|
|
179
|
+
if (canRedo.value) {
|
|
180
|
+
redo(hstStore);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
whenever(keys['Ctrl+Y'], () => {
|
|
184
|
+
if (canRedo.value) {
|
|
185
|
+
redo(hstStore);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Batch operation helper
|
|
191
|
+
* Wraps a function execution in a batch operation
|
|
192
|
+
*
|
|
193
|
+
* @param fn - The function to execute within a batch
|
|
194
|
+
* @param description - Optional description for the batch
|
|
195
|
+
* @returns The batch operation ID
|
|
196
|
+
*
|
|
197
|
+
* @example
|
|
198
|
+
* ```typescript
|
|
199
|
+
* const { withBatch } = useOperationLog()
|
|
200
|
+
*
|
|
201
|
+
* const batchId = await withBatch(() => {
|
|
202
|
+
* hstStore.set('task.123.title', 'New Title')
|
|
203
|
+
* hstStore.set('task.123.status', 'active')
|
|
204
|
+
* hstStore.set('task.123.priority', 'high')
|
|
205
|
+
* }, 'Update task details')
|
|
206
|
+
* ```
|
|
207
|
+
*
|
|
208
|
+
* @public
|
|
209
|
+
*/
|
|
210
|
+
export async function withBatch(fn, description) {
|
|
211
|
+
const { startBatch, commitBatch, cancelBatch } = useOperationLog();
|
|
212
|
+
startBatch();
|
|
213
|
+
try {
|
|
214
|
+
await fn();
|
|
215
|
+
return commitBatch(description);
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
cancelBatch();
|
|
219
|
+
throw error;
|
|
220
|
+
}
|
|
221
|
+
}
|
package/dist/doctype.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctype Meta class
|
|
3
|
+
* @public
|
|
4
|
+
*/
|
|
5
|
+
export default class DoctypeMeta {
|
|
6
|
+
/**
|
|
7
|
+
* The doctype name
|
|
8
|
+
* @public
|
|
9
|
+
* @readonly
|
|
10
|
+
*/
|
|
11
|
+
doctype;
|
|
12
|
+
/**
|
|
13
|
+
* The doctype schema
|
|
14
|
+
* @public
|
|
15
|
+
* @readonly
|
|
16
|
+
*/
|
|
17
|
+
schema;
|
|
18
|
+
/**
|
|
19
|
+
* The doctype workflow
|
|
20
|
+
* @public
|
|
21
|
+
* @readonly
|
|
22
|
+
*/
|
|
23
|
+
workflow;
|
|
24
|
+
/**
|
|
25
|
+
* The doctype actions and field triggers
|
|
26
|
+
* @public
|
|
27
|
+
* @readonly
|
|
28
|
+
*/
|
|
29
|
+
actions;
|
|
30
|
+
/**
|
|
31
|
+
* The doctype component
|
|
32
|
+
* @public
|
|
33
|
+
* @readonly
|
|
34
|
+
*/
|
|
35
|
+
component;
|
|
36
|
+
/**
|
|
37
|
+
* Creates a new DoctypeMeta instance
|
|
38
|
+
* @param doctype - The doctype name
|
|
39
|
+
* @param schema - The doctype schema definition
|
|
40
|
+
* @param workflow - The doctype workflow configuration (XState machine)
|
|
41
|
+
* @param actions - The doctype actions and field triggers
|
|
42
|
+
* @param component - Optional Vue component for rendering the doctype
|
|
43
|
+
*/
|
|
44
|
+
constructor(doctype, schema, workflow, actions, component) {
|
|
45
|
+
this.doctype = doctype;
|
|
46
|
+
this.schema = schema;
|
|
47
|
+
this.workflow = workflow;
|
|
48
|
+
this.actions = actions;
|
|
49
|
+
this.component = component;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Converts the registered doctype string to a slug (kebab-case). The following conversions are made:
|
|
53
|
+
* - It replaces camelCase and PascalCase with kebab-case strings
|
|
54
|
+
* - It replaces spaces and underscores with hyphens
|
|
55
|
+
* - It converts the string to lowercase
|
|
56
|
+
*
|
|
57
|
+
* @returns The slugified doctype string
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const doctype = new DoctypeMeta('TaskItem', schema, workflow, actions
|
|
62
|
+
* console.log(doctype.slug) // 'task-item'
|
|
63
|
+
* ```
|
|
64
|
+
*
|
|
65
|
+
* @public
|
|
66
|
+
*/
|
|
67
|
+
get slug() {
|
|
68
|
+
return this.doctype
|
|
69
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
70
|
+
.replace(/[\s_]+/g, '-')
|
|
71
|
+
.toLowerCase();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotImplementedError
|
|
3
|
+
* @param message {string} - The error message
|
|
4
|
+
* @class
|
|
5
|
+
* @description This error is thrown when a method has not been implemented
|
|
6
|
+
* @example
|
|
7
|
+
* throw new NotImplementedError('Method not implemented')
|
|
8
|
+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error|Error}
|
|
9
|
+
* @public
|
|
10
|
+
*/
|
|
11
|
+
export class NotImplementedError extends Error {
|
|
12
|
+
constructor(message = '') {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = 'NotImplemented';
|
|
15
|
+
}
|
|
16
|
+
}
|