@stonecrop/stonecrop 0.10.16 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -29
- package/dist/composables/lazy-link.js +125 -0
- package/dist/composables/stonecrop.js +123 -68
- package/dist/doctype.js +10 -2
- package/dist/field-triggers.js +15 -3
- package/dist/index.js +4 -3
- package/dist/registry.js +261 -101
- package/dist/schema-validator.js +105 -1
- package/dist/src/composables/lazy-link.d.ts +25 -0
- package/dist/src/composables/lazy-link.d.ts.map +1 -0
- package/dist/src/composables/operation-log.d.ts +5 -5
- package/dist/src/composables/operation-log.d.ts.map +1 -1
- package/dist/src/composables/stonecrop.d.ts +11 -1
- package/dist/src/composables/stonecrop.d.ts.map +1 -1
- package/dist/src/doctype.d.ts +9 -1
- package/dist/src/doctype.d.ts.map +1 -1
- package/dist/src/field-triggers.d.ts +6 -0
- package/dist/src/field-triggers.d.ts.map +1 -1
- package/dist/src/index.d.ts +3 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/registry.d.ts +102 -23
- package/dist/src/registry.d.ts.map +1 -1
- package/dist/src/schema-validator.d.ts +8 -1
- package/dist/src/schema-validator.d.ts.map +1 -1
- package/dist/src/stonecrop.d.ts +73 -28
- package/dist/src/stonecrop.d.ts.map +1 -1
- package/dist/src/stores/hst.d.ts +5 -75
- package/dist/src/stores/hst.d.ts.map +1 -1
- package/dist/src/stores/operation-log.d.ts +14 -14
- package/dist/src/stores/operation-log.d.ts.map +1 -1
- package/dist/src/types/composable.d.ts +50 -12
- package/dist/src/types/composable.d.ts.map +1 -1
- package/dist/src/types/doctype.d.ts +6 -7
- package/dist/src/types/doctype.d.ts.map +1 -1
- package/dist/src/types/field-triggers.d.ts +1 -1
- package/dist/src/types/field-triggers.d.ts.map +1 -1
- package/dist/src/types/hst.d.ts +70 -0
- package/dist/src/types/hst.d.ts.map +1 -0
- package/dist/src/types/index.d.ts +1 -0
- package/dist/src/types/index.d.ts.map +1 -1
- package/dist/src/types/operation-log.d.ts +4 -4
- package/dist/src/types/operation-log.d.ts.map +1 -1
- package/dist/src/types/schema-validator.d.ts +2 -0
- package/dist/src/types/schema-validator.d.ts.map +1 -1
- package/dist/stonecrop.d.ts +317 -99
- package/dist/stonecrop.js +2191 -1897
- package/dist/stonecrop.js.map +1 -1
- package/dist/stores/hst.js +27 -25
- package/dist/stores/operation-log.js +59 -47
- package/dist/types/hst.js +0 -0
- package/dist/types/index.js +1 -0
- package/package.json +5 -5
- package/src/composables/lazy-link.ts +146 -0
- package/src/composables/operation-log.ts +1 -1
- package/src/composables/stonecrop.ts +142 -73
- package/src/doctype.ts +13 -4
- package/src/field-triggers.ts +18 -4
- package/src/index.ts +4 -2
- package/src/registry.ts +289 -111
- package/src/schema-validator.ts +120 -1
- package/src/stonecrop.ts +230 -106
- package/src/stores/hst.ts +29 -104
- package/src/stores/operation-log.ts +64 -50
- package/src/types/composable.ts +55 -12
- package/src/types/doctype.ts +6 -7
- package/src/types/field-triggers.ts +1 -1
- package/src/types/hst.ts +77 -0
- package/src/types/index.ts +1 -0
- package/src/types/operation-log.ts +4 -4
- package/src/types/schema-validator.ts +2 -0
- package/dist/stonecrop.css +0 -1
package/dist/stores/hst.js
CHANGED
|
@@ -77,17 +77,15 @@ class HST {
|
|
|
77
77
|
// Enhanced HST Proxy with tree navigation
|
|
78
78
|
class HSTProxy {
|
|
79
79
|
target;
|
|
80
|
-
|
|
80
|
+
ancestorPath;
|
|
81
81
|
rootNode;
|
|
82
82
|
doctype;
|
|
83
|
-
parentDoctype;
|
|
84
83
|
hst;
|
|
85
|
-
constructor(target, doctype,
|
|
84
|
+
constructor(target, doctype, ancestorPath = '', rootNode = null) {
|
|
86
85
|
this.target = target;
|
|
87
|
-
this.
|
|
86
|
+
this.ancestorPath = ancestorPath;
|
|
88
87
|
this.rootNode = rootNode || this;
|
|
89
88
|
this.doctype = doctype;
|
|
90
|
-
this.parentDoctype = parentDoctype;
|
|
91
89
|
this.hst = HST.getInstance();
|
|
92
90
|
return new Proxy(this, {
|
|
93
91
|
get(hst, prop) {
|
|
@@ -121,14 +119,19 @@ class HSTProxy {
|
|
|
121
119
|
}
|
|
122
120
|
// Always wrap in HSTProxy for tree navigation
|
|
123
121
|
if (typeof value === 'object' && value !== null && !this.isPrimitive(value)) {
|
|
124
|
-
return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode
|
|
122
|
+
return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode);
|
|
125
123
|
}
|
|
126
124
|
// For primitives, return a minimal wrapper that throws on tree operations
|
|
127
|
-
return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode
|
|
125
|
+
return new HSTProxy(value, nodeDoctype, fullPath, this.rootNode);
|
|
128
126
|
}
|
|
129
127
|
set(path, value, source = 'user') {
|
|
130
128
|
// Get current value for change context
|
|
131
129
|
const fullPath = this.resolvePath(path);
|
|
130
|
+
if (fullPath === undefined) {
|
|
131
|
+
// eslint-disable-next-line no-console
|
|
132
|
+
console.warn('HST.set: resolved path is undefined, skipping operation');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
132
135
|
const beforeValue = this.has(path) ? this.get(path) : undefined;
|
|
133
136
|
// Log operation if not from undo/redo and store is available
|
|
134
137
|
if (source !== 'undo' && source !== 'redo') {
|
|
@@ -194,28 +197,28 @@ class HSTProxy {
|
|
|
194
197
|
}
|
|
195
198
|
}
|
|
196
199
|
// Tree navigation methods
|
|
197
|
-
|
|
198
|
-
if (!this.
|
|
200
|
+
getAncestor() {
|
|
201
|
+
if (!this.ancestorPath)
|
|
199
202
|
return null;
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
if (
|
|
203
|
+
const ancestorSegments = this.ancestorPath.split('.').slice(0, -1);
|
|
204
|
+
const ancestorPath = ancestorSegments.join('.');
|
|
205
|
+
if (ancestorPath === '') {
|
|
203
206
|
return this.rootNode;
|
|
204
207
|
}
|
|
205
208
|
// Return a wrapped node, not raw data
|
|
206
|
-
return this.rootNode.getNode(
|
|
209
|
+
return this.rootNode.getNode(ancestorPath);
|
|
207
210
|
}
|
|
208
211
|
getRoot() {
|
|
209
212
|
return this.rootNode;
|
|
210
213
|
}
|
|
211
214
|
getPath() {
|
|
212
|
-
return this.
|
|
215
|
+
return this.ancestorPath;
|
|
213
216
|
}
|
|
214
217
|
getDepth() {
|
|
215
|
-
return this.
|
|
218
|
+
return this.ancestorPath ? this.ancestorPath.split('.').length : 0;
|
|
216
219
|
}
|
|
217
220
|
getBreadcrumbs() {
|
|
218
|
-
return this.
|
|
221
|
+
return this.ancestorPath ? this.ancestorPath.split('.') : [];
|
|
219
222
|
}
|
|
220
223
|
/**
|
|
221
224
|
* Trigger an XState transition with optional context data
|
|
@@ -223,7 +226,7 @@ class HSTProxy {
|
|
|
223
226
|
async triggerTransition(transition, context) {
|
|
224
227
|
const triggerEngine = getGlobalTriggerEngine();
|
|
225
228
|
// Determine doctype and recordId from the current path
|
|
226
|
-
const pathSegments = this.
|
|
229
|
+
const pathSegments = this.ancestorPath.split('.');
|
|
227
230
|
let doctype = this.doctype;
|
|
228
231
|
let recordId;
|
|
229
232
|
// If we're at the root level and this is a StonecropStore, use the first path segment as the doctype
|
|
@@ -236,7 +239,7 @@ class HSTProxy {
|
|
|
236
239
|
}
|
|
237
240
|
// Build transition context
|
|
238
241
|
const transitionContext = {
|
|
239
|
-
path: this.
|
|
242
|
+
path: this.ancestorPath,
|
|
240
243
|
fieldname: '', // No specific field for transitions
|
|
241
244
|
beforeValue: undefined,
|
|
242
245
|
afterValue: undefined,
|
|
@@ -255,7 +258,7 @@ class HSTProxy {
|
|
|
255
258
|
if (logStore && typeof logStore.addOperation === 'function') {
|
|
256
259
|
logStore.addOperation({
|
|
257
260
|
type: 'transition',
|
|
258
|
-
path: this.
|
|
261
|
+
path: this.ancestorPath,
|
|
259
262
|
fieldname: transition,
|
|
260
263
|
beforeValue: context?.currentState,
|
|
261
264
|
afterValue: context?.targetState,
|
|
@@ -276,8 +279,8 @@ class HSTProxy {
|
|
|
276
279
|
// Private helper methods
|
|
277
280
|
resolvePath(path) {
|
|
278
281
|
if (path === '')
|
|
279
|
-
return this.
|
|
280
|
-
return this.
|
|
282
|
+
return this.ancestorPath ?? '';
|
|
283
|
+
return this.ancestorPath ? `${this.ancestorPath}.${path}` : path;
|
|
281
284
|
}
|
|
282
285
|
resolveValue(path) {
|
|
283
286
|
// Handle empty path - return the target object
|
|
@@ -302,7 +305,7 @@ class HSTProxy {
|
|
|
302
305
|
const segments = this.parsePath(path);
|
|
303
306
|
const lastSegment = segments.pop();
|
|
304
307
|
let current = this.target;
|
|
305
|
-
// Navigate to
|
|
308
|
+
// Navigate to ancestor object
|
|
306
309
|
for (const segment of segments) {
|
|
307
310
|
current = this.getProperty(current, segment);
|
|
308
311
|
if (current === null || current === undefined) {
|
|
@@ -482,13 +485,12 @@ class HSTProxy {
|
|
|
482
485
|
*
|
|
483
486
|
* @param target - The target object to wrap with HST functionality
|
|
484
487
|
* @param doctype - The document type identifier
|
|
485
|
-
* @param parentDoctype - Optional parent document type identifier
|
|
486
488
|
* @returns A new HSTNode proxy instance
|
|
487
489
|
*
|
|
488
490
|
* @public
|
|
489
491
|
*/
|
|
490
|
-
function createHST(target, doctype
|
|
491
|
-
return new HSTProxy(target, doctype, '', null
|
|
492
|
+
function createHST(target, doctype) {
|
|
493
|
+
return new HSTProxy(target, doctype, '', null);
|
|
492
494
|
}
|
|
493
495
|
// Export everything
|
|
494
496
|
export { HSTProxy, HST, createHST };
|
|
@@ -75,8 +75,7 @@ export const useOperationLogStore = defineStore('hst-operation-log', () => {
|
|
|
75
75
|
const currentIndex = ref(-1); // Points to the last applied operation
|
|
76
76
|
const clientId = ref(generateId());
|
|
77
77
|
const batchMode = ref(false);
|
|
78
|
-
const
|
|
79
|
-
const currentBatchId = ref(null);
|
|
78
|
+
const batchStack = ref([]);
|
|
80
79
|
// Computed
|
|
81
80
|
const canUndo = computed(() => {
|
|
82
81
|
// Can undo if there are operations and we're not at the beginning
|
|
@@ -141,9 +140,9 @@ export const useOperationLogStore = defineStore('hst-operation-log', () => {
|
|
|
141
140
|
if (config.value.operationFilter && !config.value.operationFilter(fullOperation)) {
|
|
142
141
|
return fullOperation.id;
|
|
143
142
|
}
|
|
144
|
-
// If in batch mode, collect operations
|
|
145
|
-
if (batchMode.value) {
|
|
146
|
-
|
|
143
|
+
// If in batch mode, collect operations in current batch
|
|
144
|
+
if (batchMode.value && batchStack.value.length > 0) {
|
|
145
|
+
batchStack.value[batchStack.value.length - 1].operations.push(fullOperation);
|
|
147
146
|
return fullOperation.id;
|
|
148
147
|
}
|
|
149
148
|
// Remove any operations after current index (they become invalid after new operation)
|
|
@@ -170,22 +169,30 @@ export const useOperationLogStore = defineStore('hst-operation-log', () => {
|
|
|
170
169
|
*/
|
|
171
170
|
function startBatch() {
|
|
172
171
|
batchMode.value = true;
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
batchStack.value.push({
|
|
173
|
+
id: generateId(),
|
|
174
|
+
operations: [],
|
|
175
|
+
});
|
|
175
176
|
}
|
|
176
177
|
/**
|
|
177
178
|
* Commit batch - create a single batch operation
|
|
178
179
|
*/
|
|
179
180
|
function commitBatch(description) {
|
|
180
|
-
if (!batchMode.value ||
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
181
|
+
if (!batchMode.value || batchStack.value.length === 0) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
const currentBatchData = batchStack.value.pop();
|
|
185
|
+
const batchOperations = currentBatchData.operations;
|
|
186
|
+
if (batchOperations.length === 0) {
|
|
187
|
+
// Empty batch - just pop and continue if there are outer batches
|
|
188
|
+
if (batchStack.value.length === 0) {
|
|
189
|
+
batchMode.value = false;
|
|
190
|
+
}
|
|
184
191
|
return null;
|
|
185
192
|
}
|
|
186
|
-
const batchId =
|
|
187
|
-
const allReversible =
|
|
188
|
-
// Create
|
|
193
|
+
const batchId = currentBatchData.id;
|
|
194
|
+
const allReversible = batchOperations.every(op => op.reversible);
|
|
195
|
+
// Create ancestor batch operation
|
|
189
196
|
const batchOperation = {
|
|
190
197
|
id: batchId,
|
|
191
198
|
type: 'batch',
|
|
@@ -193,39 +200,44 @@ export const useOperationLogStore = defineStore('hst-operation-log', () => {
|
|
|
193
200
|
fieldname: '',
|
|
194
201
|
beforeValue: null,
|
|
195
202
|
afterValue: null,
|
|
196
|
-
doctype:
|
|
203
|
+
doctype: batchOperations[0]?.doctype || '',
|
|
197
204
|
timestamp: new Date(),
|
|
198
205
|
source: 'user',
|
|
199
206
|
reversible: allReversible,
|
|
200
207
|
irreversibleReason: allReversible ? undefined : 'Contains irreversible operations',
|
|
201
|
-
|
|
208
|
+
descendantOperationIds: batchOperations.map(op => op.id),
|
|
202
209
|
metadata: { description },
|
|
203
210
|
};
|
|
204
|
-
// Add
|
|
205
|
-
|
|
206
|
-
op.
|
|
211
|
+
// Add ancestor operation ID to all descendants
|
|
212
|
+
batchOperations.forEach(op => {
|
|
213
|
+
op.ancestorOperationId = batchId;
|
|
207
214
|
});
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
|
|
215
|
+
// If we're inside a ancestor batch, add this batch as a descendant of the ancestor
|
|
216
|
+
if (batchStack.value.length > 0) {
|
|
217
|
+
// Nested batch - add the batch operation to the ancestor batch
|
|
218
|
+
batchStack.value[batchStack.value.length - 1].operations.push(batchOperation);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
// Top-level batch - add to the operations log
|
|
222
|
+
operations.value.push(...batchOperations, batchOperation);
|
|
223
|
+
currentIndex.value = operations.value.length - 1;
|
|
224
|
+
}
|
|
211
225
|
// Broadcast batch
|
|
212
226
|
if (config.value.enableCrossTabSync) {
|
|
213
|
-
broadcastBatch(
|
|
227
|
+
broadcastBatch(batchOperations, batchOperation);
|
|
214
228
|
}
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
return result;
|
|
229
|
+
// If no more batches on stack, exit batch mode
|
|
230
|
+
if (batchStack.value.length === 0) {
|
|
231
|
+
batchMode.value = false;
|
|
232
|
+
}
|
|
233
|
+
return batchId;
|
|
221
234
|
}
|
|
222
235
|
/**
|
|
223
236
|
* Cancel batch mode without committing
|
|
224
237
|
*/
|
|
225
238
|
function cancelBatch() {
|
|
239
|
+
batchStack.value = [];
|
|
226
240
|
batchMode.value = false;
|
|
227
|
-
currentBatch.value = [];
|
|
228
|
-
currentBatchId.value = null;
|
|
229
241
|
}
|
|
230
242
|
/**
|
|
231
243
|
* Undo the last operation
|
|
@@ -244,13 +256,13 @@ export const useOperationLogStore = defineStore('hst-operation-log', () => {
|
|
|
244
256
|
}
|
|
245
257
|
try {
|
|
246
258
|
// Handle batch operations
|
|
247
|
-
if (operation.type === 'batch' && operation.
|
|
248
|
-
// Undo all
|
|
249
|
-
for (let i = operation.
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
if (
|
|
253
|
-
revertOperation(
|
|
259
|
+
if (operation.type === 'batch' && operation.descendantOperationIds) {
|
|
260
|
+
// Undo all descendant operations in reverse order
|
|
261
|
+
for (let i = operation.descendantOperationIds.length - 1; i >= 0; i--) {
|
|
262
|
+
const descendantId = operation.descendantOperationIds[i];
|
|
263
|
+
const descendantOp = operations.value.find(op => op.id === descendantId);
|
|
264
|
+
if (descendantOp) {
|
|
265
|
+
revertOperation(descendantOp, store);
|
|
254
266
|
}
|
|
255
267
|
}
|
|
256
268
|
}
|
|
@@ -283,12 +295,12 @@ export const useOperationLogStore = defineStore('hst-operation-log', () => {
|
|
|
283
295
|
const operation = operations.value[currentIndex.value + 1];
|
|
284
296
|
try {
|
|
285
297
|
// Handle batch operations
|
|
286
|
-
if (operation.type === 'batch' && operation.
|
|
287
|
-
// Redo all
|
|
288
|
-
for (const
|
|
289
|
-
const
|
|
290
|
-
if (
|
|
291
|
-
applyOperation(
|
|
298
|
+
if (operation.type === 'batch' && operation.descendantOperationIds) {
|
|
299
|
+
// Redo all descendant operations in order
|
|
300
|
+
for (const descendantId of operation.descendantOperationIds) {
|
|
301
|
+
const descendantOp = operations.value.find(op => op.id === descendantId);
|
|
302
|
+
if (descendantOp) {
|
|
303
|
+
applyOperation(descendantOp, store);
|
|
292
304
|
}
|
|
293
305
|
}
|
|
294
306
|
}
|
|
@@ -440,12 +452,12 @@ export const useOperationLogStore = defineStore('hst-operation-log', () => {
|
|
|
440
452
|
};
|
|
441
453
|
broadcastChannel.postMessage(serializeForBroadcast(message));
|
|
442
454
|
}
|
|
443
|
-
function broadcastBatch(
|
|
455
|
+
function broadcastBatch(descendantOps, batchOp) {
|
|
444
456
|
if (!broadcastChannel)
|
|
445
457
|
return;
|
|
446
458
|
const message = {
|
|
447
459
|
type: 'operation',
|
|
448
|
-
operations: [...
|
|
460
|
+
operations: [...descendantOps, batchOp],
|
|
449
461
|
clientId: clientId.value,
|
|
450
462
|
timestamp: new Date(),
|
|
451
463
|
};
|
|
@@ -473,7 +485,7 @@ export const useOperationLogStore = defineStore('hst-operation-log', () => {
|
|
|
473
485
|
};
|
|
474
486
|
broadcastChannel.postMessage(serializeForBroadcast(message));
|
|
475
487
|
}
|
|
476
|
-
const persistedData = useLocalStorage('stonecrop-
|
|
488
|
+
const persistedData = useLocalStorage('stonecrop-operations', null, {
|
|
477
489
|
serializer: {
|
|
478
490
|
read: (v) => {
|
|
479
491
|
try {
|
|
File without changes
|
package/dist/types/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stonecrop/stonecrop",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"pinia-shared-state": "^1.0.1",
|
|
35
35
|
"pinia-xstate": "^3.0.0",
|
|
36
36
|
"xstate": "^5.25.0",
|
|
37
|
-
"@stonecrop/schema": "0.
|
|
37
|
+
"@stonecrop/schema": "0.11.1"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
40
|
"pinia": "^3.0.4",
|
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
"vue-router": "^5.0.2",
|
|
61
61
|
"vite": "^7.3.1",
|
|
62
62
|
"vitest": "^4.0.18",
|
|
63
|
-
"stonecrop
|
|
64
|
-
"@stonecrop/atable": "0.
|
|
65
|
-
"
|
|
63
|
+
"@stonecrop/aform": "0.11.1",
|
|
64
|
+
"@stonecrop/atable": "0.11.1",
|
|
65
|
+
"stonecrop-rig": "0.7.0"
|
|
66
66
|
},
|
|
67
67
|
"description": "Schema-driven framework with XState workflows and HST state management",
|
|
68
68
|
"publishConfig": {
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import type { FetchStrategy, CustomFetch } from '@stonecrop/schema'
|
|
2
|
+
import { computed, inject, ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import Doctype from '../doctype'
|
|
5
|
+
import { Stonecrop } from '../stonecrop'
|
|
6
|
+
import type { LazyLink } from '../types/composable'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get the lazy link state for a specific link field on a doctype record.
|
|
10
|
+
*
|
|
11
|
+
* This composable provides reactive state for lazy-loaded links:
|
|
12
|
+
* - `loading`: true while fetching
|
|
13
|
+
* - `loaded`: true after successful fetch (permanent until reload)
|
|
14
|
+
* - `error`: error state if any
|
|
15
|
+
* - `reload()`: explicitly trigger a fetch
|
|
16
|
+
* - `data`: computed from HST, or undefined if not loaded
|
|
17
|
+
*
|
|
18
|
+
* The reload() function respects the link's fetch strategy:
|
|
19
|
+
* - `sync`: fetches via GraphQL query through fetchNestedData
|
|
20
|
+
* - `lazy`: fetches via GraphQL query through fetchNestedData
|
|
21
|
+
* - `custom`: invokes the serialized handler function directly
|
|
22
|
+
*
|
|
23
|
+
* @param doctype - The doctype instance
|
|
24
|
+
* @param recordId - The record ID
|
|
25
|
+
* @param linkFieldname - The link fieldname to load
|
|
26
|
+
* @returns LazyLink with loading, loaded, error, reload, and data
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
export function useLazyLink(doctype: Doctype, recordId: string, linkFieldname: string): LazyLink {
|
|
30
|
+
const stonecropInstance = inject<Stonecrop>('$stonecrop') || Stonecrop._root
|
|
31
|
+
|
|
32
|
+
if (!stonecropInstance) {
|
|
33
|
+
throw new Error('Stonecrop instance not available. Ensure useStonecrop() has been called first.')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const loading = ref(false)
|
|
37
|
+
const loaded = ref(false)
|
|
38
|
+
const error = ref<Error | null>(null)
|
|
39
|
+
|
|
40
|
+
const hstStore = stonecropInstance.getStore()
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build the HST path for a lazy link field
|
|
44
|
+
*/
|
|
45
|
+
const getLinkPath = (): string => {
|
|
46
|
+
const slug = doctype.slug || doctype.doctype
|
|
47
|
+
return `${slug}.${recordId}.${linkFieldname}`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the link declaration from the doctype schema
|
|
52
|
+
*/
|
|
53
|
+
const getLinkDeclaration = (): FetchStrategy | undefined => {
|
|
54
|
+
return doctype.links?.[linkFieldname]?.fetch
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Invoke a custom fetch handler
|
|
59
|
+
* The handler is a serialized function string that we execute via new Function()
|
|
60
|
+
*/
|
|
61
|
+
const invokeCustomHandler = async (handler: CustomFetch['handler']): Promise<any> => {
|
|
62
|
+
try {
|
|
63
|
+
// Create function from serialized string and invoke it
|
|
64
|
+
// The function receives the stonecrop instance and path as parameters
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-implied-eval
|
|
66
|
+
const fn = new Function(
|
|
67
|
+
'stonecrop',
|
|
68
|
+
'path',
|
|
69
|
+
'hst',
|
|
70
|
+
`
|
|
71
|
+
return (${handler})(stonecrop, path, hst)
|
|
72
|
+
`
|
|
73
|
+
)
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
75
|
+
return await fn(stonecropInstance, getLinkPath(), hstStore)
|
|
76
|
+
} catch (err) {
|
|
77
|
+
throw new Error(`Custom handler failed: ${err instanceof Error ? err.message : String(err)}`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Fetch the link data using the appropriate strategy
|
|
83
|
+
*/
|
|
84
|
+
const fetchLinkData = async (): Promise<void> => {
|
|
85
|
+
const linkFetch = getLinkDeclaration()
|
|
86
|
+
const ancestorPath = `${doctype.slug || doctype.doctype}.${recordId}`
|
|
87
|
+
|
|
88
|
+
if (linkFetch?.method === 'custom') {
|
|
89
|
+
// Ensure ancestor path exists before invoking custom handler
|
|
90
|
+
if (!hstStore.has(ancestorPath)) {
|
|
91
|
+
hstStore.set(ancestorPath, {}, 'system')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Custom handler - invoke directly
|
|
95
|
+
const result = await invokeCustomHandler(linkFetch.handler)
|
|
96
|
+
|
|
97
|
+
// Store result in HST at the link path
|
|
98
|
+
hstStore.set(getLinkPath(), result, 'system')
|
|
99
|
+
} else {
|
|
100
|
+
// sync or lazy (both use fetchNestedData but with different includeNested)
|
|
101
|
+
// For lazy links, we still use fetchNestedData but only for this specific link
|
|
102
|
+
await stonecropInstance.fetchNestedData(ancestorPath, doctype, recordId, { includeNested: [linkFieldname] })
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Explicitly reload the lazy link data
|
|
108
|
+
*/
|
|
109
|
+
const reload = async (): Promise<void> => {
|
|
110
|
+
if (loading.value) return
|
|
111
|
+
|
|
112
|
+
loading.value = true
|
|
113
|
+
error.value = null
|
|
114
|
+
|
|
115
|
+
try {
|
|
116
|
+
await fetchLinkData()
|
|
117
|
+
loaded.value = true
|
|
118
|
+
} catch (err) {
|
|
119
|
+
error.value = err instanceof Error ? err : new Error(String(err))
|
|
120
|
+
throw err
|
|
121
|
+
} finally {
|
|
122
|
+
loading.value = false
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Computed property that returns the loaded data from HST
|
|
128
|
+
*/
|
|
129
|
+
const data = computed(() => {
|
|
130
|
+
if (!loaded.value) return undefined
|
|
131
|
+
return hstStore.get(getLinkPath())
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
// State
|
|
136
|
+
loading,
|
|
137
|
+
loaded,
|
|
138
|
+
error,
|
|
139
|
+
|
|
140
|
+
// Computed
|
|
141
|
+
data,
|
|
142
|
+
|
|
143
|
+
// Actions
|
|
144
|
+
reload,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -2,7 +2,7 @@ import { useMagicKeys, whenever } from '@vueuse/core'
|
|
|
2
2
|
import { storeToRefs } from 'pinia'
|
|
3
3
|
import { getCurrentInstance, inject } from 'vue'
|
|
4
4
|
|
|
5
|
-
import type { HSTNode } from '../
|
|
5
|
+
import type { HSTNode } from '../types/hst'
|
|
6
6
|
import { useOperationLogStore } from '../stores/operation-log'
|
|
7
7
|
import type { OperationLogConfig } from '../types/operation-log'
|
|
8
8
|
|