@nocobase/client-v2 2.1.0-alpha.30 → 2.1.0-alpha.31
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/actions/linkageRulesFormValueRefresh.d.ts +10 -0
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/actions/AssociateActionModel.d.ts +19 -0
- package/es/flow/models/actions/AssociationActionUtils.d.ts +17 -0
- package/es/flow/models/actions/DisassociateActionModel.d.ts +16 -0
- package/es/flow/models/actions/index.d.ts +3 -0
- package/es/flow/models/base/GridModel.d.ts +3 -1
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +15 -6
- package/es/flow/models/blocks/shared/filterOperators.d.ts +9 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.d.ts +9 -0
- package/es/flow-compat/data.d.ts +9 -2
- package/es/flow-compat/index.d.ts +1 -1
- package/es/index.d.ts +1 -0
- package/es/index.mjs +90 -90
- package/lib/index.js +87 -87
- package/package.json +5 -5
- package/src/BaseApplication.tsx +1 -1
- package/src/__tests__/app.test.tsx +23 -6
- package/src/__tests__/globalDeps.test.ts +5 -0
- package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +438 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +42 -0
- package/src/flow/actions/linkageRules.tsx +8 -1
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +492 -0
- package/src/flow/actions/linkageRulesRefresh.tsx +4 -2
- package/src/flow/actions/titleField.tsx +8 -3
- package/src/flow/components/FieldAssignValueInput.tsx +1 -0
- package/src/flow/components/filter/LinkageFilterItem.tsx +6 -5
- package/src/flow/components/filter/VariableFilterItem.tsx +14 -13
- package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +33 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -5
- package/src/flow/index.ts +1 -0
- package/src/flow/internal/utils/__tests__/titleFieldQuickSync.test.ts +1 -0
- package/src/flow/internal/utils/titleFieldQuickSync.ts +2 -2
- package/src/flow/models/actions/AssociateActionModel.tsx +196 -0
- package/src/flow/models/actions/AssociationActionUtils.ts +90 -0
- package/src/flow/models/actions/DisassociateActionModel.tsx +57 -0
- package/src/flow/models/actions/FilterActionModel.tsx +17 -9
- package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +250 -0
- package/src/flow/models/actions/index.ts +3 -0
- package/src/flow/models/base/GridModel.tsx +21 -1
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +98 -0
- package/src/flow/models/blocks/details/DetailsItemModel.tsx +3 -0
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +200 -36
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +270 -1
- package/src/flow/models/blocks/filter-form/__tests__/customFieldOperators.test.tsx +23 -0
- package/src/flow/models/blocks/filter-form/customFieldOperators.ts +12 -1
- package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +22 -8
- package/src/flow/models/blocks/filter-form/fields/__tests__/FilterFormCustomFieldModel.recordSelect.test.tsx +18 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +51 -1
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +75 -0
- package/src/flow/models/blocks/form/FormItemModel.tsx +48 -28
- package/src/flow/models/blocks/shared/filterOperators.ts +14 -0
- package/src/flow/models/blocks/table/TableBlockModel.tsx +19 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +5 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +21 -5
- package/src/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.ts +20 -0
- package/src/flow/models/fields/DividerItemModel.tsx +30 -15
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +11 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +235 -0
- package/src/flow-compat/data.ts +25 -3
- package/src/flow-compat/index.ts +7 -1
- package/src/index.ts +1 -0
- package/src/utils/globalDeps.ts +6 -0
|
@@ -22,6 +22,7 @@ import { FieldModel } from '../../base/FieldModel';
|
|
|
22
22
|
import { DetailsItemModel } from '../details/DetailsItemModel';
|
|
23
23
|
import { EditFormModel } from './EditFormModel';
|
|
24
24
|
import _ from 'lodash';
|
|
25
|
+
import { Tooltip } from 'antd';
|
|
25
26
|
import { coerceForToOneField } from '../../../internal/utils/associationValueCoercion';
|
|
26
27
|
import { buildDynamicNamePath } from './dynamicNamePath';
|
|
27
28
|
|
|
@@ -99,54 +100,63 @@ export class FormItemModel<T extends DefaultStructure = DefaultStructure> extend
|
|
|
99
100
|
|
|
100
101
|
renderItem() {
|
|
101
102
|
const fieldModel = this.subModels.field as FieldModel;
|
|
102
|
-
// 行索引(来自数组子表单)
|
|
103
103
|
const idx = this.context.fieldIndex;
|
|
104
104
|
const fieldKey = this.context.fieldKey;
|
|
105
105
|
const parentFieldPathArray = this.parent?.context.fieldPathArray || [];
|
|
106
|
+
const isHiddenReservedValuePreview =
|
|
107
|
+
!!this.context.flowSettingsEnabled && !!this.props.hidden && !this.hidden && !this.forbidden;
|
|
106
108
|
|
|
107
|
-
// 嵌套场景下继续传透,为字段子模型创建 fork
|
|
108
109
|
const modelForRender =
|
|
109
|
-
idx != null
|
|
110
|
+
idx != null || isHiddenReservedValuePreview
|
|
110
111
|
? (() => {
|
|
111
|
-
const
|
|
112
|
-
fork.
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
get: () => fieldKey,
|
|
117
|
-
});
|
|
118
|
-
if (this.context.currentObject) {
|
|
119
|
-
fork.context.defineProperty('currentObject', {
|
|
120
|
-
get: () => this.context.currentObject,
|
|
112
|
+
const forkKey = isHiddenReservedValuePreview ? `${this.uid}:config-visible` : `${fieldKey}`;
|
|
113
|
+
const fork = fieldModel.createFork({}, forkKey);
|
|
114
|
+
if (idx != null) {
|
|
115
|
+
fork.context.defineProperty('fieldIndex', {
|
|
116
|
+
get: () => idx,
|
|
121
117
|
});
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if (this.context.item) {
|
|
125
|
-
const { value: _value, ...rest } = (itemOptions || {}) as any;
|
|
126
|
-
fork.context.defineProperty('item', {
|
|
127
|
-
...rest,
|
|
128
|
-
get: () => this.context.item,
|
|
129
|
-
cache: false,
|
|
118
|
+
fork.context.defineProperty('fieldKey', {
|
|
119
|
+
get: () => fieldKey,
|
|
130
120
|
});
|
|
121
|
+
if (this.context.currentObject) {
|
|
122
|
+
fork.context.defineProperty('currentObject', {
|
|
123
|
+
get: () => this.context.currentObject,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const itemOptions = this.context.getPropertyOptions('item');
|
|
127
|
+
if (this.context.item) {
|
|
128
|
+
const { value: _value, ...rest } = (itemOptions || {}) as any;
|
|
129
|
+
fork.context.defineProperty('item', {
|
|
130
|
+
...rest,
|
|
131
|
+
get: () => this.context.item,
|
|
132
|
+
cache: false,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
if (this.context.pattern) {
|
|
136
|
+
fork.context.defineProperty('pattern', {
|
|
137
|
+
get: () => this.context.pattern,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
131
140
|
}
|
|
132
|
-
if (
|
|
133
|
-
fork.
|
|
134
|
-
get: () => this.context.pattern,
|
|
135
|
-
});
|
|
141
|
+
if (isHiddenReservedValuePreview) {
|
|
142
|
+
fork.setProps({ hidden: false });
|
|
136
143
|
}
|
|
137
144
|
return fork;
|
|
138
145
|
})()
|
|
139
146
|
: fieldModel;
|
|
140
147
|
const mergedProps = this.context.pattern ? { ...this.props, pattern: this.context.pattern } : this.props;
|
|
141
|
-
const { initialValue, ...mergedPropsWithoutInitial } = mergedProps as any;
|
|
148
|
+
const { initialValue, hidden, ...mergedPropsWithoutInitial } = mergedProps as any;
|
|
149
|
+
const formItemProps = isHiddenReservedValuePreview
|
|
150
|
+
? mergedPropsWithoutInitial
|
|
151
|
+
: { hidden, ...mergedPropsWithoutInitial };
|
|
142
152
|
const fieldPath = buildDynamicNamePath(this.props.name, idx);
|
|
143
153
|
this.context.defineProperty('fieldPathArray', {
|
|
144
154
|
value: [...parentFieldPathArray, ..._.castArray(fieldPath)],
|
|
145
155
|
});
|
|
146
156
|
const record = this.context.item?.value || this.context.record;
|
|
147
|
-
|
|
157
|
+
const content = (
|
|
148
158
|
<FormItem
|
|
149
|
-
{...
|
|
159
|
+
{...formItemProps}
|
|
150
160
|
name={fieldPath}
|
|
151
161
|
validateFirst={true}
|
|
152
162
|
disabled={
|
|
@@ -158,6 +168,16 @@ export class FormItemModel<T extends DefaultStructure = DefaultStructure> extend
|
|
|
158
168
|
<FieldModelRenderer model={modelForRender} name={fieldPath} />
|
|
159
169
|
</FormItem>
|
|
160
170
|
);
|
|
171
|
+
|
|
172
|
+
if (isHiddenReservedValuePreview) {
|
|
173
|
+
return (
|
|
174
|
+
<Tooltip title={this.context.t('The field is hidden and only visible when the UI Editor is active')}>
|
|
175
|
+
<div style={{ opacity: 0.3 }}>{content}</div>
|
|
176
|
+
</Tooltip>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return content;
|
|
161
181
|
}
|
|
162
182
|
}
|
|
163
183
|
|
|
@@ -0,0 +1,14 @@
|
|
|
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 function isArrayLikeField(field: any) {
|
|
11
|
+
return (
|
|
12
|
+
['multipleSelect', 'checkboxGroup'].includes(field?.interface) || ['array', 'json', 'jsonb'].includes(field?.type)
|
|
13
|
+
);
|
|
14
|
+
}
|
|
@@ -563,6 +563,9 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
|
|
|
563
563
|
columns={this.columns.value}
|
|
564
564
|
pagination={this.pagination()}
|
|
565
565
|
highlightedRowKey={highlightedRowKey}
|
|
566
|
+
selectedRowKeysFromResource={this.resource
|
|
567
|
+
.getSelectedRows()
|
|
568
|
+
.map((row) => getRowKey(row, this.collection.filterTargetKey))}
|
|
566
569
|
defaultExpandAllRows={this.props.defaultExpandAllRows}
|
|
567
570
|
expandedRowKeys={this.props.expandedRowKeys}
|
|
568
571
|
heightMode={heightMode}
|
|
@@ -581,6 +584,7 @@ const TableBlockContent = (props: {
|
|
|
581
584
|
columns: any;
|
|
582
585
|
pagination: any;
|
|
583
586
|
highlightedRowKey: string;
|
|
587
|
+
selectedRowKeysFromResource: string[];
|
|
584
588
|
defaultExpandAllRows?: boolean;
|
|
585
589
|
expandedRowKeys?: any[];
|
|
586
590
|
heightMode?: string;
|
|
@@ -594,6 +598,7 @@ const TableBlockContent = (props: {
|
|
|
594
598
|
columns,
|
|
595
599
|
pagination,
|
|
596
600
|
highlightedRowKey,
|
|
601
|
+
selectedRowKeysFromResource,
|
|
597
602
|
defaultExpandAllRows,
|
|
598
603
|
expandedRowKeys,
|
|
599
604
|
heightMode,
|
|
@@ -620,6 +625,7 @@ const TableBlockContent = (props: {
|
|
|
620
625
|
columns={columns}
|
|
621
626
|
pagination={pagination}
|
|
622
627
|
highlightedRowKey={highlightedRowKey}
|
|
628
|
+
selectedRowKeysFromResource={selectedRowKeysFromResource}
|
|
623
629
|
defaultExpandAllRows={defaultExpandAllRows}
|
|
624
630
|
expandedRowKeys={expandedRowKeys}
|
|
625
631
|
tableScroll={tableScroll}
|
|
@@ -829,6 +835,7 @@ const HighPerformanceTable = React.memo(
|
|
|
829
835
|
columns: any;
|
|
830
836
|
pagination: any;
|
|
831
837
|
highlightedRowKey: string;
|
|
838
|
+
selectedRowKeysFromResource: string[];
|
|
832
839
|
defaultExpandAllRows?: boolean;
|
|
833
840
|
expandedRowKeys?: any[];
|
|
834
841
|
tableScroll;
|
|
@@ -841,6 +848,7 @@ const HighPerformanceTable = React.memo(
|
|
|
841
848
|
columns,
|
|
842
849
|
pagination: _pagination,
|
|
843
850
|
highlightedRowKey,
|
|
851
|
+
selectedRowKeysFromResource,
|
|
844
852
|
defaultExpandAllRows,
|
|
845
853
|
expandedRowKeys,
|
|
846
854
|
tableScroll,
|
|
@@ -848,9 +856,17 @@ const HighPerformanceTable = React.memo(
|
|
|
848
856
|
const dataSourceRef = useRef(dataSource);
|
|
849
857
|
dataSourceRef.current = dataSource;
|
|
850
858
|
|
|
851
|
-
const [selectedRowKeys, setSelectedRowKeys] = React.useState<string[]>(
|
|
852
|
-
|
|
853
|
-
)
|
|
859
|
+
const [selectedRowKeys, setSelectedRowKeys] = React.useState<string[]>(selectedRowKeysFromResource || []);
|
|
860
|
+
|
|
861
|
+
useEffect(() => {
|
|
862
|
+
const nextSelectedRowKeys = selectedRowKeysFromResource || [];
|
|
863
|
+
setSelectedRowKeys((prev) => {
|
|
864
|
+
if (_.isEqual(prev, nextSelectedRowKeys)) {
|
|
865
|
+
return prev;
|
|
866
|
+
}
|
|
867
|
+
return nextSelectedRowKeys;
|
|
868
|
+
});
|
|
869
|
+
}, [selectedRowKeysFromResource]);
|
|
854
870
|
|
|
855
871
|
const getRowKeyFunc = useCallback(
|
|
856
872
|
(record) => {
|
|
@@ -38,7 +38,8 @@ import {
|
|
|
38
38
|
import { MobileLazySelect } from '../mobile-components/MobileLazySelect';
|
|
39
39
|
import { BlockSceneEnum } from '../../base/BlockModel';
|
|
40
40
|
import { ActionWithoutPermission } from '../../base/ActionModel';
|
|
41
|
-
import { EditFormModel } from '../../blocks
|
|
41
|
+
import { EditFormModel } from '../../blocks';
|
|
42
|
+
import { hasAncestorModel } from './recordSelectSettingsUtils';
|
|
42
43
|
|
|
43
44
|
function isPlainObject(val: unknown): val is Record<string, any> {
|
|
44
45
|
return !!val && typeof val === 'object' && !Array.isArray(val);
|
|
@@ -818,6 +819,9 @@ RecordSelectFieldModel.registerFlow({
|
|
|
818
819
|
if (ctx?.blockModel?.constructor?.scene === BlockSceneEnum.filter) {
|
|
819
820
|
return true;
|
|
820
821
|
}
|
|
822
|
+
if (hasAncestorModel(ctx?.model, ['SubTableColumnModel', 'SubTableFieldModel'])) {
|
|
823
|
+
return true;
|
|
824
|
+
}
|
|
821
825
|
return false;
|
|
822
826
|
},
|
|
823
827
|
defaultParams: {
|
package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx
CHANGED
|
@@ -194,7 +194,7 @@ function shouldCommitImmediately(value: any) {
|
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
const FieldModelRendererOptimize = React.memo((props: any) => {
|
|
197
|
-
const { model, onChange, value, ...rest } = props;
|
|
197
|
+
const { model, onChange, value, commitOnChange, ...rest } = props;
|
|
198
198
|
const pendingValueRef = React.useRef<any>(props?.value);
|
|
199
199
|
|
|
200
200
|
useEffect(() => {
|
|
@@ -204,11 +204,11 @@ const FieldModelRendererOptimize = React.memo((props: any) => {
|
|
|
204
204
|
const handleChange = React.useCallback(
|
|
205
205
|
(value: any) => {
|
|
206
206
|
pendingValueRef.current = value;
|
|
207
|
-
if (shouldCommitImmediately(value)) {
|
|
207
|
+
if (commitOnChange || shouldCommitImmediately(value)) {
|
|
208
208
|
onChange?.(value);
|
|
209
209
|
}
|
|
210
210
|
},
|
|
211
|
-
[onChange],
|
|
211
|
+
[commitOnChange, onChange],
|
|
212
212
|
);
|
|
213
213
|
|
|
214
214
|
const handleCommit = React.useCallback(() => {
|
|
@@ -241,10 +241,11 @@ interface CellProps {
|
|
|
241
241
|
rowFork?: any;
|
|
242
242
|
memoKey?: string;
|
|
243
243
|
width?: number;
|
|
244
|
+
commitOnChange?: boolean;
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
const MemoCell: React.FC<CellProps> = React.memo(
|
|
247
|
-
({ value, record, rowIdx, id, parent, parentFieldIndex, rowFork, width }) => {
|
|
248
|
+
({ value, record, rowIdx, id, parent, parentFieldIndex, rowFork, width, commitOnChange }) => {
|
|
248
249
|
const isNew = record?.__is_new__;
|
|
249
250
|
return (
|
|
250
251
|
<div
|
|
@@ -346,7 +347,11 @@ const MemoCell: React.FC<CellProps> = React.memo(
|
|
|
346
347
|
}
|
|
347
348
|
/>
|
|
348
349
|
) : (
|
|
349
|
-
<FieldModelRendererOptimize
|
|
350
|
+
<FieldModelRendererOptimize
|
|
351
|
+
model={fork}
|
|
352
|
+
id={[(parent as any).context.fieldPath, rowIdx]}
|
|
353
|
+
commitOnChange={commitOnChange}
|
|
354
|
+
/>
|
|
350
355
|
)}
|
|
351
356
|
</FormItem>
|
|
352
357
|
);
|
|
@@ -360,6 +365,7 @@ const MemoCell: React.FC<CellProps> = React.memo(
|
|
|
360
365
|
prev.id === next.id &&
|
|
361
366
|
prev.memoKey === next.memoKey &&
|
|
362
367
|
prev.width === next.width &&
|
|
368
|
+
prev.commitOnChange === next.commitOnChange &&
|
|
363
369
|
prev.rowIdx === next.rowIdx
|
|
364
370
|
);
|
|
365
371
|
},
|
|
@@ -428,6 +434,15 @@ export class SubTableColumnModel<
|
|
|
428
434
|
return this.parent.collection;
|
|
429
435
|
}
|
|
430
436
|
|
|
437
|
+
get hasFormulaColumn() {
|
|
438
|
+
return (
|
|
439
|
+
this.parent?.mapSubModels('columns', (column: SubTableColumnModel) => {
|
|
440
|
+
const field = column.collectionField;
|
|
441
|
+
return field?.interface === 'formula' || field?.type === 'formula';
|
|
442
|
+
}) || []
|
|
443
|
+
).some(Boolean);
|
|
444
|
+
}
|
|
445
|
+
|
|
431
446
|
onInit(options: any): void {
|
|
432
447
|
super.onInit(options);
|
|
433
448
|
this.context.defineProperty('resourceName', {
|
|
@@ -607,6 +622,7 @@ export class SubTableColumnModel<
|
|
|
607
622
|
rowFork={rowFork}
|
|
608
623
|
memoKey={rowForkKey}
|
|
609
624
|
width={this.props.width}
|
|
625
|
+
commitOnChange={this.hasFormulaColumn}
|
|
610
626
|
/>
|
|
611
627
|
);
|
|
612
628
|
};
|
|
@@ -0,0 +1,20 @@
|
|
|
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 function hasAncestorModel(model: any, modelNames: string[]) {
|
|
11
|
+
let cursor = model;
|
|
12
|
+
while (cursor) {
|
|
13
|
+
const modelName = cursor?.constructor?.name;
|
|
14
|
+
if (modelName && modelNames.includes(modelName)) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
cursor = cursor?.parent;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
@@ -8,25 +8,40 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { FormItem } from '@nocobase/flow-engine';
|
|
11
|
-
import { Divider } from 'antd';
|
|
11
|
+
import { Divider, theme } from 'antd';
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { CommonItemModel } from '../base/CommonItemModel';
|
|
14
14
|
import { NBColorPicker } from './ColorFieldModel';
|
|
15
15
|
|
|
16
|
+
const resolveThemeColor = (value: string | undefined, fallback: string) => {
|
|
17
|
+
return value ? value : fallback;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const DividerItem = (props: any) => {
|
|
21
|
+
const { token } = theme.useToken();
|
|
22
|
+
const { color, borderColor, label, orientation, dashed } = props;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<Divider
|
|
26
|
+
type="horizontal"
|
|
27
|
+
style={{
|
|
28
|
+
color: resolveThemeColor(color, token.colorText),
|
|
29
|
+
borderColor: resolveThemeColor(borderColor, token.colorSplit),
|
|
30
|
+
}}
|
|
31
|
+
orientationMargin="0"
|
|
32
|
+
orientation={orientation}
|
|
33
|
+
dashed={dashed}
|
|
34
|
+
>
|
|
35
|
+
{label}
|
|
36
|
+
</Divider>
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
16
40
|
export class DividerItemModel extends CommonItemModel {
|
|
17
41
|
render() {
|
|
18
|
-
const { color, borderColor, label, orientation, dashed } = this.props;
|
|
19
42
|
return (
|
|
20
43
|
<FormItem shouldUpdate showLabel={false}>
|
|
21
|
-
<
|
|
22
|
-
type="horizontal"
|
|
23
|
-
style={{ color, borderColor }}
|
|
24
|
-
orientationMargin="0"
|
|
25
|
-
orientation={orientation}
|
|
26
|
-
dashed={dashed}
|
|
27
|
-
>
|
|
28
|
-
{label}
|
|
29
|
-
</Divider>
|
|
44
|
+
<DividerItem {...this.props} />
|
|
30
45
|
</FormItem>
|
|
31
46
|
);
|
|
32
47
|
}
|
|
@@ -38,12 +53,12 @@ DividerItemModel.registerFlow({
|
|
|
38
53
|
steps: {
|
|
39
54
|
title: {
|
|
40
55
|
title: '{{t("Edit divider")}}',
|
|
41
|
-
defaultParams: {
|
|
56
|
+
defaultParams: (ctx) => ({
|
|
42
57
|
label: '{{t("Text")}}',
|
|
43
58
|
orientation: 'left',
|
|
44
|
-
color:
|
|
45
|
-
borderColor:
|
|
46
|
-
},
|
|
59
|
+
color: ctx.themeToken?.colorText,
|
|
60
|
+
borderColor: ctx.themeToken?.colorSplit,
|
|
61
|
+
}),
|
|
47
62
|
uiSchema(ctx) {
|
|
48
63
|
return {
|
|
49
64
|
label: {
|
|
@@ -13,7 +13,7 @@ import { Button, CheckList, Popup, SearchBar } from 'antd-mobile';
|
|
|
13
13
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
14
14
|
|
|
15
15
|
export function MobileSelect(props) {
|
|
16
|
-
const { value, onChange, disabled, options = [], mode } = props;
|
|
16
|
+
const { value, onChange, onChangeComplete, disabled, options = [], mode } = props;
|
|
17
17
|
const ctx = useFlowModelContext();
|
|
18
18
|
const t = ctx.t;
|
|
19
19
|
const [visible, setVisible] = useState(false);
|
|
@@ -28,6 +28,7 @@ export function MobileSelect(props) {
|
|
|
28
28
|
|
|
29
29
|
const handleConfirm = () => {
|
|
30
30
|
onChange(selected);
|
|
31
|
+
onChangeComplete?.();
|
|
31
32
|
setVisible(false);
|
|
32
33
|
};
|
|
33
34
|
useEffect(() => {
|
|
@@ -36,12 +37,18 @@ export function MobileSelect(props) {
|
|
|
36
37
|
} else {
|
|
37
38
|
setSearchText(null);
|
|
38
39
|
}
|
|
39
|
-
}, [visible]);
|
|
40
|
+
}, [visible, value]);
|
|
40
41
|
|
|
41
42
|
return (
|
|
42
43
|
<>
|
|
43
44
|
<div onClick={() => !disabled && setVisible(true)}>
|
|
44
|
-
<Select
|
|
45
|
+
<Select
|
|
46
|
+
{...props}
|
|
47
|
+
open={false}
|
|
48
|
+
dropdownStyle={{ display: 'none' }}
|
|
49
|
+
showSearch={false}
|
|
50
|
+
style={{ pointerEvents: 'none', width: '100%' }}
|
|
51
|
+
/>
|
|
45
52
|
</div>
|
|
46
53
|
<Popup
|
|
47
54
|
visible={visible}
|
|
@@ -71,6 +78,7 @@ export function MobileSelect(props) {
|
|
|
71
78
|
} else {
|
|
72
79
|
setSelected(val[0]);
|
|
73
80
|
onChange(val[0]);
|
|
81
|
+
onChangeComplete?.();
|
|
74
82
|
setVisible(false);
|
|
75
83
|
}
|
|
76
84
|
}}
|
|
@@ -0,0 +1,235 @@
|
|
|
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 React from 'react';
|
|
11
|
+
import { beforeEach, describe, it, expect, vi } from 'vitest';
|
|
12
|
+
import { act, fireEvent, render, screen } from '@nocobase/test/client';
|
|
13
|
+
import { MobileSelect } from '../MobileSelect';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_OPTIONS = [
|
|
16
|
+
{ label: 'Option A', value: 'a' },
|
|
17
|
+
{ label: 'Option B', value: 'b' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const mockState = vi.hoisted(() => ({
|
|
21
|
+
selectProps: undefined as any,
|
|
22
|
+
popupProps: undefined as any,
|
|
23
|
+
checklistProps: undefined as any,
|
|
24
|
+
confirmButtonProps: undefined as any,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
function resetMockState() {
|
|
28
|
+
mockState.selectProps = undefined;
|
|
29
|
+
mockState.popupProps = undefined;
|
|
30
|
+
mockState.checklistProps = undefined;
|
|
31
|
+
mockState.confirmButtonProps = undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function clickTrigger() {
|
|
35
|
+
const trigger = screen.getByTestId('antd-select').parentElement as HTMLElement | null;
|
|
36
|
+
expect(trigger).toBeTruthy();
|
|
37
|
+
act(() => {
|
|
38
|
+
fireEvent.click(trigger as HTMLElement);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function openPopup() {
|
|
43
|
+
clickTrigger();
|
|
44
|
+
expect(screen.getByTestId('popup')).toBeInTheDocument();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function selectValues(values: string[]) {
|
|
48
|
+
act(() => {
|
|
49
|
+
mockState.checklistProps?.onChange?.(values);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function confirmSelection() {
|
|
54
|
+
act(() => {
|
|
55
|
+
mockState.confirmButtonProps?.onClick?.();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderMobileSelect(props: Record<string, any> = {}) {
|
|
60
|
+
const onChange = props.onChange ?? vi.fn();
|
|
61
|
+
const onChangeComplete = props.onChangeComplete ?? vi.fn();
|
|
62
|
+
|
|
63
|
+
render(
|
|
64
|
+
<MobileSelect
|
|
65
|
+
value={undefined}
|
|
66
|
+
options={DEFAULT_OPTIONS}
|
|
67
|
+
onChange={onChange}
|
|
68
|
+
onChangeComplete={onChangeComplete}
|
|
69
|
+
{...props}
|
|
70
|
+
/>,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return { onChange, onChangeComplete };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
vi.mock('@nocobase/flow-engine', async () => {
|
|
77
|
+
const actual = await vi.importActual<any>('@nocobase/flow-engine');
|
|
78
|
+
return {
|
|
79
|
+
...actual,
|
|
80
|
+
useFlowModelContext: () => ({
|
|
81
|
+
t: (value: string) => value,
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
vi.mock('antd', async () => {
|
|
87
|
+
const actual = await vi.importActual<any>('antd');
|
|
88
|
+
return {
|
|
89
|
+
...actual,
|
|
90
|
+
Select: (props: any) => {
|
|
91
|
+
mockState.selectProps = props;
|
|
92
|
+
return <div data-testid="antd-select" />;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
vi.mock('antd-mobile', () => {
|
|
98
|
+
const MockCheckList: any = (props: any) => {
|
|
99
|
+
mockState.checklistProps = props;
|
|
100
|
+
return <div data-testid="checklist">{props.children}</div>;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
MockCheckList.Item = ({ value, children }: any) => <div data-testid={`item-${value}`}>{children}</div>;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
Button: (props: any) => {
|
|
107
|
+
mockState.confirmButtonProps = props;
|
|
108
|
+
return (
|
|
109
|
+
<button type="button" data-testid="confirm" onClick={props.onClick}>
|
|
110
|
+
{props.children}
|
|
111
|
+
</button>
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
Popup: (props: any) => {
|
|
115
|
+
mockState.popupProps = props;
|
|
116
|
+
return props.visible ? <div data-testid="popup">{props.children}</div> : null;
|
|
117
|
+
},
|
|
118
|
+
SearchBar: ({ value, onChange }: any) => (
|
|
119
|
+
<input data-testid="search" value={value ?? ''} onChange={(e) => onChange?.(e.target.value)} />
|
|
120
|
+
),
|
|
121
|
+
CheckList: MockCheckList,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('MobileSelect', () => {
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
resetMockState();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('commits the selected value immediately in single mode', () => {
|
|
131
|
+
const { onChange, onChangeComplete } = renderMobileSelect();
|
|
132
|
+
|
|
133
|
+
openPopup();
|
|
134
|
+
selectValues(['a']);
|
|
135
|
+
|
|
136
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
137
|
+
expect(onChange).toHaveBeenCalledWith('a');
|
|
138
|
+
expect(onChangeComplete).toHaveBeenCalledTimes(1);
|
|
139
|
+
expect(screen.queryByTestId('popup')).not.toBeInTheDocument();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('renders filtered options based on search text', () => {
|
|
143
|
+
const { onChange, onChangeComplete } = renderMobileSelect();
|
|
144
|
+
openPopup();
|
|
145
|
+
act(() => {
|
|
146
|
+
fireEvent.change(screen.getByTestId('search'), { target: { value: 'Option A' } });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(screen.getByTestId('item-a')).toBeInTheDocument();
|
|
150
|
+
expect(screen.queryByTestId('item-b')).not.toBeInTheDocument();
|
|
151
|
+
|
|
152
|
+
selectValues(['a']);
|
|
153
|
+
|
|
154
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
155
|
+
expect(onChange).toHaveBeenCalledWith('a');
|
|
156
|
+
expect(onChangeComplete).toHaveBeenCalledTimes(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('defers commit until confirm in multiple mode', () => {
|
|
160
|
+
const { onChange, onChangeComplete } = renderMobileSelect({ value: [], mode: 'multiple' });
|
|
161
|
+
openPopup();
|
|
162
|
+
|
|
163
|
+
selectValues(['a', 'b']);
|
|
164
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
165
|
+
expect(onChangeComplete).not.toHaveBeenCalled();
|
|
166
|
+
|
|
167
|
+
confirmSelection();
|
|
168
|
+
|
|
169
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
170
|
+
expect(onChange).toHaveBeenCalledWith(['a', 'b']);
|
|
171
|
+
expect(onChangeComplete).toHaveBeenCalledTimes(1);
|
|
172
|
+
expect(screen.queryByTestId('popup')).not.toBeInTheDocument();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('does not open popup when disabled', () => {
|
|
176
|
+
renderMobileSelect({ disabled: true });
|
|
177
|
+
|
|
178
|
+
clickTrigger();
|
|
179
|
+
expect(screen.queryByTestId('popup')).not.toBeInTheDocument();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
function SubTableCellHarness({ value, onCommit, mode }: { value: any; onCommit: (value: any) => void; mode?: string }) {
|
|
184
|
+
const pendingValueRef = React.useRef<any>(value);
|
|
185
|
+
return (
|
|
186
|
+
<div>
|
|
187
|
+
<MobileSelect
|
|
188
|
+
value={value}
|
|
189
|
+
mode={mode}
|
|
190
|
+
options={DEFAULT_OPTIONS}
|
|
191
|
+
onChange={(next) => {
|
|
192
|
+
pendingValueRef.current = next;
|
|
193
|
+
if (Array.isArray(next)) {
|
|
194
|
+
onCommit(next);
|
|
195
|
+
}
|
|
196
|
+
}}
|
|
197
|
+
onChangeComplete={() => {
|
|
198
|
+
onCommit(pendingValueRef.current);
|
|
199
|
+
}}
|
|
200
|
+
/>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
describe('MobileSelect in SubForm/SubTable containers', () => {
|
|
206
|
+
beforeEach(() => {
|
|
207
|
+
resetMockState();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('SubTable: single selection commits final value via onChangeComplete', () => {
|
|
211
|
+
const onCommit = vi.fn();
|
|
212
|
+
|
|
213
|
+
render(<SubTableCellHarness value={undefined} onCommit={onCommit} />);
|
|
214
|
+
|
|
215
|
+
openPopup();
|
|
216
|
+
selectValues(['b']);
|
|
217
|
+
|
|
218
|
+
expect(onCommit).toHaveBeenCalledTimes(1);
|
|
219
|
+
expect(onCommit).toHaveBeenCalledWith('b');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('SubTable: multiple mode only commits after confirm, and commit receives the full array', () => {
|
|
223
|
+
const onCommit = vi.fn();
|
|
224
|
+
|
|
225
|
+
render(<SubTableCellHarness value={[]} onCommit={onCommit} mode="multiple" />);
|
|
226
|
+
|
|
227
|
+
openPopup();
|
|
228
|
+
selectValues(['a', 'b']);
|
|
229
|
+
confirmSelection();
|
|
230
|
+
|
|
231
|
+
expect(onCommit).toHaveBeenCalledTimes(2);
|
|
232
|
+
expect(onCommit).toHaveBeenNthCalledWith(1, ['a', 'b']);
|
|
233
|
+
expect(onCommit).toHaveBeenNthCalledWith(2, ['a', 'b']);
|
|
234
|
+
});
|
|
235
|
+
});
|