@nocobase/client-v2 2.2.0-alpha.7 → 2.2.0-alpha.9
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/es/APIClient.d.ts +1 -3
- package/es/components/form/ScanInput/useCodeScanner.d.ts +2 -1
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +6 -0
- package/es/flow/components/filter/FilterGroup.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
- package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
- package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
- package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
- package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
- package/es/flow-compat/routeTypes.d.ts +1 -0
- package/es/index.mjs +105 -104
- package/lib/index.js +117 -116
- package/lib/locale/languageCodes.js +2 -1
- package/package.json +7 -7
- package/src/APIClient.ts +1 -10
- package/src/Application.tsx +4 -0
- package/src/BaseApplication.tsx +3 -2
- package/src/__tests__/app.test.tsx +47 -0
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +28 -0
- package/src/collection-manager/field-validation.ts +1 -1
- package/src/components/form/ScanInput/CodeScanner.tsx +23 -6
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +35 -1
- package/src/components/form/ScanInput/useCodeScanner.ts +16 -3
- package/src/components/form/TypedVariableInput.tsx +12 -8
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +44 -3
- package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +86 -11
- package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
- package/src/flow/components/FieldAssignValueInput.tsx +21 -0
- package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +84 -2
- package/src/flow/components/filter/FilterGroup.tsx +33 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
- package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
- package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
- package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
- package/src/flow/models/base/PageModel/PageTabModel.tsx +184 -3
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
- package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
- package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +572 -1
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +1 -1
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
- package/src/flow-compat/fieldValidationConstants.ts +1 -1
- package/src/flow-compat/routeTypes.ts +1 -0
- package/src/locale/languageCodes.ts +2 -1
- package/src/nocobase-buildin-plugin/index.tsx +12 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { ActionScene } from '@nocobase/flow-engine';
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
|
|
13
|
+
import * as registeredActions from '../index';
|
|
14
|
+
import * as linkageActions from '../linkageRules';
|
|
15
|
+
|
|
16
|
+
const tabLinkageRules = (linkageActions as Record<string, unknown>).tabLinkageRules as
|
|
17
|
+
| typeof linkageActions.menuLinkageRules
|
|
18
|
+
| undefined;
|
|
19
|
+
const linkageSetTabProps = (linkageActions as Record<string, unknown>).linkageSetTabProps as
|
|
20
|
+
| typeof linkageActions.linkageSetMenuItemProps
|
|
21
|
+
| undefined;
|
|
22
|
+
|
|
23
|
+
function createRuntime() {
|
|
24
|
+
let matched = true;
|
|
25
|
+
const setProps = vi.fn(function (this: { props: Record<string, unknown> }, props: Record<string, unknown>) {
|
|
26
|
+
this.props = {
|
|
27
|
+
...this.props,
|
|
28
|
+
...props,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
const setHidden = vi.fn(function (this: { hidden: boolean }, value: boolean) {
|
|
32
|
+
this.hidden = value;
|
|
33
|
+
});
|
|
34
|
+
const model = {
|
|
35
|
+
uid: 'tab-1',
|
|
36
|
+
props: {} as Record<string, unknown>,
|
|
37
|
+
hidden: false,
|
|
38
|
+
__allModels: [],
|
|
39
|
+
setProps,
|
|
40
|
+
setHidden,
|
|
41
|
+
};
|
|
42
|
+
const ctx = {
|
|
43
|
+
app: { jsonLogic: { apply: () => matched } },
|
|
44
|
+
model,
|
|
45
|
+
t: (text: string) => text,
|
|
46
|
+
getAction: (name: string) => (name === 'linkageSetTabProps' ? linkageSetTabProps : undefined),
|
|
47
|
+
resolveJsonTemplate: vi.fn(async (value: unknown) => value),
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
ctx,
|
|
52
|
+
model,
|
|
53
|
+
setMatched(value: boolean) {
|
|
54
|
+
matched = value;
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const hideRule = {
|
|
60
|
+
value: [
|
|
61
|
+
{
|
|
62
|
+
key: 'rule-1',
|
|
63
|
+
title: 'Hide tab',
|
|
64
|
+
enable: true,
|
|
65
|
+
condition: {
|
|
66
|
+
logic: '$and',
|
|
67
|
+
items: [{ path: 'status', operator: '$eq', value: 'visible' }],
|
|
68
|
+
},
|
|
69
|
+
actions: [
|
|
70
|
+
{
|
|
71
|
+
key: 'action-1',
|
|
72
|
+
name: 'linkageSetTabProps',
|
|
73
|
+
params: { value: 'hidden' },
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
describe('tab linkage rules', () => {
|
|
81
|
+
it('should append the tab linkage scene without changing existing scene values', () => {
|
|
82
|
+
const scenes = ActionScene as unknown as Record<string, number>;
|
|
83
|
+
|
|
84
|
+
expect(scenes.MENU_LINKAGE_RULES).toBe(7);
|
|
85
|
+
expect(scenes.TAB_LINKAGE_RULES).toBe(8);
|
|
86
|
+
expect(scenes.TAB_LINKAGE_RULES).toBe(Math.max(...Object.values(scenes).filter(Number.isInteger)));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should register tab linkage actions from the actions entry', () => {
|
|
90
|
+
expect((registeredActions as Record<string, unknown>).tabLinkageRules).toBe(tabLinkageRules);
|
|
91
|
+
expect((registeredActions as Record<string, unknown>).linkageSetTabProps).toBe(linkageSetTabProps);
|
|
92
|
+
expect(tabLinkageRules).toBeDefined();
|
|
93
|
+
expect(linkageSetTabProps).toBeDefined();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should expose tab state and runjs actions in ui schema', () => {
|
|
97
|
+
expect(tabLinkageRules).toBeDefined();
|
|
98
|
+
expect(linkageSetTabProps).toBeDefined();
|
|
99
|
+
|
|
100
|
+
const schema = tabLinkageRules?.uiSchema?.({
|
|
101
|
+
getActions: () =>
|
|
102
|
+
new Map([
|
|
103
|
+
['linkageSetTabProps', linkageSetTabProps],
|
|
104
|
+
['linkageRunjs', linkageActions.linkageRunjs],
|
|
105
|
+
['linkageSetMenuItemProps', linkageActions.linkageSetMenuItemProps],
|
|
106
|
+
]),
|
|
107
|
+
} as never) as Record<string, Record<string, Record<string, unknown>>> | undefined;
|
|
108
|
+
|
|
109
|
+
expect(schema?.value?.['x-component-props']?.supportedActions).toEqual(['linkageSetTabProps', 'linkageRunjs']);
|
|
110
|
+
expect(linkageSetTabProps?.scene).toBe((ActionScene as unknown as Record<string, number>).TAB_LINKAGE_RULES);
|
|
111
|
+
expect(linkageActions.linkageRunjs.scene).toContain(
|
|
112
|
+
(ActionScene as unknown as Record<string, number>).TAB_LINKAGE_RULES,
|
|
113
|
+
);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should hide the tab when the condition matches and restore it when the condition misses', async () => {
|
|
117
|
+
expect(tabLinkageRules).toBeDefined();
|
|
118
|
+
const runtime = createRuntime();
|
|
119
|
+
|
|
120
|
+
await tabLinkageRules?.handler(runtime.ctx as never, hideRule);
|
|
121
|
+
expect(runtime.model.hidden).toBe(true);
|
|
122
|
+
|
|
123
|
+
runtime.setMatched(false);
|
|
124
|
+
await tabLinkageRules?.handler(runtime.ctx as never, hideRule);
|
|
125
|
+
expect(runtime.model.hidden).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should restore the tab when linkage rules are cleared', async () => {
|
|
129
|
+
expect(tabLinkageRules).toBeDefined();
|
|
130
|
+
const runtime = createRuntime();
|
|
131
|
+
|
|
132
|
+
await tabLinkageRules?.handler(runtime.ctx as never, hideRule);
|
|
133
|
+
expect(runtime.model.hidden).toBe(true);
|
|
134
|
+
|
|
135
|
+
await tabLinkageRules?.handler(runtime.ctx as never, { value: [] });
|
|
136
|
+
expect(runtime.model.hidden).toBe(false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('should not restore stale tab props while applying or clearing visibility rules', async () => {
|
|
140
|
+
expect(tabLinkageRules).toBeDefined();
|
|
141
|
+
const runtime = createRuntime();
|
|
142
|
+
runtime.model.props = {
|
|
143
|
+
title: 'Original title',
|
|
144
|
+
icon: 'original-icon',
|
|
145
|
+
route: { options: { badge: 'original' } },
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
await tabLinkageRules?.handler(runtime.ctx as never, hideRule);
|
|
149
|
+
expect(runtime.model.hidden).toBe(true);
|
|
150
|
+
|
|
151
|
+
const updatedProps = {
|
|
152
|
+
title: 'Updated title',
|
|
153
|
+
icon: 'updated-icon',
|
|
154
|
+
route: { options: { badge: 'updated' } },
|
|
155
|
+
};
|
|
156
|
+
runtime.model.props = updatedProps;
|
|
157
|
+
|
|
158
|
+
await tabLinkageRules?.handler(runtime.ctx as never, hideRule);
|
|
159
|
+
expect(runtime.model.props).toMatchObject(updatedProps);
|
|
160
|
+
expect(runtime.model.hidden).toBe(true);
|
|
161
|
+
|
|
162
|
+
runtime.setMatched(false);
|
|
163
|
+
await tabLinkageRules?.handler(runtime.ctx as never, hideRule);
|
|
164
|
+
expect(runtime.model.props).toMatchObject(updatedProps);
|
|
165
|
+
expect(runtime.model.hidden).toBe(false);
|
|
166
|
+
|
|
167
|
+
await tabLinkageRules?.handler(runtime.ctx as never, { value: [] });
|
|
168
|
+
expect(runtime.model.props).toMatchObject(updatedProps);
|
|
169
|
+
expect(runtime.model.hidden).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -135,11 +135,15 @@ describe('linkageRulesRefresh action', () => {
|
|
|
135
135
|
expect(handler).toHaveBeenCalledWith(ctx, { value: ['master-mounted'] });
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
it('
|
|
138
|
+
it('skips master model when forks can handle flow in design mode', async () => {
|
|
139
139
|
const handler = vi.fn(async () => {});
|
|
140
140
|
const model: any = {
|
|
141
141
|
isFork: false,
|
|
142
|
-
forks: new Set([
|
|
142
|
+
forks: new Set([
|
|
143
|
+
{
|
|
144
|
+
getFlow: vi.fn(() => ({})),
|
|
145
|
+
},
|
|
146
|
+
]),
|
|
143
147
|
getFlow: vi.fn(() => ({})),
|
|
144
148
|
getStepParams: vi.fn(() => ({ value: ['master'] })),
|
|
145
149
|
context: {
|
|
@@ -157,7 +161,7 @@ describe('linkageRulesRefresh action', () => {
|
|
|
157
161
|
flowKey: 'buttonSettings',
|
|
158
162
|
});
|
|
159
163
|
|
|
160
|
-
expect(handler).
|
|
164
|
+
expect(handler).not.toHaveBeenCalled();
|
|
161
165
|
});
|
|
162
166
|
|
|
163
167
|
it('runs linkage action on fork model and resolves params', async () => {
|
|
@@ -43,10 +43,12 @@ export {
|
|
|
43
43
|
linkageSetDetailsFieldProps,
|
|
44
44
|
actionLinkageRules,
|
|
45
45
|
menuLinkageRules,
|
|
46
|
+
tabLinkageRules,
|
|
46
47
|
blockLinkageRules,
|
|
47
48
|
linkageSetBlockProps,
|
|
48
49
|
linkageSetActionProps,
|
|
49
50
|
linkageSetMenuItemProps,
|
|
51
|
+
linkageSetTabProps,
|
|
50
52
|
linkageSetFieldProps,
|
|
51
53
|
subFormLinkageSetFieldProps,
|
|
52
54
|
linkageAssignField,
|
|
@@ -731,6 +731,41 @@ export const linkageSetMenuItemProps = defineAction({
|
|
|
731
731
|
},
|
|
732
732
|
});
|
|
733
733
|
|
|
734
|
+
export const linkageSetTabProps = defineAction({
|
|
735
|
+
name: 'linkageSetTabProps',
|
|
736
|
+
title: tExpr('Set tab state'),
|
|
737
|
+
scene: ActionScene.TAB_LINKAGE_RULES,
|
|
738
|
+
sort: 100,
|
|
739
|
+
uiSchema: {
|
|
740
|
+
value: {
|
|
741
|
+
type: 'string',
|
|
742
|
+
'x-component': (props) => {
|
|
743
|
+
const { value, onChange } = props;
|
|
744
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
745
|
+
const ctx = useFlowContext();
|
|
746
|
+
const t = ctx.model.translate.bind(ctx.model);
|
|
747
|
+
|
|
748
|
+
return (
|
|
749
|
+
<Select
|
|
750
|
+
value={value}
|
|
751
|
+
onChange={onChange}
|
|
752
|
+
placeholder={t('Please select state')}
|
|
753
|
+
style={{ width: '100%' }}
|
|
754
|
+
options={[
|
|
755
|
+
{ label: t('Visible'), value: 'visible' },
|
|
756
|
+
{ label: t('Hidden'), value: 'hidden' },
|
|
757
|
+
]}
|
|
758
|
+
allowClear
|
|
759
|
+
/>
|
|
760
|
+
);
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
handler(ctx, { value, setProps }) {
|
|
765
|
+
setProps(ctx.model, { hiddenModel: value === 'hidden' });
|
|
766
|
+
},
|
|
767
|
+
});
|
|
768
|
+
|
|
734
769
|
export const linkageSetFieldProps = defineAction({
|
|
735
770
|
name: 'linkageSetFieldProps',
|
|
736
771
|
title: tExpr('Set field state'),
|
|
@@ -1339,6 +1374,7 @@ export const linkageRunjs = defineAction({
|
|
|
1339
1374
|
ActionScene.FIELD_LINKAGE_RULES,
|
|
1340
1375
|
ActionScene.ACTION_LINKAGE_RULES,
|
|
1341
1376
|
ActionScene.MENU_LINKAGE_RULES,
|
|
1377
|
+
ActionScene.TAB_LINKAGE_RULES,
|
|
1342
1378
|
ActionScene.DETAILS_FIELD_LINKAGE_RULES,
|
|
1343
1379
|
ActionScene.SUB_FORM_FIELD_LINKAGE_RULES,
|
|
1344
1380
|
],
|
|
@@ -2169,17 +2205,30 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
|
|
|
2169
2205
|
}
|
|
2170
2206
|
: props;
|
|
2171
2207
|
|
|
2172
|
-
//
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2208
|
+
// 只记录联动实际控制的属性,避免之后恢复状态时把标题、路由等无关的新配置回滚到旧快照。
|
|
2209
|
+
const originalProps = model.__originalProps || (model.__originalProps = {});
|
|
2210
|
+
const rememberOriginalProp = (key: string, value: unknown) => {
|
|
2211
|
+
if (!Object.prototype.hasOwnProperty.call(originalProps, key)) {
|
|
2212
|
+
originalProps[key] = value;
|
|
2213
|
+
}
|
|
2214
|
+
};
|
|
2215
|
+
Object.keys(normalizedProps || {}).forEach((key) => {
|
|
2216
|
+
if (key === 'hiddenModel') {
|
|
2217
|
+
rememberOriginalProp(
|
|
2218
|
+
key,
|
|
2219
|
+
Object.prototype.hasOwnProperty.call(model.props || {}, key) ? model.props?.[key] : model.hidden,
|
|
2220
|
+
);
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
rememberOriginalProp(key, model.props?.[key]);
|
|
2225
|
+
if (key === 'hiddenText') {
|
|
2226
|
+
rememberOriginalProp('title', model.props?.title);
|
|
2227
|
+
}
|
|
2228
|
+
if (key === 'required') {
|
|
2229
|
+
rememberOriginalProp('rules', model.props?.rules);
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2183
2232
|
|
|
2184
2233
|
// 临时存起来,遍历完所有规则后,再统一处理
|
|
2185
2234
|
patchPropsByModel.set(model, {
|
|
@@ -2463,6 +2512,32 @@ export const menuLinkageRules = defineAction({
|
|
|
2463
2512
|
},
|
|
2464
2513
|
});
|
|
2465
2514
|
|
|
2515
|
+
export const tabLinkageRules = defineAction({
|
|
2516
|
+
name: 'tabLinkageRules',
|
|
2517
|
+
title: tExpr('Tab linkage rules'),
|
|
2518
|
+
uiMode: 'embed',
|
|
2519
|
+
uiSchema(ctx) {
|
|
2520
|
+
return {
|
|
2521
|
+
value: {
|
|
2522
|
+
type: 'array',
|
|
2523
|
+
'x-component': LinkageRulesUI,
|
|
2524
|
+
'x-component-props': {
|
|
2525
|
+
supportedActions: getSupportedActions(ctx, ActionScene.TAB_LINKAGE_RULES),
|
|
2526
|
+
title: tExpr('Tab linkage rules'),
|
|
2527
|
+
},
|
|
2528
|
+
},
|
|
2529
|
+
};
|
|
2530
|
+
},
|
|
2531
|
+
defaultParams: {
|
|
2532
|
+
value: [],
|
|
2533
|
+
},
|
|
2534
|
+
useRawParams: true,
|
|
2535
|
+
handler: async (ctx, params) => {
|
|
2536
|
+
const resolved = await resolveLinkageRulesParamsPreservingRunJsScripts(ctx, params);
|
|
2537
|
+
return commonLinkageRulesHandler(ctx, resolved);
|
|
2538
|
+
},
|
|
2539
|
+
});
|
|
2540
|
+
|
|
2466
2541
|
export const fieldLinkageRules = defineAction({
|
|
2467
2542
|
name: 'fieldLinkageRules',
|
|
2468
2543
|
title: tExpr('Field linkage rules'),
|
|
@@ -35,12 +35,8 @@ export const linkageRulesRefresh = defineAction({
|
|
|
35
35
|
// Prefer running on the current model; fallback to blockModel when the current model doesn't own the flow.
|
|
36
36
|
if (!hasFlow) return;
|
|
37
37
|
|
|
38
|
-
//
|
|
38
|
+
// Skip master when unmounted forks can handle the same flow.
|
|
39
39
|
// Otherwise master is likely the rendered model and still needs refresh.
|
|
40
|
-
// In design mode, always refresh master so the currently edited model state stays in sync.
|
|
41
|
-
const flowSettingsEnabled = Boolean(
|
|
42
|
-
(ctx as any)?.flowSettingsEnabled || (model as any)?.context?.flowSettingsEnabled,
|
|
43
|
-
);
|
|
44
40
|
const hasForkWithFlow =
|
|
45
41
|
!model?.isFork &&
|
|
46
42
|
!!model?.forks?.size &&
|
|
@@ -49,7 +45,7 @@ export const linkageRulesRefresh = defineAction({
|
|
|
49
45
|
return !!fork?.getFlow?.(flowKey);
|
|
50
46
|
});
|
|
51
47
|
const isMasterMounted = Boolean((model as any)?.context?.ref?.current);
|
|
52
|
-
if (hasForkWithFlow && !isMasterMounted
|
|
48
|
+
if (hasForkWithFlow && !isMasterMounted) {
|
|
53
49
|
return;
|
|
54
50
|
}
|
|
55
51
|
|
|
@@ -42,6 +42,12 @@ import { normalizeFilterValueByOperator } from '../models/blocks/filter-form/val
|
|
|
42
42
|
import { FieldAssignExactDatePicker, type ExactDatePickerMode } from './FieldAssignExactDatePicker';
|
|
43
43
|
import { limitAssociationMetaTree } from './filter/metaTreeAssociationDepth';
|
|
44
44
|
|
|
45
|
+
type VariableInputConverters = {
|
|
46
|
+
renderInputComponent?: (metaTreeNode: MetaTreeNode | null) => React.ComponentType<any> | null;
|
|
47
|
+
resolvePathFromValue?: (value: any) => string[] | undefined;
|
|
48
|
+
resolveValueFromPath?: (metaTreeNode: MetaTreeNode) => any;
|
|
49
|
+
};
|
|
50
|
+
|
|
45
51
|
const DATE_FIELD_INTERFACES = new Set(['date', 'datetime', 'datetimeNoTz', 'createdAt', 'updatedAt', 'unixTimestamp']);
|
|
46
52
|
|
|
47
53
|
const TZ_AWARE_DATE_INTERFACES = new Set(['datetime', 'createdAt', 'updatedAt', 'unixTimestamp']);
|
|
@@ -345,6 +351,7 @@ interface Props {
|
|
|
345
351
|
allowRunJS?: boolean;
|
|
346
352
|
maxAssociationFieldDepth?: number;
|
|
347
353
|
disabled?: boolean;
|
|
354
|
+
variableConverters?: VariableInputConverters;
|
|
348
355
|
}
|
|
349
356
|
|
|
350
357
|
type ResolvedFieldContext = {
|
|
@@ -718,6 +725,7 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
718
725
|
allowRunJS = true,
|
|
719
726
|
maxAssociationFieldDepth = 2,
|
|
720
727
|
disabled = false,
|
|
728
|
+
variableConverters,
|
|
721
729
|
}) => {
|
|
722
730
|
const flowCtx = useFlowContext<FlowModelContext>();
|
|
723
731
|
const normalizeEventValue = React.useCallback((eventOrValue: unknown) => {
|
|
@@ -1511,7 +1519,12 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1511
1519
|
clearValue={''}
|
|
1512
1520
|
disabled={disabled}
|
|
1513
1521
|
converters={{
|
|
1522
|
+
...variableConverters,
|
|
1514
1523
|
renderInputComponent: (meta) => {
|
|
1524
|
+
const external = variableConverters?.renderInputComponent?.(meta ?? null);
|
|
1525
|
+
if (external) {
|
|
1526
|
+
return external;
|
|
1527
|
+
}
|
|
1515
1528
|
const firstPath = meta?.paths?.[0];
|
|
1516
1529
|
if (firstPath === 'constant') return ConstantEditor;
|
|
1517
1530
|
if (firstPath === 'null') return NullComponent;
|
|
@@ -1519,6 +1532,10 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1519
1532
|
return null;
|
|
1520
1533
|
},
|
|
1521
1534
|
resolveValueFromPath: (item) => {
|
|
1535
|
+
const external = variableConverters?.resolveValueFromPath?.(item);
|
|
1536
|
+
if (external !== undefined) {
|
|
1537
|
+
return external;
|
|
1538
|
+
}
|
|
1522
1539
|
const firstPath = item?.paths?.[0];
|
|
1523
1540
|
if (firstPath === 'constant') {
|
|
1524
1541
|
return useDateVariableConstant ? { type: 'today' } : '';
|
|
@@ -1528,6 +1545,10 @@ export const FieldAssignValueInput: React.FC<Props> = ({
|
|
|
1528
1545
|
return undefined;
|
|
1529
1546
|
},
|
|
1530
1547
|
resolvePathFromValue: (currentValue) => {
|
|
1548
|
+
const external = variableConverters?.resolvePathFromValue?.(currentValue);
|
|
1549
|
+
if (external !== undefined) {
|
|
1550
|
+
return external;
|
|
1551
|
+
}
|
|
1531
1552
|
if (currentValue === null) return ['null'];
|
|
1532
1553
|
if (allowRunJS && isRunJSValue(currentValue)) return ['runjs'];
|
|
1533
1554
|
if (useDateVariableConstant && isCtxDateExpression(currentValue)) {
|
|
@@ -12,9 +12,10 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
12
12
|
import { render, waitFor } from '@nocobase/test/client';
|
|
13
13
|
import { FieldAssignValueInput } from '../FieldAssignValueInput';
|
|
14
14
|
|
|
15
|
-
const { mockUseFlowContext, mockGetDefaultBindingByField } = vi.hoisted(() => ({
|
|
15
|
+
const { mockUseFlowContext, mockGetDefaultBindingByField, mockVariableInput } = vi.hoisted(() => ({
|
|
16
16
|
mockUseFlowContext: vi.fn(),
|
|
17
17
|
mockGetDefaultBindingByField: vi.fn(),
|
|
18
|
+
mockVariableInput: vi.fn(() => <div data-testid="variable-input" />),
|
|
18
19
|
}));
|
|
19
20
|
|
|
20
21
|
vi.mock('@nocobase/flow-engine', async () => {
|
|
@@ -25,7 +26,7 @@ vi.mock('@nocobase/flow-engine', async () => {
|
|
|
25
26
|
return {
|
|
26
27
|
...actual,
|
|
27
28
|
useFlowContext: () => mockUseFlowContext(),
|
|
28
|
-
VariableInput:
|
|
29
|
+
VariableInput: mockVariableInput,
|
|
29
30
|
FlowModelRenderer: () => <div data-testid="flow-model-renderer" />,
|
|
30
31
|
EditableItemModel: MockEditableItemModel,
|
|
31
32
|
};
|
|
@@ -131,4 +132,85 @@ describe('FieldAssignValueInput context', () => {
|
|
|
131
132
|
});
|
|
132
133
|
expect(engine.createModel).toHaveBeenCalledWith(expect.any(Object), { delegate: sourceContext });
|
|
133
134
|
});
|
|
135
|
+
|
|
136
|
+
it('lets callers override variable path parsing for domain-specific stored formats', async () => {
|
|
137
|
+
mockVariableInput.mockClear();
|
|
138
|
+
const sourceContext = {
|
|
139
|
+
dataSourceManager: {
|
|
140
|
+
getDataSource: vi.fn(() => ({})),
|
|
141
|
+
},
|
|
142
|
+
t: (key: string) => key,
|
|
143
|
+
};
|
|
144
|
+
const fieldModel = {
|
|
145
|
+
props: {},
|
|
146
|
+
setProps: vi.fn(),
|
|
147
|
+
dispatchEvent: vi.fn(),
|
|
148
|
+
remove: vi.fn(),
|
|
149
|
+
};
|
|
150
|
+
const tempRoot = {
|
|
151
|
+
context: {
|
|
152
|
+
defineProperty: vi.fn(),
|
|
153
|
+
},
|
|
154
|
+
subModels: {
|
|
155
|
+
fields: [fieldModel],
|
|
156
|
+
},
|
|
157
|
+
setProps: vi.fn(),
|
|
158
|
+
remove: vi.fn(),
|
|
159
|
+
};
|
|
160
|
+
const engine = {
|
|
161
|
+
createModel: vi.fn(() => tempRoot),
|
|
162
|
+
};
|
|
163
|
+
const collectionField = {
|
|
164
|
+
name: 'status',
|
|
165
|
+
interface: 'input',
|
|
166
|
+
uiSchema: { 'x-component': 'Input' },
|
|
167
|
+
isAssociationField: () => false,
|
|
168
|
+
getComponentProps: () => ({}),
|
|
169
|
+
};
|
|
170
|
+
const collection = {
|
|
171
|
+
dataSourceKey: 'main',
|
|
172
|
+
name: 'tasks',
|
|
173
|
+
getField: (name: string) => (name === 'status' ? collectionField : null),
|
|
174
|
+
getFields: () => [collectionField],
|
|
175
|
+
};
|
|
176
|
+
const formModel = {
|
|
177
|
+
context: { ...sourceContext, engine, collection, blockModel: null as any },
|
|
178
|
+
collection,
|
|
179
|
+
subModels: {},
|
|
180
|
+
};
|
|
181
|
+
formModel.context.blockModel = formModel;
|
|
182
|
+
|
|
183
|
+
mockGetDefaultBindingByField.mockReturnValue({ modelName: 'InputFieldModel' });
|
|
184
|
+
mockUseFlowContext.mockReturnValue({
|
|
185
|
+
model: formModel,
|
|
186
|
+
t: (key: string) => key,
|
|
187
|
+
getPropertyMetaTree: vi.fn(async () => []),
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const variableConverters = {
|
|
191
|
+
resolvePathFromValue: vi.fn((value: string) =>
|
|
192
|
+
value === '{{$context.data.updatedAt}}' ? ['$context', 'data', 'updatedAt'] : undefined,
|
|
193
|
+
),
|
|
194
|
+
resolveValueFromPath: vi.fn(() => undefined),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
render(
|
|
198
|
+
<FieldAssignValueInput
|
|
199
|
+
targetPath="status"
|
|
200
|
+
value="{{$context.data.updatedAt}}"
|
|
201
|
+
onChange={vi.fn()}
|
|
202
|
+
variableConverters={variableConverters}
|
|
203
|
+
/>,
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
await waitFor(() => {
|
|
207
|
+
expect(engine.createModel).toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
const latestVariableInputCall = mockVariableInput.mock.calls.at(-1)?.[0];
|
|
210
|
+
expect(latestVariableInputCall.converters.resolvePathFromValue('{{$context.data.updatedAt}}')).toEqual([
|
|
211
|
+
'$context',
|
|
212
|
+
'data',
|
|
213
|
+
'updatedAt',
|
|
214
|
+
]);
|
|
215
|
+
});
|
|
134
216
|
});
|
|
@@ -45,6 +45,7 @@ interface FilterGroupProps {
|
|
|
45
45
|
value: Record<string, any>;
|
|
46
46
|
/** 自定义筛选项组件 */
|
|
47
47
|
FilterItem?: React.FC<FilterItemProps>;
|
|
48
|
+
disabled?: boolean;
|
|
48
49
|
closeIcon?: ReactNode;
|
|
49
50
|
/** 是否显示边框 */
|
|
50
51
|
showBorder?: boolean;
|
|
@@ -89,6 +90,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
89
90
|
const {
|
|
90
91
|
value = { logic: '$and', items: [] },
|
|
91
92
|
FilterItem,
|
|
93
|
+
disabled = false,
|
|
92
94
|
showBorder = false,
|
|
93
95
|
onRemove,
|
|
94
96
|
onChange,
|
|
@@ -120,11 +122,17 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
120
122
|
};
|
|
121
123
|
|
|
122
124
|
const handleLogicChange = (newLogic: '$and' | '$or') => {
|
|
125
|
+
if (disabled) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
123
128
|
value.logic = newLogic;
|
|
124
129
|
onChange?.(value);
|
|
125
130
|
};
|
|
126
131
|
|
|
127
132
|
const handleAddCondition = () => {
|
|
133
|
+
if (disabled) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
128
136
|
items.push({
|
|
129
137
|
path: '',
|
|
130
138
|
operator: '',
|
|
@@ -134,6 +142,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
134
142
|
};
|
|
135
143
|
|
|
136
144
|
const handleAddConditionGroup = () => {
|
|
145
|
+
if (disabled) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
137
148
|
items.push({
|
|
138
149
|
logic: '$and',
|
|
139
150
|
items: [],
|
|
@@ -142,6 +153,9 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
142
153
|
};
|
|
143
154
|
|
|
144
155
|
const handleRemoveItem = (index: number) => {
|
|
156
|
+
if (disabled) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
145
159
|
items.splice(index, 1);
|
|
146
160
|
onChange?.(value);
|
|
147
161
|
};
|
|
@@ -160,11 +174,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
160
174
|
<button
|
|
161
175
|
type="button"
|
|
162
176
|
aria-label="icon-close"
|
|
177
|
+
disabled={disabled}
|
|
163
178
|
style={{
|
|
164
179
|
position: 'absolute',
|
|
165
180
|
right: 10,
|
|
166
181
|
top: 10,
|
|
167
|
-
cursor: 'pointer',
|
|
182
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
168
183
|
background: 'transparent',
|
|
169
184
|
border: 0,
|
|
170
185
|
padding: 0,
|
|
@@ -182,6 +197,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
182
197
|
data-testid="filter-select-all-or-any"
|
|
183
198
|
style={{ width: 'auto' }}
|
|
184
199
|
value={logic}
|
|
200
|
+
disabled={disabled}
|
|
185
201
|
onChange={handleLogicChange}
|
|
186
202
|
>
|
|
187
203
|
<Select.Option value="$and">{t('All')}</Select.Option>
|
|
@@ -200,6 +216,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
200
216
|
key={getFilterItemKey(item)}
|
|
201
217
|
value={item}
|
|
202
218
|
FilterItem={FilterItem}
|
|
219
|
+
disabled={disabled}
|
|
203
220
|
showBorder={true}
|
|
204
221
|
onRemove={() => handleRemoveItem(index)}
|
|
205
222
|
onChange={(v) => {
|
|
@@ -221,11 +238,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
221
238
|
<button
|
|
222
239
|
type="button"
|
|
223
240
|
aria-label="icon-close"
|
|
241
|
+
disabled={disabled}
|
|
224
242
|
style={{
|
|
225
243
|
marginLeft: 8,
|
|
226
244
|
marginRight: 8,
|
|
227
245
|
flex: '0 0 auto',
|
|
228
|
-
cursor: 'pointer',
|
|
246
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
229
247
|
background: 'transparent',
|
|
230
248
|
border: 0,
|
|
231
249
|
padding: 0,
|
|
@@ -251,11 +269,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
251
269
|
<button
|
|
252
270
|
type="button"
|
|
253
271
|
aria-label="icon-close"
|
|
272
|
+
disabled={disabled}
|
|
254
273
|
style={{
|
|
255
274
|
marginLeft: 8,
|
|
256
275
|
marginRight: 8,
|
|
257
276
|
flex: '0 0 auto',
|
|
258
|
-
cursor: 'pointer',
|
|
277
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
259
278
|
background: 'transparent',
|
|
260
279
|
border: 0,
|
|
261
280
|
padding: 0,
|
|
@@ -277,11 +296,12 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
277
296
|
<button
|
|
278
297
|
type="button"
|
|
279
298
|
aria-label="icon-close"
|
|
299
|
+
disabled={disabled}
|
|
280
300
|
style={{
|
|
281
301
|
marginLeft: 8,
|
|
282
302
|
marginRight: 8,
|
|
283
303
|
flex: '0 0 auto',
|
|
284
|
-
cursor: 'pointer',
|
|
304
|
+
cursor: disabled ? 'not-allowed' : 'pointer',
|
|
285
305
|
background: 'transparent',
|
|
286
306
|
border: 0,
|
|
287
307
|
padding: 0,
|
|
@@ -298,7 +318,14 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
298
318
|
</div>
|
|
299
319
|
|
|
300
320
|
<Space size={16} style={{ marginTop: 8, marginBottom: 8 }}>
|
|
301
|
-
<Button
|
|
321
|
+
<Button
|
|
322
|
+
style={{ padding: 0 }}
|
|
323
|
+
type="link"
|
|
324
|
+
size="small"
|
|
325
|
+
icon={<PlusOutlined />}
|
|
326
|
+
onClick={handleAddCondition}
|
|
327
|
+
disabled={disabled}
|
|
328
|
+
>
|
|
302
329
|
{t('Add condition')}
|
|
303
330
|
</Button>
|
|
304
331
|
<Button
|
|
@@ -307,6 +334,7 @@ export const FilterGroup: FC<FilterGroupProps> = observer(
|
|
|
307
334
|
size="small"
|
|
308
335
|
icon={<PlusOutlined />}
|
|
309
336
|
onClick={handleAddConditionGroup}
|
|
337
|
+
disabled={disabled}
|
|
310
338
|
>
|
|
311
339
|
{t('Add condition group')}
|
|
312
340
|
</Button>
|