@nocobase/client-v2 3.0.0-alpha.1 → 3.0.0-alpha.12
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 +12 -0
- package/es/acl/aclCheckReadiness.d.ts +11 -0
- package/es/authRedirect.d.ts +6 -2
- package/es/flow/actions/linkageRules.d.ts +24 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +4 -30
- package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
- package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
- package/es/flow/components/field-value-variable/index.d.ts +11 -0
- package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
- package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
- package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
- package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
- package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
- package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
- package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +11 -0
- package/es/flow/models/blocks/table/utils.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
- package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +2 -1
- package/es/index.mjs +140 -140
- package/es/nocobase-buildin-plugin/index.d.ts +1 -0
- package/es/settings-app/SettingsGroupNav.d.ts +3 -2
- package/es/settings-app/SettingsSearch.d.ts +1 -1
- package/es/ui-operation/index.d.ts +15 -0
- package/es/ui-operation/ui-operation-codec.d.ts +10 -0
- package/lib/index.js +145 -145
- package/package.json +7 -7
- package/src/APIClient.ts +92 -0
- package/src/Application.tsx +3 -1
- package/src/__tests__/APIClient.test.tsx +58 -0
- package/src/__tests__/app.test.tsx +122 -3
- package/src/__tests__/authRedirect.test.ts +17 -0
- package/src/__tests__/browserChecker.test.ts +81 -33
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +37 -0
- package/src/__tests__/settings-center.test.tsx +312 -11
- package/src/__tests__/settings-layout-root.test.tsx +346 -6
- package/src/__tests__/settings-runtime-paths.test.ts +1 -1
- package/src/__tests__/settings-search-shortcut.test.tsx +321 -0
- package/src/__tests__/settings-shell.test.tsx +62 -1
- package/src/acl/ACLProvider.tsx +34 -3
- package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
- package/src/acl/aclCheckReadiness.ts +44 -0
- package/src/authRedirect.ts +13 -5
- package/src/components/AppComponents.tsx +16 -7
- package/src/components/form/filter/CollectionFilterItem.tsx +4 -2
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +17 -0
- package/src/components/form/table/Table.tsx +81 -6
- package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
- package/src/flow/actions/linkageRules.tsx +33 -10
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
- package/src/flow/components/FieldAssignValueInput.tsx +38 -621
- package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
- package/src/flow/components/field-value-variable/dateValue.ts +223 -0
- package/src/flow/components/field-value-variable/index.ts +12 -0
- package/src/flow/components/filter/VariableFilterItem.tsx +12 -3
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
- package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
- package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
- package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
- package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
- package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
- package/src/flow/models/base/CollectionBlockModel.tsx +32 -1
- package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
- package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
- package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
- package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
- package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
- package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +25 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +18 -8
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
- package/src/flow/models/blocks/table/utils.ts +7 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
- package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
- package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
- package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
- package/src/index.ts +2 -0
- package/src/layout-manager/LayoutContentRoute.tsx +2 -1
- package/src/layout-manager/LayoutRoute.tsx +2 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
- package/src/nocobase-buildin-plugin/index.tsx +14 -1
- package/src/settings-app/SettingsGroupNav.tsx +11 -11
- package/src/settings-app/SettingsSearch.tsx +115 -10
- package/src/settings-app/SettingsShell.tsx +23 -6
- package/src/settings-app/runtimePaths.ts +0 -1
- package/src/settings-app/settingsTheme.ts +82 -14
- package/src/settings-center/AdminSettingsLayout.tsx +132 -107
- package/src/settings-center/__tests__/groups.test.ts +25 -0
- package/src/settings-center/groups.ts +1 -1
- package/src/ui-operation/index.ts +33 -0
- package/src/ui-operation/ui-operation-codec.ts +54 -0
|
@@ -0,0 +1,380 @@
|
|
|
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 type { MetaTreeNode, VariableInputProps } from '@nocobase/flow-engine';
|
|
11
|
+
import { parseCtxDateExpressionConfig, serializeCtxDateExpressionConfig } from '@nocobase/flow-engine';
|
|
12
|
+
import { fireEvent, render, screen } from '@nocobase/test/client';
|
|
13
|
+
import { dayjs } from '@nocobase/utils/client';
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
16
|
+
import { DateVariableEditor, serializeExactDatePickerValue } from '../DateVariableEditor';
|
|
17
|
+
import {
|
|
18
|
+
DEFAULT_DATE_VARIABLE_COMPONENT_PROPS,
|
|
19
|
+
isDateLikeField,
|
|
20
|
+
resolveDateVariableComponentProps,
|
|
21
|
+
type DateVariableComponentProps,
|
|
22
|
+
} from '../dateValue';
|
|
23
|
+
import { FieldValueVariableInput } from '../FieldValueVariableInput';
|
|
24
|
+
|
|
25
|
+
const mocks = vi.hoisted(() => ({
|
|
26
|
+
variableInputProps: undefined as VariableInputProps | undefined,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
vi.mock('@nocobase/flow-engine', async (importOriginal) => {
|
|
30
|
+
const actual = await importOriginal<typeof import('@nocobase/flow-engine')>();
|
|
31
|
+
return {
|
|
32
|
+
...actual,
|
|
33
|
+
VariableInput: (props: VariableInputProps) => {
|
|
34
|
+
mocks.variableInputProps = props;
|
|
35
|
+
return <div data-testid="variable-input" />;
|
|
36
|
+
},
|
|
37
|
+
useFlowContext: () => ({
|
|
38
|
+
isMobileLayout: false,
|
|
39
|
+
model: { translate: (value: string) => value },
|
|
40
|
+
t: (value: string) => value,
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const ConstantComponent: React.FC = () => <div>constant editor</div>;
|
|
46
|
+
const NullComponent: React.FC = () => <div>null editor</div>;
|
|
47
|
+
const RunJSComponent: React.FC = () => <div>runjs editor</div>;
|
|
48
|
+
|
|
49
|
+
function renderInput(options?: {
|
|
50
|
+
value?: unknown;
|
|
51
|
+
isDateLikeField?: boolean;
|
|
52
|
+
dateComponentProps?: DateVariableComponentProps;
|
|
53
|
+
}) {
|
|
54
|
+
const onChange = vi.fn();
|
|
55
|
+
render(
|
|
56
|
+
<FieldValueVariableInput
|
|
57
|
+
value={options?.value}
|
|
58
|
+
onChange={onChange}
|
|
59
|
+
baseMetaTree={[{ name: 'currentUser', title: 'Current user', type: 'object', paths: ['currentUser'] }]}
|
|
60
|
+
constantComponent={ConstantComponent}
|
|
61
|
+
nullComponent={NullComponent}
|
|
62
|
+
runJSComponent={RunJSComponent}
|
|
63
|
+
isDateLikeField={options?.isDateLikeField ?? false}
|
|
64
|
+
dateComponentProps={options?.dateComponentProps ?? DEFAULT_DATE_VARIABLE_COMPONENT_PROPS}
|
|
65
|
+
/>,
|
|
66
|
+
);
|
|
67
|
+
return onChange;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function resolveMetaTree() {
|
|
71
|
+
const provider = mocks.variableInputProps?.metaTree;
|
|
72
|
+
const tree = typeof provider === 'function' ? await provider() : provider;
|
|
73
|
+
return (tree || []) as MetaTreeNode[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
describe('FieldValueVariableInput', () => {
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
mocks.variableInputProps = undefined;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('adds Date as a non-selectable first-level peer with complete second-level options', async () => {
|
|
82
|
+
renderInput({ isDateLikeField: true });
|
|
83
|
+
|
|
84
|
+
const tree = await resolveMetaTree();
|
|
85
|
+
expect(tree.slice(0, 4).map((node) => node.name)).toEqual(['constant', 'null', 'date', 'runjs']);
|
|
86
|
+
|
|
87
|
+
const dateNode = tree[2];
|
|
88
|
+
expect(dateNode.selectable).toBe(false);
|
|
89
|
+
expect((dateNode.children as MetaTreeNode[]).map((node) => node.name)).toEqual([
|
|
90
|
+
'exact',
|
|
91
|
+
'past',
|
|
92
|
+
'next',
|
|
93
|
+
'now',
|
|
94
|
+
'today',
|
|
95
|
+
'yesterday',
|
|
96
|
+
'tomorrow',
|
|
97
|
+
'thisWeek',
|
|
98
|
+
'lastWeek',
|
|
99
|
+
'nextWeek',
|
|
100
|
+
'thisMonth',
|
|
101
|
+
'lastMonth',
|
|
102
|
+
'nextMonth',
|
|
103
|
+
'thisQuarter',
|
|
104
|
+
'lastQuarter',
|
|
105
|
+
'nextQuarter',
|
|
106
|
+
'thisYear',
|
|
107
|
+
'lastYear',
|
|
108
|
+
'nextYear',
|
|
109
|
+
]);
|
|
110
|
+
expect(tree[4].name).toBe('currentUser');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('does not allow Now for pure date fields', async () => {
|
|
114
|
+
const dateComponentProps: DateVariableComponentProps = {
|
|
115
|
+
...DEFAULT_DATE_VARIABLE_COMPONENT_PROPS,
|
|
116
|
+
exactNormalizeMode: 'date',
|
|
117
|
+
};
|
|
118
|
+
renderInput({ isDateLikeField: true, dateComponentProps });
|
|
119
|
+
|
|
120
|
+
const tree = await resolveMetaTree();
|
|
121
|
+
const dateNode = tree[2];
|
|
122
|
+
expect((dateNode.children as MetaTreeNode[]).map((node) => node.name)).not.toContain('now');
|
|
123
|
+
expect(
|
|
124
|
+
mocks.variableInputProps?.converters?.resolveValueFromPath?.({
|
|
125
|
+
name: 'now',
|
|
126
|
+
title: 'Now',
|
|
127
|
+
type: 'date',
|
|
128
|
+
paths: ['date', 'now'],
|
|
129
|
+
}),
|
|
130
|
+
).toBeUndefined();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('restores a legacy Now value for a pure date field without allowing it to be selected again', async () => {
|
|
134
|
+
const dateComponentProps: DateVariableComponentProps = {
|
|
135
|
+
...DEFAULT_DATE_VARIABLE_COMPONENT_PROPS,
|
|
136
|
+
exactNormalizeMode: 'date',
|
|
137
|
+
};
|
|
138
|
+
renderInput({ value: '{{ ctx.date.preset.now }}', isDateLikeField: true, dateComponentProps });
|
|
139
|
+
|
|
140
|
+
const tree = await resolveMetaTree();
|
|
141
|
+
const dateNode = tree[2];
|
|
142
|
+
const nowNode = (dateNode.children as MetaTreeNode[]).find((node) => node.name === 'now');
|
|
143
|
+
expect(nowNode).toMatchObject({ disabled: true, selectable: false });
|
|
144
|
+
expect(mocks.variableInputProps?.converters?.resolvePathFromValue?.(mocks.variableInputProps.value)).toEqual([
|
|
145
|
+
'date',
|
|
146
|
+
'now',
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('restores legacy date expressions under Date without changing Constant semantics', () => {
|
|
151
|
+
renderInput({ value: '{{ ctx.date.preset.today }}', isDateLikeField: true });
|
|
152
|
+
|
|
153
|
+
expect(mocks.variableInputProps?.value).toMatchObject({ kind: 'preset', preset: 'today' });
|
|
154
|
+
expect(mocks.variableInputProps?.converters?.resolvePathFromValue?.(mocks.variableInputProps.value)).toEqual([
|
|
155
|
+
'date',
|
|
156
|
+
'today',
|
|
157
|
+
]);
|
|
158
|
+
expect(
|
|
159
|
+
mocks.variableInputProps?.converters?.resolveValueFromPath?.({
|
|
160
|
+
name: 'constant',
|
|
161
|
+
title: 'Constant',
|
|
162
|
+
type: 'string',
|
|
163
|
+
paths: ['constant'],
|
|
164
|
+
}),
|
|
165
|
+
).toBe('');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it.each([
|
|
169
|
+
{ kind: 'exact', value: 'plain JSON value' },
|
|
170
|
+
{ kind: 'relative', direction: 'next', amount: 1, unit: 'day' },
|
|
171
|
+
{ kind: 'preset', preset: 'today' },
|
|
172
|
+
])('keeps a JSON constant containing Date-like keys as a constant', (constantValue) => {
|
|
173
|
+
const onChange = renderInput({ value: constantValue, isDateLikeField: false });
|
|
174
|
+
|
|
175
|
+
expect(mocks.variableInputProps?.converters?.resolvePathFromValue?.(mocks.variableInputProps.value)).toEqual([
|
|
176
|
+
'constant',
|
|
177
|
+
]);
|
|
178
|
+
mocks.variableInputProps?.onChange?.(constantValue);
|
|
179
|
+
expect(onChange).toHaveBeenCalledWith(constantValue);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it.each(['exact', 'past', 'next', 'today'])(
|
|
183
|
+
'initializes %s as a complete formatted expression for a non-date field',
|
|
184
|
+
(nodeName) => {
|
|
185
|
+
const onChange = renderInput({ isDateLikeField: false });
|
|
186
|
+
const initial = mocks.variableInputProps?.converters?.resolveValueFromPath?.({
|
|
187
|
+
name: nodeName,
|
|
188
|
+
title: nodeName,
|
|
189
|
+
type: 'date',
|
|
190
|
+
paths: ['date', nodeName],
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
mocks.variableInputProps?.onChange?.(initial);
|
|
194
|
+
|
|
195
|
+
const stored = onChange.mock.calls[0]?.[0];
|
|
196
|
+
const parsed = parseCtxDateExpressionConfig(stored);
|
|
197
|
+
expect(parsed).toBeDefined();
|
|
198
|
+
expect(parsed?.format).toBe('YYYY-MM-DD');
|
|
199
|
+
},
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
it('uses a datetime default Format for Now and restores a custom Format without emitting a change', () => {
|
|
203
|
+
const onChange = renderInput({ isDateLikeField: false });
|
|
204
|
+
const initial = mocks.variableInputProps?.converters?.resolveValueFromPath?.({
|
|
205
|
+
name: 'now',
|
|
206
|
+
title: 'Now',
|
|
207
|
+
type: 'date',
|
|
208
|
+
paths: ['date', 'now'],
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
mocks.variableInputProps?.onChange?.(initial);
|
|
212
|
+
expect(parseCtxDateExpressionConfig(onChange.mock.calls[0]?.[0])?.format).toBe('YYYY-MM-DD HH:mm:ss');
|
|
213
|
+
|
|
214
|
+
const customExpression = serializeCtxDateExpressionConfig({
|
|
215
|
+
kind: 'preset',
|
|
216
|
+
preset: 'today',
|
|
217
|
+
format: 'YYYY/MM/DD',
|
|
218
|
+
});
|
|
219
|
+
if (!customExpression) throw new Error('Expected a formatted Date expression');
|
|
220
|
+
const customOnChange = renderInput({ value: customExpression, isDateLikeField: false });
|
|
221
|
+
|
|
222
|
+
mocks.variableInputProps?.onChange?.(mocks.variableInputProps.value);
|
|
223
|
+
expect(mocks.variableInputProps?.value).toMatchObject({ kind: 'preset', preset: 'today', format: 'YYYY/MM/DD' });
|
|
224
|
+
expect(customOnChange).not.toHaveBeenCalled();
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('does not add the default Format while restoring a legacy expression for a non-date field', () => {
|
|
228
|
+
const onChange = renderInput({ value: '{{ ctx.date.preset.today }}', isDateLikeField: false });
|
|
229
|
+
|
|
230
|
+
mocks.variableInputProps?.onChange?.(mocks.variableInputProps.value);
|
|
231
|
+
|
|
232
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('keeps date-like field expressions unformatted', () => {
|
|
236
|
+
const onChange = renderInput({ isDateLikeField: true });
|
|
237
|
+
const initial = mocks.variableInputProps?.converters?.resolveValueFromPath?.({
|
|
238
|
+
name: 'today',
|
|
239
|
+
title: 'Today',
|
|
240
|
+
type: 'date',
|
|
241
|
+
paths: ['date', 'today'],
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
mocks.variableInputProps?.onChange?.(initial);
|
|
245
|
+
|
|
246
|
+
expect(parseCtxDateExpressionConfig(onChange.mock.calls[0]?.[0])).toEqual({
|
|
247
|
+
kind: 'preset',
|
|
248
|
+
preset: 'today',
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const formattedExpression = serializeCtxDateExpressionConfig({
|
|
252
|
+
kind: 'preset',
|
|
253
|
+
preset: 'today',
|
|
254
|
+
format: 'YYYY/MM/DD',
|
|
255
|
+
});
|
|
256
|
+
if (!formattedExpression) throw new Error('Expected a formatted Date expression');
|
|
257
|
+
const formattedOnChange = renderInput({ value: formattedExpression, isDateLikeField: true });
|
|
258
|
+
|
|
259
|
+
mocks.variableInputProps?.onChange?.(mocks.variableInputProps.value);
|
|
260
|
+
expect(mocks.variableInputProps?.value).toMatchObject({ kind: 'preset', preset: 'today', format: 'YYYY/MM/DD' });
|
|
261
|
+
expect(formattedOnChange).not.toHaveBeenCalled();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('preserves spaces while editing a custom Format', () => {
|
|
265
|
+
const expression = serializeCtxDateExpressionConfig({
|
|
266
|
+
kind: 'preset',
|
|
267
|
+
preset: 'today',
|
|
268
|
+
format: 'YYYY-MM-DD',
|
|
269
|
+
});
|
|
270
|
+
if (!expression) throw new Error('Expected a formatted Date expression');
|
|
271
|
+
const onChange = renderInput({ value: expression, isDateLikeField: false });
|
|
272
|
+
const DateEditor = mocks.variableInputProps?.converters?.renderInputComponent?.({
|
|
273
|
+
name: 'today',
|
|
274
|
+
title: 'Today',
|
|
275
|
+
type: 'date',
|
|
276
|
+
paths: ['date', 'today'],
|
|
277
|
+
});
|
|
278
|
+
if (!DateEditor) throw new Error('Expected the Date editor');
|
|
279
|
+
|
|
280
|
+
render(<DateEditor value={mocks.variableInputProps?.value} onChange={mocks.variableInputProps?.onChange} />);
|
|
281
|
+
fireEvent.change(screen.getByRole('combobox', { name: 'Format' }), {
|
|
282
|
+
target: { value: 'YYYY-MM-DD ' },
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
expect(parseCtxDateExpressionConfig(onChange.mock.calls[0]?.[0])?.format).toBe('YYYY-MM-DD ');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
describe('DateVariableEditor Format', () => {
|
|
290
|
+
it('stores date-only picker values without converting them to a timezone-dependent ISO instant', () => {
|
|
291
|
+
expect(serializeExactDatePickerValue(dayjs('2026-02-12'), false)).toBe('2026-02-12');
|
|
292
|
+
expect(serializeExactDatePickerValue([dayjs('2026-02-12'), dayjs('2026-02-20')], false)).toEqual([
|
|
293
|
+
'2026-02-12',
|
|
294
|
+
'2026-02-20',
|
|
295
|
+
]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it('shows Format for non-date fields', () => {
|
|
299
|
+
render(
|
|
300
|
+
<DateVariableEditor
|
|
301
|
+
value={{ kind: 'preset', preset: 'today' }}
|
|
302
|
+
isDateLikeField={false}
|
|
303
|
+
dateComponentProps={DEFAULT_DATE_VARIABLE_COMPONENT_PROPS}
|
|
304
|
+
/>,
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
expect(screen.getByRole('combobox', { name: 'Format' })).toBeInTheDocument();
|
|
308
|
+
expect(screen.queryByRole('textbox', { name: 'Preview' })).not.toBeInTheDocument();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('keeps the relative amount visible beside its fixed-width unit selector', () => {
|
|
312
|
+
render(
|
|
313
|
+
<DateVariableEditor
|
|
314
|
+
value={{ kind: 'relative', direction: 'next', amount: 1, unit: 'day' }}
|
|
315
|
+
isDateLikeField={false}
|
|
316
|
+
dateComponentProps={DEFAULT_DATE_VARIABLE_COMPONENT_PROPS}
|
|
317
|
+
/>,
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const amountInput = screen.getByRole('spinbutton', { name: 'Next' });
|
|
321
|
+
expect(amountInput).toHaveValue('1');
|
|
322
|
+
expect(amountInput.closest('.ant-input-number')).toHaveStyle({ minWidth: '80px' });
|
|
323
|
+
expect(screen.getByRole('combobox', { name: 'Unit' }).closest('.ant-select')).toHaveStyle({ flex: '0 0 130px' });
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('shows the default Format as a placeholder when a legacy expression has no Format', () => {
|
|
327
|
+
render(
|
|
328
|
+
<DateVariableEditor
|
|
329
|
+
value={{ kind: 'preset', preset: 'now' }}
|
|
330
|
+
isDateLikeField={false}
|
|
331
|
+
dateComponentProps={DEFAULT_DATE_VARIABLE_COMPONENT_PROPS}
|
|
332
|
+
/>,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const formatInput = screen.getByRole('combobox', { name: 'Format' });
|
|
336
|
+
expect(formatInput).toHaveValue('');
|
|
337
|
+
expect(screen.getByText('YYYY-MM-DD HH:mm:ss')).toHaveClass('ant-select-selection-placeholder');
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('hides Format for date-like fields', () => {
|
|
341
|
+
render(
|
|
342
|
+
<DateVariableEditor
|
|
343
|
+
value={{ kind: 'preset', preset: 'today' }}
|
|
344
|
+
isDateLikeField
|
|
345
|
+
dateComponentProps={DEFAULT_DATE_VARIABLE_COMPONENT_PROPS}
|
|
346
|
+
/>,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
expect(screen.queryByRole('combobox', { name: 'Format' })).not.toBeInTheDocument();
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe('date-like field detection', () => {
|
|
354
|
+
it('treats dateOnly interface and type as pure date fields', () => {
|
|
355
|
+
expect(isDateLikeField({ interface: 'dateOnly' })).toBe(true);
|
|
356
|
+
expect(isDateLikeField({ type: 'dateOnly' })).toBe(true);
|
|
357
|
+
expect(resolveDateVariableComponentProps({ interface: 'dateOnly' })).toMatchObject({
|
|
358
|
+
showTime: false,
|
|
359
|
+
exactNormalizeMode: 'date',
|
|
360
|
+
});
|
|
361
|
+
expect(resolveDateVariableComponentProps({ type: 'dateOnly' })).toMatchObject({
|
|
362
|
+
showTime: false,
|
|
363
|
+
exactNormalizeMode: 'date',
|
|
364
|
+
});
|
|
365
|
+
expect(
|
|
366
|
+
resolveDateVariableComponentProps({
|
|
367
|
+
interface: 'dateOnly',
|
|
368
|
+
getComponentProps: () => ({ showTime: true, format: 'YYYY-MM-DD HH:mm:ss' }),
|
|
369
|
+
}),
|
|
370
|
+
).toMatchObject({
|
|
371
|
+
showTime: false,
|
|
372
|
+
format: 'YYYY-MM-DD',
|
|
373
|
+
exactNormalizeMode: 'date',
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it('uses the resolved fallback interface when the preferred field metadata omits it', () => {
|
|
378
|
+
expect(isDateLikeField({}, 'scheduledAt', 'datetime')).toBe(true);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
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 { isCtxDateExpression, isRunJSValue, isVariableExpression, serializeCtxDateValue } from '@nocobase/flow-engine';
|
|
11
|
+
import { dayjs } from '@nocobase/utils/client';
|
|
12
|
+
import type { ExactDatePickerMode } from '../FieldAssignExactDatePicker';
|
|
13
|
+
|
|
14
|
+
const DATE_FIELD_INTERFACES = new Set([
|
|
15
|
+
'date',
|
|
16
|
+
'dateOnly',
|
|
17
|
+
'datetime',
|
|
18
|
+
'datetimeNoTz',
|
|
19
|
+
'createdAt',
|
|
20
|
+
'updatedAt',
|
|
21
|
+
'unixTimestamp',
|
|
22
|
+
]);
|
|
23
|
+
const TZ_AWARE_DATE_INTERFACES = new Set(['datetime', 'createdAt', 'updatedAt', 'unixTimestamp']);
|
|
24
|
+
const DATE_ONLY_OUTPUT_FORMAT = 'YYYY-MM-DD';
|
|
25
|
+
const DATETIME_NO_TZ_OUTPUT_FORMAT = 'YYYY-MM-DD HH:mm:ss';
|
|
26
|
+
|
|
27
|
+
type DateFieldLike = {
|
|
28
|
+
interface?: unknown;
|
|
29
|
+
type?: unknown;
|
|
30
|
+
getComponentProps?: () => unknown;
|
|
31
|
+
uiSchema?: Record<string, unknown>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type DateVariableExactNormalizeMode = 'none' | 'date' | 'datetimeNoTz' | 'iso';
|
|
35
|
+
|
|
36
|
+
export type DateVariableComponentProps = {
|
|
37
|
+
picker: ExactDatePickerMode;
|
|
38
|
+
showTime: boolean;
|
|
39
|
+
timeFormat: string;
|
|
40
|
+
format: string;
|
|
41
|
+
exactNormalizeMode: DateVariableExactNormalizeMode;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const DEFAULT_DATE_VARIABLE_COMPONENT_PROPS: DateVariableComponentProps = {
|
|
45
|
+
picker: 'date',
|
|
46
|
+
showTime: false,
|
|
47
|
+
timeFormat: 'HH:mm:ss',
|
|
48
|
+
format: DATE_ONLY_OUTPUT_FORMAT,
|
|
49
|
+
exactNormalizeMode: 'none',
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export function getFieldInterface(field: DateFieldLike | null | undefined): string {
|
|
53
|
+
return typeof field?.interface === 'string' ? field.interface : '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function getFieldComponentProps(field: DateFieldLike | null | undefined): Record<string, unknown> {
|
|
57
|
+
const fromGetter = typeof field?.getComponentProps === 'function' ? field.getComponentProps() : undefined;
|
|
58
|
+
if (fromGetter && typeof fromGetter === 'object' && !Array.isArray(fromGetter)) {
|
|
59
|
+
return fromGetter as Record<string, unknown>;
|
|
60
|
+
}
|
|
61
|
+
const fromSchema = field?.uiSchema?.['x-component-props'];
|
|
62
|
+
return fromSchema && typeof fromSchema === 'object' && !Array.isArray(fromSchema)
|
|
63
|
+
? (fromSchema as Record<string, unknown>)
|
|
64
|
+
: {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeExactDatePickerMode(value: unknown): ExactDatePickerMode {
|
|
68
|
+
if (value === 'year' || value === 'quarter' || value === 'month' || value === 'date') {
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
return 'date';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function getDateVariableExactNormalizeMode(
|
|
75
|
+
fieldInterface: string,
|
|
76
|
+
field: DateFieldLike | null | undefined,
|
|
77
|
+
): DateVariableExactNormalizeMode {
|
|
78
|
+
if (fieldInterface === 'date' || fieldInterface === 'dateOnly' || field?.type === 'dateOnly') return 'date';
|
|
79
|
+
if (fieldInterface === 'datetimeNoTz') return 'datetimeNoTz';
|
|
80
|
+
if (TZ_AWARE_DATE_INTERFACES.has(fieldInterface)) return 'iso';
|
|
81
|
+
return 'none';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function isDateLikeField(
|
|
85
|
+
field: DateFieldLike | null | undefined,
|
|
86
|
+
fallbackFieldName?: string,
|
|
87
|
+
fallbackFieldInterface?: string,
|
|
88
|
+
): boolean {
|
|
89
|
+
const fieldInterface = getFieldInterface(field) || fallbackFieldInterface || '';
|
|
90
|
+
if (DATE_FIELD_INTERFACES.has(fieldInterface) || field?.type === 'dateOnly') return true;
|
|
91
|
+
return fallbackFieldName === 'createdAt' || fallbackFieldName === 'updatedAt';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function resolveDateVariableComponentProps(
|
|
95
|
+
field: DateFieldLike | null | undefined,
|
|
96
|
+
fieldInterface = getFieldInterface(field),
|
|
97
|
+
): DateVariableComponentProps {
|
|
98
|
+
const componentProps = getFieldComponentProps(field);
|
|
99
|
+
const picker = normalizeExactDatePickerMode(componentProps.picker);
|
|
100
|
+
const isDateOnly = fieldInterface === 'date' || fieldInterface === 'dateOnly' || field?.type === 'dateOnly';
|
|
101
|
+
const inferredShowTime = ['datetime', 'datetimeNoTz', 'createdAt', 'updatedAt', 'unixTimestamp'].includes(
|
|
102
|
+
fieldInterface,
|
|
103
|
+
);
|
|
104
|
+
const showTime = isDateOnly
|
|
105
|
+
? false
|
|
106
|
+
: typeof componentProps.showTime === 'boolean'
|
|
107
|
+
? componentProps.showTime
|
|
108
|
+
: inferredShowTime;
|
|
109
|
+
const configuredFormat = typeof componentProps.format === 'string' ? componentProps.format : '';
|
|
110
|
+
const dateFormat =
|
|
111
|
+
typeof componentProps.dateFormat === 'string' && componentProps.dateFormat
|
|
112
|
+
? componentProps.dateFormat
|
|
113
|
+
: configuredFormat.split(' ')[0] || DATE_ONLY_OUTPUT_FORMAT;
|
|
114
|
+
const timeFormat =
|
|
115
|
+
typeof componentProps.timeFormat === 'string' && componentProps.timeFormat ? componentProps.timeFormat : 'HH:mm:ss';
|
|
116
|
+
const format = isDateOnly ? dateFormat : configuredFormat || (showTime ? `${dateFormat} ${timeFormat}` : dateFormat);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
picker,
|
|
120
|
+
showTime,
|
|
121
|
+
timeFormat,
|
|
122
|
+
format,
|
|
123
|
+
exactNormalizeMode: getDateVariableExactNormalizeMode(fieldInterface, field),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseDateByFormat(value: string, format: string): dayjs.Dayjs | null {
|
|
128
|
+
const raw = String(value || '').trim();
|
|
129
|
+
if (!raw) return null;
|
|
130
|
+
|
|
131
|
+
const hasTimezone = /(?:Z|[+-]\d{2}:\d{2})$/i.test(raw);
|
|
132
|
+
if (hasTimezone) {
|
|
133
|
+
const parsed = dayjs(raw);
|
|
134
|
+
if (parsed.isValid()) return parsed;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (format) {
|
|
138
|
+
const strict = dayjs(raw, format, true);
|
|
139
|
+
if (strict.isValid()) return strict;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const fallback = dayjs(raw);
|
|
143
|
+
if (fallback.isValid()) return fallback;
|
|
144
|
+
|
|
145
|
+
if (format) {
|
|
146
|
+
const loose = dayjs(raw, format);
|
|
147
|
+
if (loose.isValid()) return loose;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function parseDateFromRawValue(value: unknown, format: string): dayjs.Dayjs | null {
|
|
154
|
+
if (dayjs.isDayjs(value)) return value;
|
|
155
|
+
if (value instanceof Date) {
|
|
156
|
+
const parsedDate = dayjs(value);
|
|
157
|
+
return parsedDate.isValid() ? parsedDate : null;
|
|
158
|
+
}
|
|
159
|
+
return typeof value === 'string' ? parseDateByFormat(value, format) : null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function normalizeExactDateValue(
|
|
163
|
+
value: unknown,
|
|
164
|
+
options: Pick<DateVariableComponentProps, 'format' | 'showTime' | 'exactNormalizeMode'>,
|
|
165
|
+
): unknown {
|
|
166
|
+
const parsed = parseDateFromRawValue(value, options.format);
|
|
167
|
+
if (!parsed?.isValid()) return value;
|
|
168
|
+
|
|
169
|
+
switch (options.exactNormalizeMode) {
|
|
170
|
+
case 'date':
|
|
171
|
+
return parsed.format(DATE_ONLY_OUTPUT_FORMAT);
|
|
172
|
+
case 'datetimeNoTz':
|
|
173
|
+
return parsed.format(options.showTime ? DATETIME_NO_TZ_OUTPUT_FORMAT : DATE_ONLY_OUTPUT_FORMAT);
|
|
174
|
+
case 'iso':
|
|
175
|
+
return parsed.toISOString();
|
|
176
|
+
default:
|
|
177
|
+
return value;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function normalizeDateVariableExactValue(
|
|
182
|
+
rawValue: unknown,
|
|
183
|
+
options: Pick<DateVariableComponentProps, 'format' | 'showTime' | 'exactNormalizeMode'>,
|
|
184
|
+
): unknown {
|
|
185
|
+
if (options.exactNormalizeMode === 'none') return rawValue;
|
|
186
|
+
if (typeof rawValue === 'string' || dayjs.isDayjs(rawValue) || rawValue instanceof Date) {
|
|
187
|
+
return normalizeExactDateValue(rawValue, options);
|
|
188
|
+
}
|
|
189
|
+
if (Array.isArray(rawValue)) {
|
|
190
|
+
return rawValue.map((item) => {
|
|
191
|
+
if (typeof item === 'string' || dayjs.isDayjs(item) || item instanceof Date) {
|
|
192
|
+
return normalizeExactDateValue(item, options);
|
|
193
|
+
}
|
|
194
|
+
return item;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return rawValue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function toExactPickerDisplayValue(
|
|
201
|
+
rawValue: unknown,
|
|
202
|
+
options: { format: string; isRange: boolean },
|
|
203
|
+
): dayjs.Dayjs | [dayjs.Dayjs, dayjs.Dayjs] | null {
|
|
204
|
+
if (options.isRange) {
|
|
205
|
+
if (!Array.isArray(rawValue)) return null;
|
|
206
|
+
const left = parseDateFromRawValue(rawValue[0], options.format);
|
|
207
|
+
const right = parseDateFromRawValue(rawValue[1], options.format);
|
|
208
|
+
return left && right ? [left, right] : null;
|
|
209
|
+
}
|
|
210
|
+
return parseDateFromRawValue(rawValue, options.format);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Compatibility helper retained for existing callers and tests. */
|
|
214
|
+
export function normalizeDateVariableOutput(rawValue: unknown, options: DateVariableComponentProps): unknown {
|
|
215
|
+
if (rawValue === null || isRunJSValue(rawValue)) return rawValue;
|
|
216
|
+
if (typeof rawValue === 'string' && isVariableExpression(rawValue) && !isCtxDateExpression(rawValue)) {
|
|
217
|
+
return rawValue;
|
|
218
|
+
}
|
|
219
|
+
if (rawValue === '' || typeof rawValue === 'undefined') return '';
|
|
220
|
+
|
|
221
|
+
const normalized = normalizeDateVariableExactValue(rawValue, options);
|
|
222
|
+
return serializeCtxDateValue(normalized) || normalized;
|
|
223
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
export * from './dateValue';
|
|
11
|
+
export * from './DateVariableEditor';
|
|
12
|
+
export * from './FieldValueVariableInput';
|
|
@@ -34,6 +34,8 @@ const { DateFilterDynamicComponent: DateFilterDynamicComponentLazy } = lazy(
|
|
|
34
34
|
'DateFilterDynamicComponent',
|
|
35
35
|
);
|
|
36
36
|
|
|
37
|
+
const fallbackInputComponents = new Set(['Input', 'Input.URL', 'NanoIDInput', 'DatePicker']);
|
|
38
|
+
|
|
37
39
|
// 简化的本地辅助:按需获取 ctx.collection 的字段树(MetaTreeNode[])
|
|
38
40
|
async function buildCollectionLeftMetaTreeLocal(ctx: any): Promise<MetaTreeNode[]> {
|
|
39
41
|
const resolve = async (sub: any): Promise<MetaTreeNode[]> => {
|
|
@@ -276,9 +278,9 @@ function createStaticInputRenderer(
|
|
|
276
278
|
onChange={(v: unknown) => onChange?.(v as VariableFilterItemValue['value'])}
|
|
277
279
|
/>
|
|
278
280
|
);
|
|
279
|
-
//
|
|
281
|
+
// 未直接处理的 x-component:尝试从 app 解析组件,可由筛选器接管的过渡组件查找失败时静默回退到 Input
|
|
280
282
|
if (xComp && app?.getComponent) {
|
|
281
|
-
const Comp = app.getComponent(xComp
|
|
283
|
+
const Comp = app.getComponent(xComp, !fallbackInputComponents.has(xComp));
|
|
282
284
|
if (Comp) {
|
|
283
285
|
return (
|
|
284
286
|
<Comp
|
|
@@ -595,8 +597,14 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
595
597
|
|
|
596
598
|
const renderRightValueComponent = useCallback(() => {
|
|
597
599
|
// 文本类多关键词:优先使用操作符 schema 声明的组件
|
|
598
|
-
const resolved = resolveOperatorComponent(model.context.app, operator, operatorMetaList);
|
|
599
600
|
const supportKeyword = operator === '$in' || operator === '$notIn';
|
|
601
|
+
const operatorComponent = currentOpMeta?.schema?.['x-component'] as string | undefined;
|
|
602
|
+
const isLocallyRenderedComponent =
|
|
603
|
+
operatorComponent === 'Select' ||
|
|
604
|
+
(typeof operatorComponent === 'string' && fallbackInputComponents.has(operatorComponent));
|
|
605
|
+
const resolved = supportKeyword
|
|
606
|
+
? resolveOperatorComponent(model.context.app, operator, operatorMetaList, !isLocallyRenderedComponent)
|
|
607
|
+
: null;
|
|
600
608
|
if (resolved && supportKeyword) {
|
|
601
609
|
const { Comp, props: xProps } = resolved;
|
|
602
610
|
const nextProps = { ...xProps };
|
|
@@ -642,6 +650,7 @@ export const VariableFilterItem: React.FC<VariableFilterItemProps> = observer(
|
|
|
642
650
|
disabled,
|
|
643
651
|
operator,
|
|
644
652
|
operatorMetaList,
|
|
653
|
+
currentOpMeta,
|
|
645
654
|
rightValue,
|
|
646
655
|
staticInputRenderer,
|
|
647
656
|
model.context.app,
|