@nocobase/flow-engine 2.2.0-beta.9 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/acl/Acl.d.ts +2 -1
- package/lib/acl/Acl.js +28 -0
- package/lib/components/FlowContextSelector.js +62 -13
- package/lib/components/FormItem.js +11 -7
- package/lib/components/MobilePopup.js +39 -10
- package/lib/components/MobilePopup.style.js +11 -1
- package/lib/components/subModel/LazyDropdown.js +62 -33
- package/lib/components/variables/VariableHybridInput.d.ts +9 -0
- package/lib/components/variables/VariableHybridInput.js +146 -17
- package/lib/components/variables/VariableInput.js +19 -7
- package/lib/components/variables/VariableTag.js +48 -36
- package/lib/components/variables/types.d.ts +21 -0
- package/lib/flowContext.d.ts +12 -1
- package/lib/flowContext.js +47 -6
- package/lib/flowEngine.js +6 -0
- package/lib/flowI18n.js +3 -3
- package/lib/locale/en-US.json +2 -0
- package/lib/locale/index.d.ts +4 -0
- package/lib/locale/zh-CN.json +2 -0
- package/lib/resources/flowResource.js +1 -0
- package/lib/types.d.ts +3 -1
- package/lib/types.js +1 -0
- package/lib/utils/associationObjectVariable.d.ts +10 -0
- package/lib/utils/associationObjectVariable.js +10 -7
- package/lib/utils/dateVariable.d.ts +22 -0
- package/lib/utils/dateVariable.js +123 -16
- package/lib/utils/dirtyAwareApiClient.d.ts +1 -0
- package/lib/utils/dirtyAwareApiClient.js +280 -13
- package/lib/utils/index.d.ts +3 -3
- package/lib/utils/index.js +8 -0
- package/lib/utils/loadedPageCache.d.ts +1 -0
- package/lib/utils/loadedPageCache.js +6 -0
- package/lib/utils/params-resolvers.d.ts +3 -0
- package/lib/utils/params-resolvers.js +10 -0
- package/lib/utils/variablesParams.js +5 -0
- package/lib/views/createViewMeta.d.ts +1 -0
- package/lib/views/createViewMeta.js +53 -22
- package/package.json +4 -4
- package/src/__tests__/createViewMeta.popup.test.ts +84 -1
- package/src/__tests__/flowContext.test.ts +8 -0
- package/src/__tests__/flowI18n.test.ts +11 -0
- package/src/__tests__/objectVariable.test.ts +6 -1
- package/src/__tests__/runjsFormSubmit.test.ts +138 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +72 -6
- package/src/acl/Acl.tsx +36 -1
- package/src/acl/__tests__/Acl.test.tsx +70 -0
- package/src/components/FlowContextSelector.tsx +73 -12
- package/src/components/FormItem.tsx +12 -7
- package/src/components/MobilePopup.style.ts +12 -1
- package/src/components/MobilePopup.tsx +42 -10
- package/src/components/__tests__/FormItem.test.tsx +17 -2
- package/src/components/__tests__/MobilePopup.test.tsx +150 -0
- package/src/components/subModel/LazyDropdown.tsx +71 -38
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +85 -2
- package/src/components/subModel/__tests__/LazyDropdown.test.tsx +202 -0
- package/src/components/variables/VariableHybridInput.tsx +185 -14
- package/src/components/variables/VariableInput.tsx +32 -7
- package/src/components/variables/VariableTag.tsx +51 -37
- package/src/components/variables/__tests__/FlowContextSelector.test.tsx +95 -3
- package/src/components/variables/__tests__/VariableHybridInput.test.tsx +212 -0
- package/src/components/variables/__tests__/VariableInput.test.tsx +202 -6
- package/src/components/variables/__tests__/VariableTag.test.tsx +80 -0
- package/src/components/variables/types.ts +21 -0
- package/src/flowContext.ts +79 -6
- package/src/flowEngine.ts +6 -0
- package/src/flowI18n.ts +8 -3
- package/src/locale/__tests__/index.test.ts +21 -0
- package/src/locale/en-US.json +2 -0
- package/src/locale/zh-CN.json +2 -0
- package/src/resources/__tests__/flowResource.test.ts +3 -0
- package/src/resources/flowResource.ts +1 -0
- package/src/types.ts +2 -0
- package/src/utils/__tests__/dateVariable.test.ts +57 -4
- package/src/utils/__tests__/dirtyAwareApiClient.test.ts +321 -0
- package/src/utils/__tests__/variablesParams.test.ts +28 -1
- package/src/utils/associationObjectVariable.ts +9 -6
- package/src/utils/dateVariable.ts +145 -18
- package/src/utils/dirtyAwareApiClient.ts +348 -13
- package/src/utils/index.ts +17 -2
- package/src/utils/loadedPageCache.ts +7 -0
- package/src/utils/params-resolvers.ts +12 -0
- package/src/utils/variablesParams.ts +10 -0
- package/src/views/createViewMeta.ts +52 -18
|
@@ -7,13 +7,28 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import
|
|
10
|
+
import { APIClient } from '@nocobase/sdk';
|
|
11
|
+
import type { ActionParams, IResource, RequestOptions } from '@nocobase/sdk';
|
|
11
12
|
import type { FlowContext } from '../flowContext';
|
|
12
13
|
import { getDataSourceKeyFromHeaders, markDataSourceDirty } from './dataSourceDirty';
|
|
13
14
|
|
|
14
15
|
type ResourceActionFn = (params?: ActionParams, opts?: unknown) => Promise<unknown>;
|
|
15
16
|
|
|
16
17
|
export const SKIP_DATA_SOURCE_DIRTY = '__nocobaseSkipDataSourceDirty';
|
|
18
|
+
// Carries one logical mutation through APIClient's request/resource cross-dispatch.
|
|
19
|
+
const DIRTY_DISPATCH_TOKEN = Symbol('nocobaseDirtyDispatchToken');
|
|
20
|
+
|
|
21
|
+
type DirtyDispatchToken = {
|
|
22
|
+
marked: boolean;
|
|
23
|
+
requestKey?: string;
|
|
24
|
+
resourceKey?: string;
|
|
25
|
+
skip: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type DirtyDispatchFrame = {
|
|
29
|
+
key?: string;
|
|
30
|
+
token: DirtyDispatchToken;
|
|
31
|
+
};
|
|
17
32
|
|
|
18
33
|
type ResourceRequestOptions = RequestOptions & {
|
|
19
34
|
resource?: unknown;
|
|
@@ -21,14 +36,25 @@ type ResourceRequestOptions = RequestOptions & {
|
|
|
21
36
|
action?: unknown;
|
|
22
37
|
headers?: unknown;
|
|
23
38
|
[SKIP_DATA_SOURCE_DIRTY]?: boolean;
|
|
39
|
+
[DIRTY_DISPATCH_TOKEN]?: DirtyDispatchToken;
|
|
24
40
|
};
|
|
25
41
|
|
|
26
42
|
type DirtyResourceAction = {
|
|
27
43
|
dataSourceKey?: string;
|
|
28
44
|
resourceName: string;
|
|
45
|
+
resourceOf?: unknown;
|
|
29
46
|
actionName: string;
|
|
30
47
|
};
|
|
31
48
|
|
|
49
|
+
export const PREPARE_CONTEXT_RESOURCE_ACTION_PARAMS = Symbol('prepareContextResourceActionParams');
|
|
50
|
+
|
|
51
|
+
type ContextResourceActionParamsPreparer = {
|
|
52
|
+
[PREPARE_CONTEXT_RESOURCE_ACTION_PARAMS]?: (
|
|
53
|
+
action: DirtyResourceAction,
|
|
54
|
+
params: ActionParams | undefined,
|
|
55
|
+
) => ActionParams | undefined;
|
|
56
|
+
};
|
|
57
|
+
|
|
32
58
|
type ApiUrlProvider = {
|
|
33
59
|
getApiUrl?: (pathname?: string) => string;
|
|
34
60
|
};
|
|
@@ -312,6 +338,7 @@ function resolveDirtyResourceActionFromResource(
|
|
|
312
338
|
|
|
313
339
|
return {
|
|
314
340
|
resourceName: normalizedResourceName,
|
|
341
|
+
resourceOf,
|
|
315
342
|
actionName: normalizedActionName,
|
|
316
343
|
};
|
|
317
344
|
}
|
|
@@ -329,6 +356,53 @@ function resolveDirtyResourceAction(
|
|
|
329
356
|
return parseDirtyResourceActionFromUrl(options?.url, context);
|
|
330
357
|
}
|
|
331
358
|
|
|
359
|
+
function getDirtyResourceActionDispatchKey(
|
|
360
|
+
dirtyResourceAction: DirtyResourceAction | undefined,
|
|
361
|
+
headers: unknown,
|
|
362
|
+
): string | undefined {
|
|
363
|
+
if (!dirtyResourceAction) {
|
|
364
|
+
return undefined;
|
|
365
|
+
}
|
|
366
|
+
return JSON.stringify([
|
|
367
|
+
dirtyResourceAction.dataSourceKey || getDataSourceKeyFromHeaders(headers),
|
|
368
|
+
dirtyResourceAction.resourceName,
|
|
369
|
+
dirtyResourceAction.actionName,
|
|
370
|
+
]);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function getRequestDispatchKey(options: ResourceRequestOptions, context: FlowContext): string | undefined {
|
|
374
|
+
return getDirtyResourceActionDispatchKey(resolveDirtyResourceAction(options, context), options.headers);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function getResourceDispatchKey(name: unknown, of: unknown, headers: unknown): string | undefined {
|
|
378
|
+
const resourceName = String(name ?? '').trim();
|
|
379
|
+
if (!resourceName) {
|
|
380
|
+
return undefined;
|
|
381
|
+
}
|
|
382
|
+
return JSON.stringify([resourceName, String(of ?? ''), getDataSourceKeyFromHeaders(headers)]);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function getMatchingRequestToken(
|
|
386
|
+
token: DirtyDispatchToken | undefined,
|
|
387
|
+
requestKey: string | undefined,
|
|
388
|
+
): DirtyDispatchToken | undefined {
|
|
389
|
+
// A matching key prevents helper requests inside an override from sharing the outer mutation token.
|
|
390
|
+
if (!token || !requestKey || (token.requestKey && token.requestKey !== requestKey)) {
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
return token;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function getMatchingResourceToken(
|
|
397
|
+
token: DirtyDispatchToken | undefined,
|
|
398
|
+
resourceKey: string | undefined,
|
|
399
|
+
): DirtyDispatchToken | undefined {
|
|
400
|
+
if (!token || !resourceKey || (token.resourceKey && token.resourceKey !== resourceKey)) {
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
return token;
|
|
404
|
+
}
|
|
405
|
+
|
|
332
406
|
function markResourceActionDataSourceDirty(
|
|
333
407
|
context: FlowContext,
|
|
334
408
|
dirtyResourceAction: DirtyResourceAction,
|
|
@@ -342,12 +416,31 @@ function markResourceActionDataSourceDirty(
|
|
|
342
416
|
});
|
|
343
417
|
}
|
|
344
418
|
|
|
419
|
+
function markResourceActionDataSourceDirtyOnce(
|
|
420
|
+
token: DirtyDispatchToken,
|
|
421
|
+
context: FlowContext,
|
|
422
|
+
dirtyResourceAction: DirtyResourceAction | undefined,
|
|
423
|
+
headers: unknown,
|
|
424
|
+
) {
|
|
425
|
+
if (token.skip || token.marked || !dirtyResourceAction || !isMutatingResourceAction(dirtyResourceAction.actionName)) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
token.marked = true;
|
|
429
|
+
markResourceActionDataSourceDirty(context, dirtyResourceAction, headers);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function isObjectRecord(value: unknown): value is Record<PropertyKey, unknown> {
|
|
433
|
+
return !!value && typeof value === 'object';
|
|
434
|
+
}
|
|
435
|
+
|
|
345
436
|
function createDirtyAwareResource(
|
|
346
437
|
context: FlowContext,
|
|
347
438
|
resource: IResource,
|
|
348
439
|
resourceName: string,
|
|
349
440
|
resourceOf: unknown,
|
|
350
441
|
headers: unknown,
|
|
442
|
+
requestTokenStack: DirtyDispatchFrame[],
|
|
443
|
+
parentToken?: DirtyDispatchToken,
|
|
351
444
|
): IResource {
|
|
352
445
|
return new Proxy(resource, {
|
|
353
446
|
get(target, prop, receiver) {
|
|
@@ -358,10 +451,54 @@ function createDirtyAwareResource(
|
|
|
358
451
|
|
|
359
452
|
const action = original as ResourceActionFn;
|
|
360
453
|
return async (...args: Parameters<ResourceActionFn>) => {
|
|
361
|
-
const
|
|
454
|
+
const actionOptions = isObjectRecord(args[1]) ? args[1] : undefined;
|
|
362
455
|
const dirtyResourceAction = resolveDirtyResourceActionFromResource(resourceName, resourceOf, prop, context);
|
|
363
|
-
|
|
364
|
-
|
|
456
|
+
const prepareParams = (context as ContextResourceActionParamsPreparer)[PREPARE_CONTEXT_RESOURCE_ACTION_PARAMS];
|
|
457
|
+
const actionParams =
|
|
458
|
+
dirtyResourceAction && typeof prepareParams === 'function'
|
|
459
|
+
? prepareParams.call(
|
|
460
|
+
context,
|
|
461
|
+
{
|
|
462
|
+
...dirtyResourceAction,
|
|
463
|
+
dataSourceKey: dirtyResourceAction.dataSourceKey || getDataSourceKeyFromHeaders(headers),
|
|
464
|
+
},
|
|
465
|
+
args[0],
|
|
466
|
+
)
|
|
467
|
+
: args[0];
|
|
468
|
+
const requestKey = getDirtyResourceActionDispatchKey(dirtyResourceAction, headers);
|
|
469
|
+
const resourceKey = getResourceDispatchKey(resourceName, resourceOf, headers);
|
|
470
|
+
const inheritedToken =
|
|
471
|
+
getMatchingRequestToken(
|
|
472
|
+
actionOptions?.[DIRTY_DISPATCH_TOKEN] as DirtyDispatchToken | undefined,
|
|
473
|
+
requestKey,
|
|
474
|
+
) || getMatchingRequestToken(parentToken, requestKey);
|
|
475
|
+
const token = inheritedToken || { marked: false, skip: false };
|
|
476
|
+
const ownsToken = !inheritedToken;
|
|
477
|
+
token.requestKey ||= requestKey;
|
|
478
|
+
token.resourceKey ||= resourceKey;
|
|
479
|
+
if (actionOptions?.[SKIP_DATA_SOURCE_DIRTY]) {
|
|
480
|
+
token.skip = true;
|
|
481
|
+
}
|
|
482
|
+
const forwardedArgs: Parameters<ResourceActionFn> =
|
|
483
|
+
actionOptions || args[1] == null
|
|
484
|
+
? [
|
|
485
|
+
actionParams,
|
|
486
|
+
{
|
|
487
|
+
...actionOptions,
|
|
488
|
+
[DIRTY_DISPATCH_TOKEN]: token,
|
|
489
|
+
},
|
|
490
|
+
]
|
|
491
|
+
: [actionParams, args[1]];
|
|
492
|
+
let actionResult: Promise<unknown>;
|
|
493
|
+
requestTokenStack.push({ key: requestKey, token });
|
|
494
|
+
try {
|
|
495
|
+
actionResult = Reflect.apply(action, receiver, forwardedArgs);
|
|
496
|
+
} finally {
|
|
497
|
+
requestTokenStack.pop();
|
|
498
|
+
}
|
|
499
|
+
const result = await actionResult;
|
|
500
|
+
if (ownsToken) {
|
|
501
|
+
markResourceActionDataSourceDirtyOnce(token, context, dirtyResourceAction, headers);
|
|
365
502
|
}
|
|
366
503
|
return result;
|
|
367
504
|
};
|
|
@@ -370,34 +507,232 @@ function createDirtyAwareResource(
|
|
|
370
507
|
}
|
|
371
508
|
|
|
372
509
|
function createDirtyAwareApiClient(api: DirtyAwareAPIClient, context: FlowContext): APIClient {
|
|
510
|
+
const baseResource = api.resource;
|
|
511
|
+
const baseRequest = api.request;
|
|
512
|
+
// SDK methods use the dispatch receiver; custom methods keep the original instance for private fields and state.
|
|
513
|
+
const shouldUseResourceDispatchReceiver = baseResource === APIClient.prototype.resource;
|
|
514
|
+
const shouldUseRequestDispatchReceiver = baseRequest === APIClient.prototype.request;
|
|
515
|
+
// Stacks cover synchronous delegation; the symbol token survives async overrides that forward arguments.
|
|
516
|
+
const resourceTokenStack: DirtyDispatchFrame[] = [];
|
|
517
|
+
const requestTokenStack: DirtyDispatchFrame[] = [];
|
|
518
|
+
let hasResourceOverride = false;
|
|
519
|
+
let resourceOverride: unknown;
|
|
520
|
+
let hasRequestOverride = false;
|
|
521
|
+
let requestOverride: unknown;
|
|
522
|
+
|
|
523
|
+
const getCurrentResource = () => (hasResourceOverride ? resourceOverride : resource) as APIClient['resource'];
|
|
524
|
+
const getCurrentRequest = () => (hasRequestOverride ? requestOverride : request) as APIClient['request'];
|
|
525
|
+
|
|
526
|
+
const dispatchResource = (
|
|
527
|
+
token: DirtyDispatchToken | undefined,
|
|
528
|
+
args: Parameters<APIClient['resource']>,
|
|
529
|
+
): IResource => {
|
|
530
|
+
const resourceKey = getResourceDispatchKey(args[0], args[1], args[2]);
|
|
531
|
+
const activeToken = getMatchingResourceToken(token, resourceKey);
|
|
532
|
+
const shouldWrapActions = !!activeToken && hasResourceOverride;
|
|
533
|
+
if (activeToken) {
|
|
534
|
+
activeToken.resourceKey ||= resourceKey;
|
|
535
|
+
resourceTokenStack.push({ key: resourceKey, token: activeToken });
|
|
536
|
+
}
|
|
537
|
+
try {
|
|
538
|
+
const resourceInstance = Reflect.apply(getCurrentResource(), proxy, args);
|
|
539
|
+
if (!shouldWrapActions || !activeToken) {
|
|
540
|
+
return resourceInstance;
|
|
541
|
+
}
|
|
542
|
+
return new Proxy(resourceInstance, {
|
|
543
|
+
get(target, prop, receiver) {
|
|
544
|
+
const original = Reflect.get(target, prop, receiver);
|
|
545
|
+
if (typeof prop !== 'string' || typeof original !== 'function' || !isMutatingResourceAction(prop)) {
|
|
546
|
+
return original;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const action = original as ResourceActionFn;
|
|
550
|
+
return (...actionArgs: Parameters<ResourceActionFn>) => {
|
|
551
|
+
const actionOptions = isObjectRecord(actionArgs[1]) ? actionArgs[1] : undefined;
|
|
552
|
+
const forwardedArgs: Parameters<ResourceActionFn> =
|
|
553
|
+
actionOptions || actionArgs[1] == null
|
|
554
|
+
? [
|
|
555
|
+
actionArgs[0],
|
|
556
|
+
{
|
|
557
|
+
...actionOptions,
|
|
558
|
+
[DIRTY_DISPATCH_TOKEN]: activeToken,
|
|
559
|
+
},
|
|
560
|
+
]
|
|
561
|
+
: actionArgs;
|
|
562
|
+
resourceTokenStack.push({ key: resourceKey, token: activeToken });
|
|
563
|
+
requestTokenStack.push({ key: activeToken.requestKey, token: activeToken });
|
|
564
|
+
try {
|
|
565
|
+
return Reflect.apply(action, receiver, forwardedArgs);
|
|
566
|
+
} finally {
|
|
567
|
+
requestTokenStack.pop();
|
|
568
|
+
resourceTokenStack.pop();
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
},
|
|
572
|
+
});
|
|
573
|
+
} finally {
|
|
574
|
+
if (activeToken) {
|
|
575
|
+
resourceTokenStack.pop();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
const createDispatchReceiver = (token?: DirtyDispatchToken): DirtyAwareAPIClient => {
|
|
581
|
+
const receiver = Object.create(api) as DirtyAwareAPIClient;
|
|
582
|
+
Object.defineProperties(receiver, {
|
|
583
|
+
request: {
|
|
584
|
+
configurable: true,
|
|
585
|
+
value: (<T, R, D>(config: APIClientRequestConfig): Promise<R> => {
|
|
586
|
+
const options = config as ResourceRequestOptions;
|
|
587
|
+
const requestKey = getRequestDispatchKey(options, context);
|
|
588
|
+
const stackFrame = requestTokenStack.at(-1);
|
|
589
|
+
const activeToken =
|
|
590
|
+
getMatchingRequestToken(options?.[DIRTY_DISPATCH_TOKEN], requestKey) ||
|
|
591
|
+
(requestKey && stackFrame?.key === requestKey ? stackFrame.token : undefined) ||
|
|
592
|
+
getMatchingRequestToken(token, requestKey);
|
|
593
|
+
const { [DIRTY_DISPATCH_TOKEN]: _dirtyDispatchToken, ...cleanOptions } = options;
|
|
594
|
+
const configWithToken = activeToken ? { ...cleanOptions, [DIRTY_DISPATCH_TOKEN]: activeToken } : cleanOptions;
|
|
595
|
+
if (activeToken) {
|
|
596
|
+
activeToken.requestKey ||= requestKey;
|
|
597
|
+
requestTokenStack.push({ key: requestKey, token: activeToken });
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
return Reflect.apply(getCurrentRequest(), proxy, [configWithToken]) as Promise<R>;
|
|
601
|
+
} finally {
|
|
602
|
+
if (activeToken) {
|
|
603
|
+
requestTokenStack.pop();
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}) as APIClient['request'],
|
|
607
|
+
},
|
|
608
|
+
resource: {
|
|
609
|
+
configurable: true,
|
|
610
|
+
value: ((...args: Parameters<APIClient['resource']>) => {
|
|
611
|
+
const resourceKey = getResourceDispatchKey(args[0], args[1], args[2]);
|
|
612
|
+
const stackFrame = resourceTokenStack.at(-1);
|
|
613
|
+
const activeToken =
|
|
614
|
+
getMatchingResourceToken(token, resourceKey) ||
|
|
615
|
+
(resourceKey && stackFrame?.key === resourceKey ? stackFrame.token : undefined);
|
|
616
|
+
return dispatchResource(activeToken, args);
|
|
617
|
+
}) as APIClient['resource'],
|
|
618
|
+
},
|
|
619
|
+
});
|
|
620
|
+
return receiver;
|
|
621
|
+
};
|
|
622
|
+
|
|
373
623
|
const resource: APIClient['resource'] = (name, of, headers, cancel) => {
|
|
374
|
-
const
|
|
375
|
-
|
|
624
|
+
const resourceKey = getResourceDispatchKey(name, of, headers);
|
|
625
|
+
const stackFrame = resourceTokenStack.at(-1);
|
|
626
|
+
const parentToken = resourceKey && stackFrame?.key === resourceKey ? stackFrame.token : undefined;
|
|
627
|
+
const receiver = createDispatchReceiver(parentToken);
|
|
628
|
+
const resourceInstance = Reflect.apply(baseResource, shouldUseResourceDispatchReceiver ? receiver : api, [
|
|
629
|
+
name,
|
|
630
|
+
of,
|
|
631
|
+
headers,
|
|
632
|
+
cancel,
|
|
633
|
+
]);
|
|
634
|
+
return createDirtyAwareResource(context, resourceInstance, name, of, headers, requestTokenStack, parentToken);
|
|
376
635
|
};
|
|
377
636
|
|
|
378
637
|
const request = (<T, R, D>(config: APIClientRequestConfig): Promise<R> => {
|
|
379
638
|
const options = config as ResourceRequestOptions;
|
|
639
|
+
const requestKey = getRequestDispatchKey(options, context);
|
|
640
|
+
const stackFrame = requestTokenStack.at(-1);
|
|
641
|
+
const inheritedToken =
|
|
642
|
+
getMatchingRequestToken(options?.[DIRTY_DISPATCH_TOKEN], requestKey) ||
|
|
643
|
+
(requestKey && stackFrame?.key === requestKey ? stackFrame.token : undefined);
|
|
644
|
+
const token = inheritedToken || { marked: false, skip: false };
|
|
645
|
+
const ownsToken = !inheritedToken;
|
|
646
|
+
token.requestKey ||= requestKey;
|
|
647
|
+
if (typeof options.resource === 'string') {
|
|
648
|
+
token.resourceKey ||= getResourceDispatchKey(options.resource, options.resourceOf, options.headers);
|
|
649
|
+
}
|
|
380
650
|
const skipDataSourceDirty = options?.[SKIP_DATA_SOURCE_DIRTY];
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
651
|
+
if (skipDataSourceDirty) {
|
|
652
|
+
token.skip = true;
|
|
653
|
+
}
|
|
654
|
+
const dirtyResourceAction = resolveDirtyResourceAction(options, context);
|
|
655
|
+
const {
|
|
656
|
+
[DIRTY_DISPATCH_TOKEN]: _dirtyDispatchToken,
|
|
657
|
+
[SKIP_DATA_SOURCE_DIRTY]: _skipDataSourceDirty,
|
|
658
|
+
...cleanConfig
|
|
659
|
+
} = options;
|
|
660
|
+
const receiver = createDispatchReceiver(token);
|
|
661
|
+
return (
|
|
662
|
+
Reflect.apply(baseRequest, shouldUseRequestDispatchReceiver ? receiver : api, [cleanConfig]) as Promise<R>
|
|
663
|
+
).then((result) => {
|
|
664
|
+
if (ownsToken) {
|
|
665
|
+
markResourceActionDataSourceDirtyOnce(token, context, dirtyResourceAction, options.headers);
|
|
386
666
|
}
|
|
387
667
|
return result;
|
|
388
668
|
});
|
|
389
669
|
}) as APIClient['request'];
|
|
390
670
|
|
|
671
|
+
const isLockedOwnProperty = (target: DirtyAwareAPIClient, prop: PropertyKey) => {
|
|
672
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
673
|
+
return !!descriptor && !descriptor.configurable;
|
|
674
|
+
};
|
|
675
|
+
|
|
391
676
|
const proxy = new Proxy(api, {
|
|
392
677
|
get(target, prop, receiver) {
|
|
393
678
|
if (prop === 'resource') {
|
|
394
|
-
|
|
679
|
+
if (isLockedOwnProperty(target, prop)) {
|
|
680
|
+
return Reflect.get(target, prop, receiver);
|
|
681
|
+
}
|
|
682
|
+
return hasResourceOverride ? resourceOverride : resource;
|
|
395
683
|
}
|
|
396
684
|
if (prop === 'request') {
|
|
397
|
-
|
|
685
|
+
if (isLockedOwnProperty(target, prop)) {
|
|
686
|
+
return Reflect.get(target, prop, receiver);
|
|
687
|
+
}
|
|
688
|
+
return hasRequestOverride ? requestOverride : request;
|
|
398
689
|
}
|
|
399
690
|
return Reflect.get(target, prop, receiver);
|
|
400
691
|
},
|
|
692
|
+
set(target, prop, value, receiver) {
|
|
693
|
+
if (prop === 'resource') {
|
|
694
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
695
|
+
if (descriptor && (!descriptor.configurable || !Reflect.isExtensible(target))) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
hasResourceOverride = true;
|
|
699
|
+
resourceOverride = value;
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
if (prop === 'request') {
|
|
703
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
704
|
+
if (descriptor && (!descriptor.configurable || !Reflect.isExtensible(target))) {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
hasRequestOverride = true;
|
|
708
|
+
requestOverride = value;
|
|
709
|
+
return true;
|
|
710
|
+
}
|
|
711
|
+
return Reflect.set(target, prop, value, receiver);
|
|
712
|
+
},
|
|
713
|
+
deleteProperty(target, prop) {
|
|
714
|
+
if (prop !== 'resource' && prop !== 'request') {
|
|
715
|
+
return Reflect.deleteProperty(target, prop);
|
|
716
|
+
}
|
|
717
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, prop);
|
|
718
|
+
if (descriptor && (!descriptor.configurable || !Reflect.isExtensible(target))) {
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
if (prop === 'resource') {
|
|
722
|
+
hasResourceOverride = false;
|
|
723
|
+
resourceOverride = undefined;
|
|
724
|
+
} else {
|
|
725
|
+
hasRequestOverride = false;
|
|
726
|
+
requestOverride = undefined;
|
|
727
|
+
}
|
|
728
|
+
return true;
|
|
729
|
+
},
|
|
730
|
+
defineProperty(target, prop, descriptor) {
|
|
731
|
+
if (prop === 'resource' || prop === 'request') {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
return Reflect.defineProperty(target, prop, descriptor);
|
|
735
|
+
},
|
|
401
736
|
}) as APIClient;
|
|
402
737
|
dirtyAwareApiClientProxies.add(proxy);
|
|
403
738
|
return proxy;
|
package/src/utils/index.ts
CHANGED
|
@@ -31,7 +31,12 @@ export { defineAction } from './flow-definitions';
|
|
|
31
31
|
export { isInheritedFrom } from './inheritance';
|
|
32
32
|
|
|
33
33
|
// 参数解析器
|
|
34
|
-
export {
|
|
34
|
+
export {
|
|
35
|
+
buildFlowModelResolveDescriptor,
|
|
36
|
+
resolveCreateModelOptions,
|
|
37
|
+
resolveDefaultParams,
|
|
38
|
+
resolveExpressions,
|
|
39
|
+
} from './params-resolvers';
|
|
35
40
|
|
|
36
41
|
// Schema 工具
|
|
37
42
|
export {
|
|
@@ -48,7 +53,11 @@ export { setupRuntimeContextSteps } from './setupRuntimeContextSteps';
|
|
|
48
53
|
|
|
49
54
|
// Record Proxy 工具
|
|
50
55
|
export { createCollectionContextMeta } from './createCollectionContextMeta';
|
|
51
|
-
export {
|
|
56
|
+
export {
|
|
57
|
+
createAssociationAwareObjectMetaFactory,
|
|
58
|
+
createAssociationSubpathResolver,
|
|
59
|
+
getAssociationFilterByTk,
|
|
60
|
+
} from './associationObjectVariable';
|
|
52
61
|
export {
|
|
53
62
|
buildRecordMeta,
|
|
54
63
|
collectContextParamsForTemplate,
|
|
@@ -83,8 +92,14 @@ export {
|
|
|
83
92
|
isCtxDatePathPrefix,
|
|
84
93
|
isCtxDateExpression,
|
|
85
94
|
parseCtxDateExpression,
|
|
95
|
+
parseCtxDateExpressionConfig,
|
|
86
96
|
resolveCtxDatePath,
|
|
97
|
+
serializeCtxDateExpressionConfig,
|
|
87
98
|
serializeCtxDateValue,
|
|
99
|
+
type CtxDateExpressionConfig,
|
|
100
|
+
type CtxDatePreset,
|
|
101
|
+
type CtxDateRelativeDirection,
|
|
102
|
+
type CtxDateRelativeUnit,
|
|
88
103
|
} from './dateVariable';
|
|
89
104
|
|
|
90
105
|
// RunJS value helpers
|
|
@@ -123,6 +123,13 @@ export const createLoadedPageCache = () => {
|
|
|
123
123
|
}
|
|
124
124
|
},
|
|
125
125
|
|
|
126
|
+
markDirtyForOptions(options?: LoadedPageOptions): void {
|
|
127
|
+
const key = getLoadedPageKey(options);
|
|
128
|
+
if (key) {
|
|
129
|
+
dirtyKeys.add(key);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
126
133
|
shouldBypass(options?: LoadedPageOptions, isFlowSettingsEnabled?: () => boolean): boolean {
|
|
127
134
|
const key = getLoadedPageKey(options);
|
|
128
135
|
if (!key || !dirtyKeys.has(key)) {
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { getValuesByPath } from '@nocobase/shared';
|
|
11
|
+
import { generateFlowModelRdFromToken } from '@nocobase/utils/client';
|
|
11
12
|
import _ from 'lodash';
|
|
12
13
|
import { FlowContext, FlowModelContext, FlowRuntimeContext } from '../flowContext';
|
|
13
14
|
import type { FlowModel } from '../models';
|
|
@@ -78,6 +79,8 @@ export type JSONValue = string | { [key: string]: JSONValue } | JSONValue[];
|
|
|
78
79
|
// =========================
|
|
79
80
|
|
|
80
81
|
type BatchPayload = {
|
|
82
|
+
contractRd?: string;
|
|
83
|
+
rd?: string;
|
|
81
84
|
template: JSONValue;
|
|
82
85
|
contextParams?: ServerContextParams | undefined;
|
|
83
86
|
};
|
|
@@ -170,6 +173,8 @@ export function enqueueVariablesResolve(ctx: FlowRuntimeContext, payload: BatchP
|
|
|
170
173
|
try {
|
|
171
174
|
const batch = items.map((it) => ({
|
|
172
175
|
id: it.id,
|
|
176
|
+
contractRd: it.payload.contractRd,
|
|
177
|
+
rd: it.payload.rd,
|
|
173
178
|
template: it.payload.template,
|
|
174
179
|
contextParams: it.payload.contextParams || {},
|
|
175
180
|
}));
|
|
@@ -218,6 +223,13 @@ export function enqueueVariablesResolve(ctx: FlowRuntimeContext, payload: BatchP
|
|
|
218
223
|
return p;
|
|
219
224
|
}
|
|
220
225
|
|
|
226
|
+
export function buildFlowModelResolveDescriptor(
|
|
227
|
+
ctx: Pick<FlowModelContext, 'api'>,
|
|
228
|
+
flowModelUid?: string | number | null,
|
|
229
|
+
) {
|
|
230
|
+
return generateFlowModelRdFromToken(flowModelUid, ctx?.api?.auth?.token);
|
|
231
|
+
}
|
|
232
|
+
|
|
221
233
|
/**
|
|
222
234
|
* 解析参数中的 {{xxx}} 表达式,自动处理异步属性访问
|
|
223
235
|
*/
|
|
@@ -263,5 +263,15 @@ export async function collectContextParamsForTemplate(
|
|
|
263
263
|
input[key] = built;
|
|
264
264
|
}
|
|
265
265
|
}
|
|
266
|
+
|
|
267
|
+
const viewPaths = usage.view || [];
|
|
268
|
+
if (
|
|
269
|
+
!input.view &&
|
|
270
|
+
viewPaths.some((path) => path === 'record' || path.startsWith('record.') || path.startsWith('record['))
|
|
271
|
+
) {
|
|
272
|
+
const recordRef = inferViewRecordRef(ctx);
|
|
273
|
+
if (recordRef) input.view = { record: recordRef };
|
|
274
|
+
}
|
|
275
|
+
|
|
266
276
|
return buildServerContextParams(ctx, input);
|
|
267
277
|
}
|
|
@@ -15,6 +15,13 @@ import type { FlowView } from './FlowView';
|
|
|
15
15
|
|
|
16
16
|
type PopupModelLike = { getStepParams?: (a: string, b: string) => any } | undefined;
|
|
17
17
|
|
|
18
|
+
function buildPopupSourceRecordRef(ref?: Pick<RecordRef, 'associationName' | 'dataSourceKey' | 'sourceId'>) {
|
|
19
|
+
if (ref?.sourceId == null || ref.sourceId === '' || typeof ref.associationName !== 'string') return undefined;
|
|
20
|
+
const collection = ref.associationName.split('.')[0];
|
|
21
|
+
if (!collection) return undefined;
|
|
22
|
+
return { collection, dataSourceKey: ref.dataSourceKey || 'main', filterByTk: ref.sourceId };
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
function isDefined(value: any) {
|
|
19
26
|
return value !== undefined && value !== null;
|
|
20
27
|
}
|
|
@@ -327,13 +334,15 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
|
|
|
327
334
|
const stack = getViewStack(view);
|
|
328
335
|
const currentIndex = getAnchoredViewStackIndex(view, stack);
|
|
329
336
|
if (currentIndex >= 2) {
|
|
330
|
-
let cur
|
|
337
|
+
let cur = params;
|
|
331
338
|
let level = 1;
|
|
332
339
|
let parentRef = await getParentRecordRef(level, c);
|
|
333
340
|
while (parentRef) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
341
|
+
const parent: PopupVariableParams = { record: parentRef };
|
|
342
|
+
const sourceRecord = buildPopupSourceRecordRef(parentRef);
|
|
343
|
+
if (sourceRecord) parent.sourceRecord = sourceRecord;
|
|
344
|
+
cur.parent = parent;
|
|
345
|
+
cur = parent;
|
|
337
346
|
level += 1;
|
|
338
347
|
parentRef = await getParentRecordRef(level, c);
|
|
339
348
|
}
|
|
@@ -343,20 +352,12 @@ export function createPopupMeta(ctx: FlowContext, anchorView?: FlowView): Proper
|
|
|
343
352
|
}
|
|
344
353
|
|
|
345
354
|
try {
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (parentCollectionName) {
|
|
353
|
-
params.sourceRecord = {
|
|
354
|
-
collection: parentCollectionName,
|
|
355
|
-
dataSourceKey: dsKey,
|
|
356
|
-
filterByTk: srcId,
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
}
|
|
355
|
+
const sourceRecord = buildPopupSourceRecordRef({
|
|
356
|
+
sourceId: inputArgs?.sourceId,
|
|
357
|
+
associationName: inputArgs?.associationName,
|
|
358
|
+
dataSourceKey: inputArgs?.dataSourceKey,
|
|
359
|
+
});
|
|
360
|
+
if (sourceRecord) params.sourceRecord = sourceRecord;
|
|
360
361
|
} catch (err) {
|
|
361
362
|
c.logger?.debug?.({ err }, '[FlowEngine] buildVariablesParams: infer sourceRecord failed');
|
|
362
363
|
}
|
|
@@ -474,12 +475,14 @@ interface PopupNodeResource {
|
|
|
474
475
|
interface PopupNode {
|
|
475
476
|
uid?: string;
|
|
476
477
|
resource: PopupNodeResource;
|
|
478
|
+
sourceRecord?: unknown;
|
|
477
479
|
parent?: PopupNode;
|
|
478
480
|
}
|
|
479
481
|
|
|
480
482
|
export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promise<PopupNode | undefined> {
|
|
481
483
|
const stack = getViewStack(view);
|
|
482
484
|
const currentIndex = getAnchoredViewStackIndex(view, stack);
|
|
485
|
+
const sourceRecord = view?.inputArgs?.parentItem?.value;
|
|
483
486
|
|
|
484
487
|
const openerUids = view?.inputArgs?.openerUids;
|
|
485
488
|
const hasOpener = Array.isArray(openerUids) && openerUids.length > 0;
|
|
@@ -502,6 +505,7 @@ export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promi
|
|
|
502
505
|
filterByTk: args.filterByTk,
|
|
503
506
|
sourceId: args.sourceId,
|
|
504
507
|
},
|
|
508
|
+
...(typeof sourceRecord !== 'undefined' ? { sourceRecord } : {}),
|
|
505
509
|
};
|
|
506
510
|
}
|
|
507
511
|
|
|
@@ -530,6 +534,9 @@ export async function buildPopupRuntime(ctx: FlowContext, view: FlowView): Promi
|
|
|
530
534
|
return node;
|
|
531
535
|
};
|
|
532
536
|
const currentNode = await buildNode(currentIndex);
|
|
537
|
+
if (currentNode && typeof sourceRecord !== 'undefined') {
|
|
538
|
+
currentNode.sourceRecord = sourceRecord;
|
|
539
|
+
}
|
|
533
540
|
return currentNode;
|
|
534
541
|
}
|
|
535
542
|
|
|
@@ -541,6 +548,30 @@ export function registerPopupVariable(ctx: FlowContext, view: FlowView) {
|
|
|
541
548
|
// - 任意层级 parent.parent... 下的 record / sourceRecord 及其子字段
|
|
542
549
|
const POPUP_SERVER_PATH_RE =
|
|
543
550
|
/^(?:record|sourceRecord)(?:\.|$)|^parent(?:\.parent)*(?:\.(?:record|sourceRecord))(?:\.|$)/;
|
|
551
|
+
const shouldResolveSourceRecordOnServer = (path: string): boolean => {
|
|
552
|
+
if (path !== 'sourceRecord' && !path.startsWith('sourceRecord.')) return false;
|
|
553
|
+
|
|
554
|
+
const parentItem = view?.inputArgs?.parentItem;
|
|
555
|
+
if (typeof parentItem?.value === 'undefined') return true;
|
|
556
|
+
|
|
557
|
+
const sourcePath = path === 'sourceRecord' ? '' : path.slice('sourceRecord.'.length);
|
|
558
|
+
if (!sourcePath) return false;
|
|
559
|
+
|
|
560
|
+
const parentItemResolver = view?.inputArgs?.parentItemResolver;
|
|
561
|
+
if (typeof parentItemResolver === 'function') {
|
|
562
|
+
return parentItemResolver(`value.${sourcePath}`);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const segments = sourcePath.split('.').filter(Boolean);
|
|
566
|
+
let current = parentItem.value;
|
|
567
|
+
for (const segment of segments) {
|
|
568
|
+
if (current === null || typeof current !== 'object' || !(segment in current)) {
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
current = current[segment];
|
|
572
|
+
}
|
|
573
|
+
return false;
|
|
574
|
+
};
|
|
544
575
|
// 始终注册 popup 变量:
|
|
545
576
|
// - 若当前视图无可推断记录,仅在元信息中不呈现 record 字段;
|
|
546
577
|
// - 但仍可依据 navigation 推断并展示上级弹窗信息。
|
|
@@ -549,6 +580,9 @@ export function registerPopupVariable(ctx: FlowContext, view: FlowView) {
|
|
|
549
580
|
meta: createPopupMeta(ctx, view),
|
|
550
581
|
resolveOnServer: (p: string) => {
|
|
551
582
|
try {
|
|
583
|
+
if (p === 'sourceRecord' || p.startsWith('sourceRecord.')) {
|
|
584
|
+
return shouldResolveSourceRecordOnServer(p);
|
|
585
|
+
}
|
|
552
586
|
return !!p && POPUP_SERVER_PATH_RE.test(p);
|
|
553
587
|
} catch (_) {
|
|
554
588
|
return false;
|