@nocobase/client-v2 2.2.0-alpha.7 → 2.2.0-alpha.8
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/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 +1 -0
- package/es/index.mjs +10 -10
- package/lib/index.js +30 -30
- package/package.json +7 -7
- package/src/BaseApplication.tsx +3 -2
- package/src/__tests__/app.test.tsx +29 -0
- package/src/components/form/TypedVariableInput.tsx +12 -8
- package/src/components/form/__tests__/TypedVariableInput.test.tsx +44 -3
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
- 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 +139 -6
- 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 +21 -0
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
- package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
- package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.2.0-alpha.
|
|
3
|
+
"version": "2.2.0-alpha.8",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"@formily/antd-v5": "1.2.3",
|
|
28
28
|
"@formily/react": "^2.2.27",
|
|
29
29
|
"@formily/shared": "^2.2.27",
|
|
30
|
-
"@nocobase/evaluators": "2.2.0-alpha.
|
|
31
|
-
"@nocobase/flow-engine": "2.2.0-alpha.
|
|
32
|
-
"@nocobase/sdk": "2.2.0-alpha.
|
|
33
|
-
"@nocobase/shared": "2.2.0-alpha.
|
|
34
|
-
"@nocobase/utils": "2.2.0-alpha.
|
|
30
|
+
"@nocobase/evaluators": "2.2.0-alpha.8",
|
|
31
|
+
"@nocobase/flow-engine": "2.2.0-alpha.8",
|
|
32
|
+
"@nocobase/sdk": "2.2.0-alpha.8",
|
|
33
|
+
"@nocobase/shared": "2.2.0-alpha.8",
|
|
34
|
+
"@nocobase/utils": "2.2.0-alpha.8",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -47,5 +47,5 @@
|
|
|
47
47
|
"react-i18next": "^11.15.1",
|
|
48
48
|
"react-router-dom": "^6.30.1"
|
|
49
49
|
},
|
|
50
|
-
"gitHead": "
|
|
50
|
+
"gitHead": "810c81e5963966bda7ec03b6453a3a80fe60e027"
|
|
51
51
|
}
|
package/src/BaseApplication.tsx
CHANGED
|
@@ -35,6 +35,7 @@ import { SystemSettingsSource } from './flow/system-settings';
|
|
|
35
35
|
import { LayoutManager } from './layout-manager/LayoutManager';
|
|
36
36
|
import type { PluginClass, PluginManager, PluginType } from './PluginManager';
|
|
37
37
|
import { RouteRepository } from './RouteRepository';
|
|
38
|
+
import { stripModernClientPrefix } from './authRedirect';
|
|
38
39
|
import type {
|
|
39
40
|
ComponentTypeAndString,
|
|
40
41
|
RenderableComponentType,
|
|
@@ -402,7 +403,7 @@ export abstract class BaseApplication<
|
|
|
402
403
|
this.favicon = favicon || '';
|
|
403
404
|
}
|
|
404
405
|
|
|
405
|
-
const iconHref = this.favicon || '
|
|
406
|
+
const iconHref = this.favicon || this.getCdnUrl() + 'favicon/favicon.ico';
|
|
406
407
|
|
|
407
408
|
if (faviconLinkElement) {
|
|
408
409
|
faviconLinkElement.href = iconHref;
|
|
@@ -438,7 +439,7 @@ export abstract class BaseApplication<
|
|
|
438
439
|
}
|
|
439
440
|
|
|
440
441
|
getCdnUrl() {
|
|
441
|
-
return ensureTrailingSlash(window['__webpack_public_path__'] || this.getPublicPath());
|
|
442
|
+
return ensureTrailingSlash(window['__webpack_public_path__'] || stripModernClientPrefix(this.getPublicPath()));
|
|
442
443
|
}
|
|
443
444
|
|
|
444
445
|
getPublicPath() {
|
|
@@ -35,6 +35,7 @@ describe('app', () => {
|
|
|
35
35
|
document.querySelectorAll('link[rel="shortcut icon"]').forEach((node) => node.remove());
|
|
36
36
|
document.documentElement.removeAttribute('lang');
|
|
37
37
|
delete window['__webpack_public_path__'];
|
|
38
|
+
delete window['__nocobase_modern_client_prefix__'];
|
|
38
39
|
vi.restoreAllMocks();
|
|
39
40
|
});
|
|
40
41
|
|
|
@@ -84,6 +85,34 @@ describe('app', () => {
|
|
|
84
85
|
expect(app.getCdnUrl()).toBe('/cdn/assets/');
|
|
85
86
|
});
|
|
86
87
|
|
|
88
|
+
it('should remove the modern client prefix from the CDN fallback path', () => {
|
|
89
|
+
const app = new Application({
|
|
90
|
+
router,
|
|
91
|
+
publicPath: '/v/',
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(app.getCdnUrl()).toBe('/');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should preserve APP_PUBLIC_PATH when removing the modern client prefix', () => {
|
|
98
|
+
const app = new Application({
|
|
99
|
+
router,
|
|
100
|
+
publicPath: '/nocobase/v/',
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(app.getCdnUrl()).toBe('/nocobase/');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should support a custom modern client prefix', () => {
|
|
107
|
+
window['__nocobase_modern_client_prefix__'] = 'modern';
|
|
108
|
+
const app = new Application({
|
|
109
|
+
router,
|
|
110
|
+
publicPath: '/nocobase/modern/',
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(app.getCdnUrl()).toBe('/nocobase/');
|
|
114
|
+
});
|
|
115
|
+
|
|
87
116
|
it('should apply the provided favicon immediately', () => {
|
|
88
117
|
const app = new Application({ router });
|
|
89
118
|
|
|
@@ -121,12 +121,15 @@ function normalizeTypes(types: TypedConstantSpec[]): NormalizedType[] {
|
|
|
121
121
|
);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
function defaultValueFor(type: TypedConstantType): unknown {
|
|
124
|
+
function defaultValueFor(type: TypedConstantType, typedProps: Record<string, unknown> = {}): unknown {
|
|
125
125
|
switch (type) {
|
|
126
126
|
case 'string':
|
|
127
127
|
return '';
|
|
128
|
-
case 'number':
|
|
128
|
+
case 'number': {
|
|
129
|
+
const min = typeof typedProps.min === 'number' ? typedProps.min : undefined;
|
|
130
|
+
if (min !== undefined && min > 0) return min;
|
|
129
131
|
return 0;
|
|
132
|
+
}
|
|
130
133
|
case 'boolean':
|
|
131
134
|
return false;
|
|
132
135
|
case 'date': {
|
|
@@ -433,7 +436,7 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
|
|
|
433
436
|
return undefined;
|
|
434
437
|
}
|
|
435
438
|
const firstType = normalizedTypes[0];
|
|
436
|
-
return firstType ? defaultValueFor(firstType.type) : undefined;
|
|
439
|
+
return firstType ? defaultValueFor(firstType.type, firstType.props) : undefined;
|
|
437
440
|
}, [defaultToFirstConstantTypeWhenUndefined, normalizedTypes, value, variableOnly]);
|
|
438
441
|
const effectiveValue = value === undefined && defaultedValue !== undefined ? defaultedValue : value;
|
|
439
442
|
const detected = useMemo(() => detectMode(effectiveValue, parseVariablePath), [effectiveValue, parseVariablePath]);
|
|
@@ -529,7 +532,8 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
|
|
|
529
532
|
const targetType = (path[1] as TypedConstantType | undefined) ?? normalizedTypes[0]?.type;
|
|
530
533
|
if (!targetType) return;
|
|
531
534
|
if (detected.mode === targetType) return;
|
|
532
|
-
|
|
535
|
+
const target = normalizedTypes.find(({ type }) => type === targetType);
|
|
536
|
+
onChange?.(defaultValueFor(targetType, target?.props));
|
|
533
537
|
return;
|
|
534
538
|
}
|
|
535
539
|
const leaf = selectedOptions?.[selectedOptions.length - 1] as SwitcherOption | undefined;
|
|
@@ -548,7 +552,7 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
|
|
|
548
552
|
}
|
|
549
553
|
const first = normalizedTypes[0];
|
|
550
554
|
if (first) {
|
|
551
|
-
onChange?.(defaultValueFor(first.type));
|
|
555
|
+
onChange?.(defaultValueFor(first.type, first.props));
|
|
552
556
|
return;
|
|
553
557
|
}
|
|
554
558
|
if (nullable) {
|
|
@@ -585,11 +589,11 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
|
|
|
585
589
|
if (variableOnly) {
|
|
586
590
|
return undefined;
|
|
587
591
|
}
|
|
588
|
-
if (isNull) {
|
|
592
|
+
if (isNull && nullable) {
|
|
589
593
|
return [NULL_KEY];
|
|
590
594
|
}
|
|
591
595
|
return [CONST_KEY, constantTypeForRendering];
|
|
592
|
-
}, [constantTypeForRendering, detected.variablePath, isNull, isVariable, variableOnly]);
|
|
596
|
+
}, [constantTypeForRendering, detected.variablePath, isNull, isVariable, nullable, variableOnly]);
|
|
593
597
|
|
|
594
598
|
// Preload a saved variable's label path across lazy levels. `resolveVariableLabels` can only read already-loaded
|
|
595
599
|
// `children`; when a saved reference points below a node whose children are still a lazy thunk (e.g. a relation field
|
|
@@ -734,7 +738,7 @@ export function TypedVariableInput(props: TypedVariableInputProps) {
|
|
|
734
738
|
</div>
|
|
735
739
|
) : variableOnly ? (
|
|
736
740
|
<Input placeholder={placeholder} readOnly disabled={disabled} style={{ width: '100%' }} />
|
|
737
|
-
) : isNull ? (
|
|
741
|
+
) : isNull && nullable ? (
|
|
738
742
|
// v1 used the `placeholder` slot (not `value`) so the antd default placeholder colour applies — keeps the
|
|
739
743
|
// field looking visibly empty/inactive rather than holding a real text value.
|
|
740
744
|
<Input placeholder={`<${t('Null')}>`} readOnly disabled={disabled} style={{ width: '100%' }} />
|
|
@@ -81,6 +81,27 @@ describe('TypedVariableInput - constant rendering', () => {
|
|
|
81
81
|
expect(screen.getByRole('button', { name: 'variable-switcher' }).className).not.toContain('ant-btn-primary');
|
|
82
82
|
});
|
|
83
83
|
|
|
84
|
+
it('keeps the number editor usable when value=null and nullable=false', async () => {
|
|
85
|
+
const ctx = createContextWithEnv();
|
|
86
|
+
const handleChange = vi.fn();
|
|
87
|
+
renderWithCtx(
|
|
88
|
+
ctx,
|
|
89
|
+
<TypedVariableInput
|
|
90
|
+
value={null}
|
|
91
|
+
types={[['number', { min: 1 }]]}
|
|
92
|
+
namespaces={['$env']}
|
|
93
|
+
nullable={false}
|
|
94
|
+
onChange={handleChange}
|
|
95
|
+
/>,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const numberInput = await screen.findByRole('spinbutton');
|
|
99
|
+
expect(screen.queryByPlaceholderText('<Null>')).toBeNull();
|
|
100
|
+
fireEvent.change(numberInput, { target: { value: '2' } });
|
|
101
|
+
fireEvent.blur(numberInput);
|
|
102
|
+
expect(handleChange).toHaveBeenCalledWith(2);
|
|
103
|
+
});
|
|
104
|
+
|
|
84
105
|
it('defaults undefined to the first constant type', async () => {
|
|
85
106
|
const ctx = createContextWithEnv();
|
|
86
107
|
const handleChange = vi.fn();
|
|
@@ -102,6 +123,26 @@ describe('TypedVariableInput - constant rendering', () => {
|
|
|
102
123
|
});
|
|
103
124
|
});
|
|
104
125
|
|
|
126
|
+
it('uses a positive numeric minimum as the default value', async () => {
|
|
127
|
+
const ctx = createContextWithEnv();
|
|
128
|
+
const handleChange = vi.fn();
|
|
129
|
+
renderWithCtx(
|
|
130
|
+
ctx,
|
|
131
|
+
<TypedVariableInput
|
|
132
|
+
value={undefined}
|
|
133
|
+
types={[['number', { min: 1 }]]}
|
|
134
|
+
namespaces={['$env']}
|
|
135
|
+
nullable={false}
|
|
136
|
+
onChange={handleChange}
|
|
137
|
+
/>,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(await screen.findByDisplayValue('1')).toBeInTheDocument();
|
|
141
|
+
await waitFor(() => {
|
|
142
|
+
expect(handleChange).toHaveBeenCalledWith(1);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
105
146
|
it('can still opt out to keep the null placeholder for undefined', async () => {
|
|
106
147
|
const ctx = createContextWithEnv();
|
|
107
148
|
renderWithCtx(
|
|
@@ -183,14 +224,14 @@ describe('TypedVariableInput - variable rendering', () => {
|
|
|
183
224
|
expect(handleChange).toHaveBeenCalledWith(0);
|
|
184
225
|
});
|
|
185
226
|
|
|
186
|
-
it('clears back to
|
|
227
|
+
it('clears back to the valid minimum of the first type when nullable=false', async () => {
|
|
187
228
|
const ctx = createContextWithEnv();
|
|
188
229
|
const handleChange = vi.fn();
|
|
189
230
|
const { container } = renderWithCtx(
|
|
190
231
|
ctx,
|
|
191
232
|
<TypedVariableInput
|
|
192
233
|
value="{{$env.SMTP_PORT}}"
|
|
193
|
-
types={['number']}
|
|
234
|
+
types={[['number', { min: 1 }]]}
|
|
194
235
|
namespaces={['$env']}
|
|
195
236
|
nullable={false}
|
|
196
237
|
onChange={handleChange}
|
|
@@ -199,7 +240,7 @@ describe('TypedVariableInput - variable rendering', () => {
|
|
|
199
240
|
const clear = container.querySelector('button.clear-button') as HTMLButtonElement | null;
|
|
200
241
|
expect(clear).not.toBeNull();
|
|
201
242
|
fireEvent.click(clear as HTMLButtonElement);
|
|
202
|
-
expect(handleChange).toHaveBeenCalledWith(
|
|
243
|
+
expect(handleChange).toHaveBeenCalledWith(1);
|
|
203
244
|
});
|
|
204
245
|
|
|
205
246
|
it('treats types=[] as variable-only mode with a readonly placeholder before selection', async () => {
|
|
@@ -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 () => {
|
|
@@ -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>
|