@stonecrop/stonecrop 0.6.3 → 0.7.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.
@@ -1,348 +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
- // 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
- }
@@ -1,221 +0,0 @@
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 DELETED
@@ -1,73 +0,0 @@
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
- }
@@ -1,16 +0,0 @@
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
- }