@nocobase/client-v2 2.2.0-beta.17 → 2.2.0-beta.18
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/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
- package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -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 +1 -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.mjs +125 -116
- package/lib/index.js +162 -153
- 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__/settings-center.test.tsx +270 -6
- package/src/acl/ACLProvider.tsx +2 -0
- package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
- 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.subFormSetFieldProps.test.ts +72 -0
- package/src/flow/actions/linkageRules.tsx +8 -3
- 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/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/base/CollectionBlockModel.tsx +23 -1
- package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +65 -9
- 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 +43 -0
- package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
- 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__/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/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 +121 -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 +5 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +122 -14
- package/src/settings-center/AdminSettingsLayout.tsx +23 -2
|
@@ -0,0 +1,131 @@
|
|
|
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 { FlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import { CreateFormModel } from '../CreateFormModel';
|
|
13
|
+
import { EditFormModel } from '../EditFormModel';
|
|
14
|
+
import { FormActionModel } from '../FormActionModel';
|
|
15
|
+
|
|
16
|
+
function prepareFormModel<T extends CreateFormModel | EditFormModel>(model: T, publicFormRuntime = false) {
|
|
17
|
+
const engine = new FlowEngine();
|
|
18
|
+
engine.registerModels({ CreateFormModel, EditFormModel });
|
|
19
|
+
model.flowEngine = engine;
|
|
20
|
+
Object.defineProperty(model, 'context', {
|
|
21
|
+
value: {
|
|
22
|
+
publicFormRuntime,
|
|
23
|
+
message: { error: vi.fn() },
|
|
24
|
+
t: (value: string) => value,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
return model;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function createSubmitAction() {
|
|
31
|
+
const onClick = vi.fn();
|
|
32
|
+
const action = {
|
|
33
|
+
getFlow: vi.fn((key: string) => (key === 'submitSettings' ? {} : undefined)),
|
|
34
|
+
onClick,
|
|
35
|
+
} as unknown as FormActionModel;
|
|
36
|
+
return { action, onClick };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('FormBlockModel.submitFromRunJs', () => {
|
|
40
|
+
it('dispatches the first action with a submitSettings flow', () => {
|
|
41
|
+
const { action, onClick } = createSubmitAction();
|
|
42
|
+
const { action: secondAction, onClick: secondOnClick } = createSubmitAction();
|
|
43
|
+
const blockModel = prepareFormModel(
|
|
44
|
+
Object.assign(Object.create(CreateFormModel.prototype), {
|
|
45
|
+
mapSubModels: (_key: string, callback: (item: FormActionModel) => FormActionModel) => [
|
|
46
|
+
callback(action),
|
|
47
|
+
callback(secondAction),
|
|
48
|
+
],
|
|
49
|
+
}) as CreateFormModel,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
blockModel.submitFromRunJs();
|
|
53
|
+
|
|
54
|
+
expect(onClick).toHaveBeenCalledWith(undefined);
|
|
55
|
+
expect(secondOnClick).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not dispatch an edit submit action without an available record', () => {
|
|
59
|
+
const { action, onClick } = createSubmitAction();
|
|
60
|
+
const blockModel = prepareFormModel(
|
|
61
|
+
Object.assign(Object.create(EditFormModel.prototype), {
|
|
62
|
+
hasAvailableData: () => false,
|
|
63
|
+
mapSubModels: (_key: string, callback: (item: FormActionModel) => FormActionModel) => [callback(action)],
|
|
64
|
+
}) as EditFormModel,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
blockModel.submitFromRunJs();
|
|
68
|
+
|
|
69
|
+
expect(onClick).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('falls back to the core form submit handler when no submit action exists', () => {
|
|
73
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
74
|
+
const blockModel = prepareFormModel(
|
|
75
|
+
Object.assign(Object.create(CreateFormModel.prototype), {
|
|
76
|
+
mapSubModels: vi.fn(() => []),
|
|
77
|
+
submit,
|
|
78
|
+
}) as CreateFormModel,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
blockModel.submitFromRunJs();
|
|
82
|
+
|
|
83
|
+
expect(submit).toHaveBeenCalledOnce();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('falls back to the core edit submit handler when a record exists', () => {
|
|
87
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
88
|
+
const blockModel = prepareFormModel(
|
|
89
|
+
Object.assign(Object.create(EditFormModel.prototype), {
|
|
90
|
+
hasAvailableData: () => true,
|
|
91
|
+
mapSubModels: vi.fn(() => []),
|
|
92
|
+
submit,
|
|
93
|
+
}) as EditFormModel,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
blockModel.submitFromRunJs();
|
|
97
|
+
|
|
98
|
+
expect(submit).toHaveBeenCalledOnce();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('uses the core fallback for specialized form subclasses', () => {
|
|
102
|
+
class SpecializedCreateFormModel extends CreateFormModel {}
|
|
103
|
+
|
|
104
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
105
|
+
const blockModel = prepareFormModel(
|
|
106
|
+
Object.assign(Object.create(SpecializedCreateFormModel.prototype), {
|
|
107
|
+
mapSubModels: vi.fn(() => []),
|
|
108
|
+
submit,
|
|
109
|
+
}) as SpecializedCreateFormModel,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
blockModel.submitFromRunJs();
|
|
113
|
+
|
|
114
|
+
expect(submit).toHaveBeenCalledOnce();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('does not use the core fallback for public forms without a submit action', () => {
|
|
118
|
+
const submit = vi.fn().mockResolvedValue(undefined);
|
|
119
|
+
const blockModel = prepareFormModel(
|
|
120
|
+
Object.assign(Object.create(CreateFormModel.prototype), {
|
|
121
|
+
mapSubModels: vi.fn(() => []),
|
|
122
|
+
submit,
|
|
123
|
+
}) as CreateFormModel,
|
|
124
|
+
true,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
blockModel.submitFromRunJs();
|
|
128
|
+
|
|
129
|
+
expect(submit).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -36,7 +36,15 @@ import { BlockSceneEnum } from '../../base/BlockModel';
|
|
|
36
36
|
import { CollectionBlockModel } from '../../base/CollectionBlockModel';
|
|
37
37
|
import { QuickEditFormModel } from '../form/QuickEditFormModel';
|
|
38
38
|
import { TableColumnModel } from './TableColumnModel';
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
extractIndex,
|
|
41
|
+
adjustColumnOrder,
|
|
42
|
+
setNestedValue,
|
|
43
|
+
extractIds,
|
|
44
|
+
extractRowKeys,
|
|
45
|
+
getRowKey,
|
|
46
|
+
useBlockHeight,
|
|
47
|
+
} from './utils';
|
|
40
48
|
import { resolveTableSorterField } from './sortUtils';
|
|
41
49
|
import { commonConditionHandler, ConditionBuilder } from '../../../components/ConditionBuilder';
|
|
42
50
|
import { BulkDeleteActionModel } from '../../actions/BulkDeleteActionModel';
|
|
@@ -222,6 +230,22 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
|
|
|
222
230
|
return super.resource as MultiRecordResource;
|
|
223
231
|
}
|
|
224
232
|
|
|
233
|
+
resetAfterFilterChange() {
|
|
234
|
+
if (!this.props.treeTable) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const expandAll = !!this.props.defaultExpandAllRows;
|
|
239
|
+
const expandedRowKeys = expandAll ? extractRowKeys(this.resource.getData(), this.collection.filterTargetKey) : [];
|
|
240
|
+
|
|
241
|
+
this.setProps('expandedRowKeys', expandedRowKeys);
|
|
242
|
+
this.mapSubModels('actions', (action) => {
|
|
243
|
+
if ('setExpandFlag' in action && typeof action.setExpandFlag === 'function') {
|
|
244
|
+
action.setExpandFlag(expandAll);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
225
249
|
private readonly columns = observable.ref([]);
|
|
226
250
|
private disposeAutorun: () => void;
|
|
227
251
|
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
ModelRenderMode,
|
|
28
28
|
useFlowModel,
|
|
29
29
|
} from '@nocobase/flow-engine';
|
|
30
|
+
import type { FlowModel } from '@nocobase/flow-engine';
|
|
30
31
|
import { useTranslation } from 'react-i18next';
|
|
31
32
|
import { TableColumnProps, Tooltip, Space, InputNumber, Button, Divider } from 'antd';
|
|
32
33
|
import { get, omit, capitalize } from 'lodash';
|
|
@@ -61,19 +62,28 @@ export function FieldDeletePlaceholder(props: any) {
|
|
|
61
62
|
);
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
type FieldPermissionPlaceholderModel = FlowModel & {
|
|
66
|
+
fieldPath?: string;
|
|
67
|
+
forbidden?: { actionName?: string } | null;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export function FieldWithoutPermissionPlaceholder({
|
|
71
|
+
targetModel,
|
|
72
|
+
}: {
|
|
73
|
+
targetModel?: FieldPermissionPlaceholderModel;
|
|
74
|
+
} = {}) {
|
|
65
75
|
const { t } = useTranslation();
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
const collection = model.context.collectionField?.collection || blockModel
|
|
69
|
-
const dataSource = collection
|
|
76
|
+
const contextModel = useFlowModel<FieldPermissionPlaceholderModel>();
|
|
77
|
+
const model = targetModel || contextModel;
|
|
78
|
+
const collection = model.context.collectionField?.collection || model.context.blockModel?.collection;
|
|
79
|
+
const dataSource = collection?.dataSource;
|
|
70
80
|
const name = model.context.collectionField?.name || model.fieldPath;
|
|
71
81
|
const nameValue = useMemo(() => {
|
|
72
|
-
const dataSourcePrefix = `${t(dataSource.displayName || dataSource.key)} >
|
|
82
|
+
const dataSourcePrefix = dataSource ? `${t(dataSource.displayName || dataSource.key)} > ` : '';
|
|
73
83
|
const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} > ` : '';
|
|
74
84
|
return `${dataSourcePrefix}${collectionPrefix}${name}`;
|
|
75
|
-
}, [collection, dataSource
|
|
76
|
-
const
|
|
85
|
+
}, [collection, dataSource, name, t]);
|
|
86
|
+
const actionName = model.forbidden?.actionName || 'view';
|
|
77
87
|
const messageValue = useMemo(() => {
|
|
78
88
|
return t(
|
|
79
89
|
`The current user only has the UI configuration permission, but don't have "{{actionName}}" permission for field "{{name}}"`,
|
|
@@ -0,0 +1,78 @@
|
|
|
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 { describe, expect, it, vi } from 'vitest';
|
|
11
|
+
import { TableBlockModel } from '../TableBlockModel';
|
|
12
|
+
|
|
13
|
+
describe('TableBlockModel filter reset', () => {
|
|
14
|
+
it('collapses the filtered tree when rows are not expanded by default', () => {
|
|
15
|
+
const setExpandFlag = vi.fn();
|
|
16
|
+
const setProps = vi.fn();
|
|
17
|
+
const model = {
|
|
18
|
+
props: { treeTable: true, defaultExpandAllRows: false, expandedRowKeys: [1, 2] },
|
|
19
|
+
resource: { getData: () => [{ id: 3, children: [{ id: 4 }] }] },
|
|
20
|
+
setProps,
|
|
21
|
+
mapSubModels: (_key, callback) => callback({ setExpandFlag }),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
TableBlockModel.prototype.resetAfterFilterChange.call(model);
|
|
25
|
+
|
|
26
|
+
expect(setProps).toHaveBeenCalledWith('expandedRowKeys', []);
|
|
27
|
+
expect(setExpandFlag).toHaveBeenCalledWith(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('expands the filtered tree when rows are expanded by default', () => {
|
|
31
|
+
const setExpandFlag = vi.fn();
|
|
32
|
+
const setProps = vi.fn();
|
|
33
|
+
const model = {
|
|
34
|
+
props: { treeTable: true, defaultExpandAllRows: true, expandedRowKeys: [1, 2] },
|
|
35
|
+
collection: { filterTargetKey: 'id' },
|
|
36
|
+
resource: { getData: () => [{ id: 3, children: [{ id: 4 }] }] },
|
|
37
|
+
setProps,
|
|
38
|
+
mapSubModels: (_key, callback) => callback({ setExpandFlag }),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
TableBlockModel.prototype.resetAfterFilterChange.call(model);
|
|
42
|
+
|
|
43
|
+
expect(setProps).toHaveBeenCalledWith('expandedRowKeys', [3, 4]);
|
|
44
|
+
expect(setExpandFlag).toHaveBeenCalledWith(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('uses the collection filter target key when expanding the filtered tree', () => {
|
|
48
|
+
const setProps = vi.fn();
|
|
49
|
+
const model = {
|
|
50
|
+
props: { treeTable: true, defaultExpandAllRows: true },
|
|
51
|
+
collection: { filterTargetKey: ['tenant', 'slug'] },
|
|
52
|
+
resource: {
|
|
53
|
+
getData: () => [{ tenant: 'acme', slug: 'root', children: [{ tenant: 'acme', slug: 'child' }] }],
|
|
54
|
+
},
|
|
55
|
+
setProps,
|
|
56
|
+
mapSubModels: vi.fn(),
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
TableBlockModel.prototype.resetAfterFilterChange.call(model);
|
|
60
|
+
|
|
61
|
+
expect(setProps).toHaveBeenCalledWith('expandedRowKeys', ['acme-root', 'acme-child']);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('does not change expansion state for a regular table', () => {
|
|
65
|
+
const setProps = vi.fn();
|
|
66
|
+
const mapSubModels = vi.fn();
|
|
67
|
+
const model = {
|
|
68
|
+
props: { treeTable: false },
|
|
69
|
+
setProps,
|
|
70
|
+
mapSubModels,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
TableBlockModel.prototype.resetAfterFilterChange.call(model);
|
|
74
|
+
|
|
75
|
+
expect(setProps).not.toHaveBeenCalled();
|
|
76
|
+
expect(mapSubModels).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -67,6 +67,13 @@ export function extractIds(data) {
|
|
|
67
67
|
return ids;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
export function extractRowKeys(data: Record<string, unknown>[], key: string | string[]) {
|
|
71
|
+
return data.flatMap((item) => {
|
|
72
|
+
const children = Array.isArray(item.children) ? extractRowKeys(item.children, key) : [];
|
|
73
|
+
return [getRowKey(item, key), ...children];
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
70
77
|
/**
|
|
71
78
|
* 生成表格行唯一 key
|
|
72
79
|
* @param record 当前行数据
|
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Cascader, Space, Button } from 'antd';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
CollectionField,
|
|
13
|
+
EditableItemModel,
|
|
14
|
+
FilterableItemModel,
|
|
15
|
+
MultiRecordResource,
|
|
16
|
+
tExpr,
|
|
17
|
+
} from '@nocobase/flow-engine';
|
|
12
18
|
import { DeleteOutlined } from '@ant-design/icons';
|
|
13
19
|
import { css, cx } from '@emotion/css';
|
|
14
20
|
import { debounce, castArray, omit, last, isEqual } from 'lodash';
|
|
@@ -31,6 +37,23 @@ function normalizeLabelKeys(labelKeyOrKeys?: string | string[]): string[] {
|
|
|
31
37
|
return Array.from(new Set(list.filter((item): item is string => !!item && typeof item === 'string')));
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
function normalizeCascadeFieldKey(key: unknown): string | undefined {
|
|
41
|
+
if (typeof key === 'string' && key) {
|
|
42
|
+
return key;
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(key) && key.length === 1 && typeof key[0] === 'string') {
|
|
45
|
+
return key[0];
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getDefaultCascadeFieldNames(field: CollectionField) {
|
|
51
|
+
const targetCollection = field?.targetCollection;
|
|
52
|
+
const value = normalizeCascadeFieldKey(targetCollection?.filterTargetKey) || 'id';
|
|
53
|
+
const label = targetCollection?.titleCollectionField?.name || value;
|
|
54
|
+
return { label, value };
|
|
55
|
+
}
|
|
56
|
+
|
|
34
57
|
function getRecordDisplayLabel(item: any, labelKeyOrKeys?: string | string[]): string | null {
|
|
35
58
|
if (!isPlainRecord(item)) return null;
|
|
36
59
|
const labelKeys = normalizeLabelKeys(labelKeyOrKeys);
|
|
@@ -815,6 +838,15 @@ EditableItemModel.bindModelToInterface('CascadeSelectFieldModel', ['m2o', 'o2o',
|
|
|
815
838
|
order: 60,
|
|
816
839
|
});
|
|
817
840
|
|
|
841
|
+
FilterableItemModel.bindModelToInterface('CascadeSelectFieldModel', ['m2o', 'o2o', 'oho', 'obo'], {
|
|
842
|
+
when: (ctx, field) => field.targetCollection?.template === 'tree',
|
|
843
|
+
isDefault: true,
|
|
844
|
+
order: 60,
|
|
845
|
+
defaultProps: (ctx, field) => ({
|
|
846
|
+
fieldNames: getDefaultCascadeFieldNames(field),
|
|
847
|
+
}),
|
|
848
|
+
});
|
|
849
|
+
|
|
818
850
|
EditableItemModel.bindModelToInterface('CascadeSelectListFieldModel', ['m2m', 'o2m', 'mbm'], {
|
|
819
851
|
when: (ctx, field) => field.targetCollection?.template === 'tree',
|
|
820
852
|
isDefault: true,
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
11
|
CollectionField,
|
|
12
|
+
MultiRecordResource,
|
|
12
13
|
EditableItemModel,
|
|
13
14
|
tExpr,
|
|
14
15
|
FlowModel,
|
|
@@ -31,7 +32,12 @@ import {
|
|
|
31
32
|
createRootItemChain,
|
|
32
33
|
type ItemChain,
|
|
33
34
|
} from './itemChain';
|
|
34
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
buildOpenerUids,
|
|
37
|
+
LabelByField,
|
|
38
|
+
type AssociationFieldNames,
|
|
39
|
+
useAssociationValueHydration,
|
|
40
|
+
} from './recordSelectShared';
|
|
35
41
|
|
|
36
42
|
const MULTIPLE_ASSOCIATION_TYPES = ['belongsToMany', 'hasMany', 'belongsToArray'];
|
|
37
43
|
|
|
@@ -328,6 +334,13 @@ function RecordPickerField(props) {
|
|
|
328
334
|
useEffect(() => {
|
|
329
335
|
ctx.model.selectedRows.value = props.value;
|
|
330
336
|
}, [ctx.model.selectedRows, props.value]);
|
|
337
|
+
useAssociationValueHydration({
|
|
338
|
+
model: ctx.model,
|
|
339
|
+
value: props.value,
|
|
340
|
+
isMultiple: allowMultiple,
|
|
341
|
+
fieldNames,
|
|
342
|
+
onChange: props.onChange,
|
|
343
|
+
});
|
|
331
344
|
|
|
332
345
|
return (
|
|
333
346
|
<Select
|
|
@@ -400,6 +413,7 @@ function RecordPickerField(props) {
|
|
|
400
413
|
}
|
|
401
414
|
|
|
402
415
|
export class RecordPickerFieldModel extends FieldModel {
|
|
416
|
+
declare resource: MultiRecordResource;
|
|
403
417
|
selectedRows = observable.ref([]);
|
|
404
418
|
_closeView;
|
|
405
419
|
selectBlockModel;
|
|
@@ -564,6 +578,15 @@ RecordPickerFieldModel.registerFlow({
|
|
|
564
578
|
title: tExpr('RecordPicker settings'),
|
|
565
579
|
sort: 200,
|
|
566
580
|
steps: {
|
|
581
|
+
init: {
|
|
582
|
+
handler(ctx) {
|
|
583
|
+
const { target, dataSourceKey } = ctx.model.collectionField;
|
|
584
|
+
const resource = ctx.createResource(MultiRecordResource);
|
|
585
|
+
resource.setDataSourceKey(dataSourceKey);
|
|
586
|
+
resource.setResourceName(target);
|
|
587
|
+
ctx.model.resource = resource;
|
|
588
|
+
},
|
|
589
|
+
},
|
|
567
590
|
fieldNames: {
|
|
568
591
|
use: 'titleField',
|
|
569
592
|
},
|
|
@@ -22,7 +22,7 @@ import { css } from '@emotion/css';
|
|
|
22
22
|
import { debounce } from 'lodash';
|
|
23
23
|
import { useRequest } from 'ahooks';
|
|
24
24
|
import { PlusOutlined } from '@ant-design/icons';
|
|
25
|
-
import React, {
|
|
25
|
+
import React, { useMemo, useRef, useState } from 'react';
|
|
26
26
|
import { useTranslation } from 'react-i18next';
|
|
27
27
|
import { SkeletonFallback } from '../../../components/SkeletonFallback';
|
|
28
28
|
import { AssociationFieldModel } from './AssociationFieldModel';
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
normalizeAssociationFieldNames,
|
|
33
33
|
resolveOptions,
|
|
34
34
|
toSelectValue,
|
|
35
|
+
useAssociationValueHydration,
|
|
35
36
|
type AssociationOption,
|
|
36
37
|
type LazySelectProps,
|
|
37
38
|
} from './recordSelectShared';
|
|
@@ -41,81 +42,6 @@ import { ActionWithoutPermission } from '../../base/ActionModel';
|
|
|
41
42
|
import { EditFormModel } from '../../blocks';
|
|
42
43
|
import { hasAncestorModel } from './recordSelectSettingsUtils';
|
|
43
44
|
|
|
44
|
-
function isPlainObject(val: unknown): val is Record<string, any> {
|
|
45
|
-
return !!val && typeof val === 'object' && !Array.isArray(val);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
type HydrateStatus = 'pending' | 'done';
|
|
49
|
-
|
|
50
|
-
type HydrationCandidate = {
|
|
51
|
-
item: AssociationOption;
|
|
52
|
-
tk: any;
|
|
53
|
-
tkKey: string;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export function collectAssociationHydrationCandidates(options: {
|
|
57
|
-
value: LazySelectProps['value'];
|
|
58
|
-
isMultiple: boolean;
|
|
59
|
-
valueKey: string;
|
|
60
|
-
labelKey: string;
|
|
61
|
-
statusMap: Map<string, HydrateStatus>;
|
|
62
|
-
}): HydrationCandidate[] {
|
|
63
|
-
const { value, isMultiple, valueKey, labelKey, statusMap } = options;
|
|
64
|
-
const list = isMultiple ? (Array.isArray(value) ? value : []) : value != null ? [value] : [];
|
|
65
|
-
|
|
66
|
-
const activeTkKeys = new Set<string>();
|
|
67
|
-
for (const item of list) {
|
|
68
|
-
if (!isPlainObject(item)) continue;
|
|
69
|
-
const tk = item?.[valueKey];
|
|
70
|
-
if (tk == null) continue;
|
|
71
|
-
const tkKey = typeof tk === 'object' ? JSON.stringify(tk) : String(tk);
|
|
72
|
-
if (!tkKey) continue;
|
|
73
|
-
activeTkKeys.add(tkKey);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
for (const key of Array.from(statusMap.keys())) {
|
|
77
|
-
if (!activeTkKeys.has(key)) {
|
|
78
|
-
statusMap.delete(key);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const candidates: HydrationCandidate[] = [];
|
|
83
|
-
for (const item of list) {
|
|
84
|
-
if (!isPlainObject(item)) continue;
|
|
85
|
-
const tk = item?.[valueKey];
|
|
86
|
-
if (tk == null) continue;
|
|
87
|
-
if (item?.[labelKey] != null) continue;
|
|
88
|
-
|
|
89
|
-
const tkKey = typeof tk === 'object' ? JSON.stringify(tk) : String(tk);
|
|
90
|
-
if (!tkKey) continue;
|
|
91
|
-
|
|
92
|
-
const status = statusMap.get(tkKey);
|
|
93
|
-
if (status === 'pending' || status === 'done') continue;
|
|
94
|
-
|
|
95
|
-
statusMap.set(tkKey, 'pending');
|
|
96
|
-
candidates.push({ item, tk, tkKey });
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return candidates;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function getAssociationHydrationNamePath(model: any) {
|
|
103
|
-
return model?.context?.fieldPathArray ?? model?.context?.fieldPath ?? model?.props?.name;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export function getAssociationHydrationSetterContext(model: any) {
|
|
107
|
-
if (typeof model?.context?.setFormValue === 'function') {
|
|
108
|
-
return model.context;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return model?.context?.blockModel?.context;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function markAssociationHydrationDone(statusMap: Map<string, HydrateStatus>, tkKey: string | null | undefined) {
|
|
115
|
-
if (!tkKey) return;
|
|
116
|
-
statusMap.set(tkKey, 'done');
|
|
117
|
-
}
|
|
118
|
-
|
|
119
45
|
function RemoteModelRenderer({ options }) {
|
|
120
46
|
const ctx = useFlowViewContext();
|
|
121
47
|
const { data, loading } = useRequest(
|
|
@@ -281,66 +207,13 @@ const LazySelect = (props: Readonly<LazySelectProps>) => {
|
|
|
281
207
|
};
|
|
282
208
|
const isConfigMode = !!model.context.flowSettingsEnabled;
|
|
283
209
|
const { t } = useTranslation();
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if (!valueKey || !labelKey) return;
|
|
292
|
-
|
|
293
|
-
const current = value;
|
|
294
|
-
const candidates = collectAssociationHydrationCandidates({
|
|
295
|
-
value: current,
|
|
296
|
-
isMultiple,
|
|
297
|
-
valueKey,
|
|
298
|
-
labelKey,
|
|
299
|
-
statusMap: hydrateStatusRef.current,
|
|
300
|
-
});
|
|
301
|
-
if (!candidates.length) return;
|
|
302
|
-
|
|
303
|
-
const namePath = getAssociationHydrationNamePath(model);
|
|
304
|
-
const setterCtx: any = getAssociationHydrationSetterContext(model);
|
|
305
|
-
|
|
306
|
-
candidates.forEach(({ item, tk, tkKey }) => {
|
|
307
|
-
void (async () => {
|
|
308
|
-
try {
|
|
309
|
-
const record = await resource.get(tk);
|
|
310
|
-
if (!record || typeof record !== 'object') {
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const merge = { ...(item as any), ...(record as any) };
|
|
315
|
-
if (merge?.[labelKey] == null) {
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const nextValue = isMultiple
|
|
320
|
-
? (Array.isArray(current) ? current : []).map((v: any) => {
|
|
321
|
-
if (!isPlainObject(v)) return v;
|
|
322
|
-
return v?.[valueKey] === tk ? merge : v;
|
|
323
|
-
})
|
|
324
|
-
: merge;
|
|
325
|
-
|
|
326
|
-
if (setterCtx && typeof setterCtx.setFormValue === 'function' && namePath != null) {
|
|
327
|
-
await setterCtx.setFormValue(namePath, nextValue, {
|
|
328
|
-
source: 'default',
|
|
329
|
-
markExplicit: false,
|
|
330
|
-
triggerEvent: false,
|
|
331
|
-
});
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
onChange(nextValue as any);
|
|
336
|
-
} catch (error) {
|
|
337
|
-
// ignore
|
|
338
|
-
} finally {
|
|
339
|
-
markAssociationHydrationDone(hydrateStatusRef.current, tkKey);
|
|
340
|
-
}
|
|
341
|
-
})();
|
|
342
|
-
});
|
|
343
|
-
}, [isMultiple, model, normalizedFieldNames.label, normalizedFieldNames.value, onChange, value]);
|
|
210
|
+
useAssociationValueHydration({
|
|
211
|
+
model,
|
|
212
|
+
value,
|
|
213
|
+
isMultiple,
|
|
214
|
+
fieldNames: normalizedFieldNames,
|
|
215
|
+
onChange,
|
|
216
|
+
});
|
|
344
217
|
|
|
345
218
|
const QuickAddContent = ({ searchText }) => {
|
|
346
219
|
return (
|