@nocobase/client-v2 2.1.0-beta.29 → 2.1.0-beta.32
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/BaseApplication.d.ts +1 -0
- package/es/PluginManager.d.ts +1 -0
- package/es/components/form/DrawerFormLayout.d.ts +49 -0
- package/es/components/form/EnvVariableInput.d.ts +42 -0
- package/es/components/form/FileSizeInput.d.ts +27 -0
- package/es/components/form/createFormRegistry.d.ts +33 -0
- package/es/components/form/index.d.ts +13 -0
- package/es/components/index.d.ts +1 -1
- package/es/flow/actions/index.d.ts +1 -1
- package/es/flow/actions/linkageRules.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +4 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutModel.d.ts +7 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +5 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +1 -0
- package/es/flow/internal/utils/enumOptionsUtils.d.ts +5 -0
- package/es/flow/models/actions/AssociationActionUtils.d.ts +5 -0
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/filter-manager/FilterManager.d.ts +5 -1
- package/es/flow/models/blocks/table/TableSelectModel.d.ts +8 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +3 -0
- package/es/flow/models/fields/DisplayTitleFieldModel.d.ts +1 -1
- package/es/flow/models/utils/displayValueUtils.d.ts +12 -0
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.mjs +122 -106
- package/es/utils/remotePlugins.d.ts +0 -4
- package/lib/index.js +121 -105
- package/package.json +6 -5
- package/src/BaseApplication.tsx +14 -8
- package/src/PluginManager.ts +1 -0
- package/src/__tests__/app.test.tsx +28 -1
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +67 -46
- package/src/__tests__/remotePlugins.test.ts +29 -18
- package/src/__tests__/settings-center.test.tsx +30 -0
- package/src/components/form/DrawerFormLayout.tsx +103 -0
- package/src/components/form/EnvVariableInput.tsx +126 -0
- package/src/components/form/FileSizeInput.tsx +105 -0
- package/src/components/form/createFormRegistry.ts +60 -0
- package/src/components/form/index.tsx +14 -0
- package/src/components/index.ts +1 -1
- package/src/flow/__tests__/FlowRoute.test.tsx +4 -5
- package/src/flow/actions/__tests__/actionLinkageRules.race.repro.test.ts +199 -0
- package/src/flow/actions/__tests__/dataScopeFilter.test.ts +92 -13
- package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +6 -1
- package/src/flow/actions/__tests__/linkageRules.menu.test.ts +90 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +476 -1
- package/src/flow/actions/index.ts +2 -0
- package/src/flow/actions/linkageRules.tsx +316 -280
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +2 -8
- package/src/flow/actions/setTargetDataScope.tsx +32 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +8 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +70 -12
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +26 -87
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +11 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +5 -1
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +292 -31
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +50 -12
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +77 -56
- package/src/flow/components/AdminLayout.tsx +2 -2
- package/src/flow/components/FieldAssignRulesEditor.tsx +2 -0
- package/src/flow/components/FlowRoute.tsx +17 -4
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
- package/src/flow/components/filter/LinkageFilterItem.tsx +9 -2
- package/src/flow/components/filter/VariableFilterItem.tsx +2 -6
- package/src/flow/components/filter/__tests__/LinkageFilterItem.test.tsx +71 -0
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +48 -0
- package/src/flow/internal/utils/__tests__/enumOptionsUtils.test.ts +10 -1
- package/src/flow/internal/utils/enumOptionsUtils.ts +29 -0
- package/src/flow/models/actions/AssociateActionModel.tsx +2 -2
- package/src/flow/models/actions/AssociationActionUtils.ts +14 -0
- package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +63 -0
- package/src/flow/models/base/CollectionBlockModel.tsx +7 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +33 -9
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +53 -13
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +63 -3
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +33 -1
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +66 -2
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +270 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +8 -5
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +30 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +6 -1
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +6 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +11 -6
- package/src/flow/models/blocks/table/TableColumnModel.tsx +3 -0
- package/src/flow/models/blocks/table/TableSelectModel.tsx +36 -26
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.rowClick.test.ts +69 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +96 -1
- package/src/flow/models/blocks/table/__tests__/TableSelectModel.test.ts +41 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +7 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +4 -1
- package/src/flow/models/fields/ClickableFieldModel.tsx +9 -4
- package/src/flow/models/fields/DisplayTitleFieldModel.tsx +12 -4
- package/src/flow/models/fields/SelectFieldModel.tsx +31 -1
- package/src/flow/models/fields/__tests__/ClickableFieldModel.test.ts +23 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +2 -1
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +7 -0
- package/src/flow/models/utils/displayValueUtils.ts +57 -0
- package/src/flow/system-settings/useSystemSettings.tsx +36 -1
- package/src/utils/globalDeps.ts +2 -0
- package/src/utils/remotePlugins.ts +7 -27
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { FlowEngine } from '@nocobase/flow-engine';
|
|
11
|
-
import { describe, expect, it } from 'vitest';
|
|
11
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
12
12
|
import { TableColumnModel } from '../TableColumnModel';
|
|
13
13
|
|
|
14
14
|
describe('TableColumnModel sorter settings', () => {
|
|
@@ -69,4 +69,99 @@ describe('TableColumnModel sorter settings', () => {
|
|
|
69
69
|
|
|
70
70
|
expect(hidden).toBe(true);
|
|
71
71
|
});
|
|
72
|
+
|
|
73
|
+
it('updates field component setting when association title field changes', async () => {
|
|
74
|
+
const engine = new FlowEngine();
|
|
75
|
+
const model = new TableColumnModel({ uid: 'table-column-title-field-component', flowEngine: engine } as any);
|
|
76
|
+
const titleFieldStep = model.getFlow('tableColumnSettings')?.steps?.fieldNames as any;
|
|
77
|
+
const setStepParams = vi.fn();
|
|
78
|
+
const setProps = vi.fn();
|
|
79
|
+
const dispatchEvent = vi.fn();
|
|
80
|
+
const targetCollectionField = {
|
|
81
|
+
getComponentProps: () => ({}),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
await titleFieldStep.beforeParamsSave(
|
|
85
|
+
{
|
|
86
|
+
collectionField: {
|
|
87
|
+
isAssociationField: () => true,
|
|
88
|
+
targetCollection: {
|
|
89
|
+
name: 'departments',
|
|
90
|
+
getField: () => targetCollectionField,
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
model: {
|
|
94
|
+
collectionField: {
|
|
95
|
+
dataSourceKey: 'main',
|
|
96
|
+
},
|
|
97
|
+
constructor: {
|
|
98
|
+
getDefaultBindingByField: () => ({
|
|
99
|
+
modelName: 'DisplayTextFieldModel',
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
subModels: {
|
|
103
|
+
field: {
|
|
104
|
+
use: 'DisplayTextFieldModel',
|
|
105
|
+
setStepParams,
|
|
106
|
+
dispatchEvent,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
setStepParams,
|
|
110
|
+
setProps,
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{ label: 'code' },
|
|
114
|
+
{ label: 'name' },
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
expect(setStepParams).toHaveBeenCalledWith('tableColumnSettings', 'model', { use: 'DisplayTextFieldModel' });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('does not update field component setting when title field refresh fails', async () => {
|
|
121
|
+
const engine = new FlowEngine();
|
|
122
|
+
const model = new TableColumnModel({ uid: 'table-column-title-field-component-failed', flowEngine: engine } as any);
|
|
123
|
+
const titleFieldStep = model.getFlow('tableColumnSettings')?.steps?.fieldNames as any;
|
|
124
|
+
const setStepParams = vi.fn();
|
|
125
|
+
const setProps = vi.fn();
|
|
126
|
+
const targetCollectionField = {
|
|
127
|
+
getComponentProps: () => ({}),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
await expect(
|
|
131
|
+
titleFieldStep.beforeParamsSave(
|
|
132
|
+
{
|
|
133
|
+
collectionField: {
|
|
134
|
+
isAssociationField: () => true,
|
|
135
|
+
targetCollection: {
|
|
136
|
+
name: 'departments',
|
|
137
|
+
getField: () => targetCollectionField,
|
|
138
|
+
},
|
|
139
|
+
},
|
|
140
|
+
model: {
|
|
141
|
+
collectionField: {
|
|
142
|
+
dataSourceKey: 'main',
|
|
143
|
+
},
|
|
144
|
+
constructor: {
|
|
145
|
+
getDefaultBindingByField: () => ({
|
|
146
|
+
modelName: 'DisplayTextFieldModel',
|
|
147
|
+
}),
|
|
148
|
+
},
|
|
149
|
+
subModels: {
|
|
150
|
+
field: {
|
|
151
|
+
use: 'DisplayTextFieldModel',
|
|
152
|
+
setStepParams: vi.fn(),
|
|
153
|
+
dispatchEvent: vi.fn().mockRejectedValue(new Error('beforeRender failed')),
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
setStepParams,
|
|
157
|
+
setProps,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{ label: 'code' },
|
|
161
|
+
{ label: 'name' },
|
|
162
|
+
),
|
|
163
|
+
).rejects.toThrow('beforeRender failed');
|
|
164
|
+
|
|
165
|
+
expect(setStepParams).not.toHaveBeenCalledWith('tableColumnSettings', 'model', expect.anything());
|
|
166
|
+
});
|
|
72
167
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
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 } from 'vitest';
|
|
11
|
+
import '@nocobase/client';
|
|
12
|
+
import { getAssociationSelectAssociatedRecordsFilter, getAssociationSelectForeignKeyFilter } from '../TableSelectModel';
|
|
13
|
+
|
|
14
|
+
describe('TableSelectModel', () => {
|
|
15
|
+
it('filters out already associated o2m records in association select table', () => {
|
|
16
|
+
expect(
|
|
17
|
+
getAssociationSelectForeignKeyFilter({
|
|
18
|
+
interface: 'o2m',
|
|
19
|
+
name: 'orders',
|
|
20
|
+
foreignKey: 'f_y2quq75zibi',
|
|
21
|
+
}),
|
|
22
|
+
).toEqual({
|
|
23
|
+
f_y2quq75zibi: {
|
|
24
|
+
$is: null,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('filters out already associated m2m records in association select table', () => {
|
|
30
|
+
expect(
|
|
31
|
+
getAssociationSelectAssociatedRecordsFilter(
|
|
32
|
+
{
|
|
33
|
+
filterTargetKey: 'id',
|
|
34
|
+
},
|
|
35
|
+
[{ id: 11 }, { id: 12 }],
|
|
36
|
+
),
|
|
37
|
+
).toEqual({
|
|
38
|
+
'id.$ne': [11, 12],
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -30,6 +30,7 @@ import { useTranslation } from 'react-i18next';
|
|
|
30
30
|
import { buildRecordPickerPopupContextInputArgs, RecordPickerContent } from '../RecordPickerFieldModel';
|
|
31
31
|
import { AssociationFieldModel } from '../AssociationFieldModel';
|
|
32
32
|
import { adjustColumnOrder } from '../../../blocks/table/utils';
|
|
33
|
+
import { isSubTableColumnFieldComponentContext } from '../SubTableFieldModel/SubTableColumnModel';
|
|
33
34
|
import { EditFormContent } from './actions/PopupSubTableEditActionModel';
|
|
34
35
|
import { QuickEditFormModel } from '../../../blocks/form/QuickEditFormModel';
|
|
35
36
|
import { ActionWithoutPermission } from '../../../base/ActionModel';
|
|
@@ -901,6 +902,9 @@ PopupSubTableFieldModel.define({
|
|
|
901
902
|
EditableItemModel.bindModelToInterface('PopupSubTableFieldModel', ['m2m', 'o2m', 'mbm'], {
|
|
902
903
|
order: 300,
|
|
903
904
|
when: (ctx, field) => {
|
|
905
|
+
if (isSubTableColumnFieldComponentContext(ctx)) {
|
|
906
|
+
return false;
|
|
907
|
+
}
|
|
904
908
|
if (field.targetCollection) {
|
|
905
909
|
return field.targetCollection.template !== 'file';
|
|
906
910
|
}
|
package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx
CHANGED
|
@@ -36,6 +36,12 @@ import { FieldDeletePlaceholder, CustomWidth } from '../../../blocks/table/Table
|
|
|
36
36
|
import { buildDynamicNamePath } from '../../../blocks/form/dynamicNamePath';
|
|
37
37
|
import { getSubTableRowIdentity } from './rowIdentity';
|
|
38
38
|
|
|
39
|
+
export const SUB_TABLE_COLUMN_FIELD_COMPONENT_CONTEXT = 'subTableColumn';
|
|
40
|
+
|
|
41
|
+
export function isSubTableColumnFieldComponentContext(ctx) {
|
|
42
|
+
return (ctx?.model?.constructor as any)?.fieldComponentContext === SUB_TABLE_COLUMN_FIELD_COMPONENT_CONTEXT;
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
const SubTableRowRuleBinder: React.FC<{ model: any }> = ({ model }) => {
|
|
40
46
|
React.useEffect(() => {
|
|
41
47
|
const emitter = model?.flowEngine?.emitter;
|
|
@@ -401,6 +407,7 @@ export class SubTableColumnModel<
|
|
|
401
407
|
T extends SubTableColumnModelStructure = SubTableColumnModelStructure,
|
|
402
408
|
> extends EditableItemModel<T> {
|
|
403
409
|
static renderMode = ModelRenderMode.RenderFunction;
|
|
410
|
+
static fieldComponentContext = SUB_TABLE_COLUMN_FIELD_COMPONENT_CONTEXT;
|
|
404
411
|
|
|
405
412
|
renderHiddenInConfig() {
|
|
406
413
|
return <FieldWithoutPermissionPlaceholder targetModel={this} />;
|
|
@@ -21,7 +21,7 @@ import { uid } from '@formily/shared';
|
|
|
21
21
|
import { FormItemModel } from '../../../blocks/form';
|
|
22
22
|
import { AssociationFieldModel } from '../AssociationFieldModel';
|
|
23
23
|
import { buildRecordPickerPopupContextInputArgs, RecordPickerContent } from '../RecordPickerFieldModel';
|
|
24
|
-
import { SubTableColumnModel } from './SubTableColumnModel';
|
|
24
|
+
import { isSubTableColumnFieldComponentContext, SubTableColumnModel } from './SubTableColumnModel';
|
|
25
25
|
import { SubTableField } from './SubTableField';
|
|
26
26
|
import { adjustColumnOrder } from '../../../blocks/table/utils';
|
|
27
27
|
|
|
@@ -389,6 +389,9 @@ export { SubTableColumnModel };
|
|
|
389
389
|
FormItemModel.bindModelToInterface('SubTableFieldModel', ['m2m', 'o2m', 'mbm'], {
|
|
390
390
|
order: 200,
|
|
391
391
|
when: (ctx, field) => {
|
|
392
|
+
if (isSubTableColumnFieldComponentContext(ctx)) {
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
392
395
|
if (field.targetCollection) {
|
|
393
396
|
return field.targetCollection.template !== 'file';
|
|
394
397
|
}
|
|
@@ -14,6 +14,7 @@ import React from 'react';
|
|
|
14
14
|
import { EllipsisWithTooltip } from '../../components/EllipsisWithTooltip';
|
|
15
15
|
import { openViewFlow } from '../../flows/openViewFlow';
|
|
16
16
|
import { FieldModel } from '../base/FieldModel';
|
|
17
|
+
import { hasDisplayValue, normalizeDisplayValue } from '../utils/displayValueUtils';
|
|
17
18
|
|
|
18
19
|
export function transformNestedData(inputData) {
|
|
19
20
|
const resultArray = [];
|
|
@@ -150,11 +151,15 @@ export class ClickableFieldModel extends FieldModel {
|
|
|
150
151
|
|
|
151
152
|
renderInDisplayStyle(value, record?, isToMany?, wrap?) {
|
|
152
153
|
const { clickToOpen = false, displayStyle, titleField, overflowMode, disabled, ...restProps } = this.props;
|
|
153
|
-
|
|
154
|
+
const titleCollectionField = titleField
|
|
155
|
+
? this.context.collectionField?.targetCollection?.getField?.(titleField) || this.context.collectionField
|
|
156
|
+
: this.context.collectionField;
|
|
157
|
+
const displayValue = normalizeDisplayValue(value, { collectionField: titleCollectionField });
|
|
158
|
+
if (!hasDisplayValue(displayValue) && value && typeof value === 'object' && restProps.target) {
|
|
154
159
|
return;
|
|
155
160
|
}
|
|
156
|
-
const result = this.renderComponent(
|
|
157
|
-
const display = record ? (
|
|
161
|
+
const result = this.renderComponent(displayValue, wrap);
|
|
162
|
+
const display = record ? (hasDisplayValue(displayValue) ? result : 'N/A') : result;
|
|
158
163
|
const isTag = displayStyle === 'tag';
|
|
159
164
|
const handleClick = (e) => {
|
|
160
165
|
clickToOpen && this.onClick(e, record);
|
|
@@ -170,7 +175,7 @@ export class ClickableFieldModel extends FieldModel {
|
|
|
170
175
|
|
|
171
176
|
if (isTag) {
|
|
172
177
|
return (
|
|
173
|
-
|
|
178
|
+
hasDisplayValue(displayValue) && (
|
|
174
179
|
<Tag {...restProps} style={commonStyle} onClick={handleClick} className={restProps.className}>
|
|
175
180
|
{display}
|
|
176
181
|
</Tag>
|
|
@@ -12,7 +12,8 @@ import { Typography } from 'antd';
|
|
|
12
12
|
import { castArray } from 'lodash';
|
|
13
13
|
import { css } from '@emotion/css';
|
|
14
14
|
import React from 'react';
|
|
15
|
-
import { FieldModel } from '../base
|
|
15
|
+
import { FieldModel } from '../base';
|
|
16
|
+
import { hasDisplayValue, normalizeDisplayValue } from '../utils/displayValueUtils';
|
|
16
17
|
|
|
17
18
|
export class DisplayTitleFieldModel extends FieldModel {
|
|
18
19
|
get collectionField(): CollectionField {
|
|
@@ -55,13 +56,20 @@ export class DisplayTitleFieldModel extends FieldModel {
|
|
|
55
56
|
};
|
|
56
57
|
if (titleField) {
|
|
57
58
|
const result = castArray(value).flatMap((v, idx) => {
|
|
58
|
-
const
|
|
59
|
-
|
|
59
|
+
const titleCollectionField =
|
|
60
|
+
this.context.collectionField?.targetCollection?.getField?.(titleField) || this.context.collectionField;
|
|
61
|
+
const displayValue = normalizeDisplayValue(v?.[titleField], { collectionField: titleCollectionField });
|
|
62
|
+
const result = this.renderComponent(displayValue);
|
|
63
|
+
const node = hasDisplayValue(displayValue) ? result : 'N/A';
|
|
60
64
|
return idx === 0 ? [node] : [<span key={`sep-${idx}`}>, </span>, node];
|
|
61
65
|
});
|
|
62
66
|
return <Typography.Text {...typographyProps}>{result}</Typography.Text>;
|
|
63
67
|
} else {
|
|
64
|
-
const textContent =
|
|
68
|
+
const textContent = (
|
|
69
|
+
<Typography.Text {...typographyProps}>
|
|
70
|
+
{this.renderComponent(normalizeDisplayValue(value, { collectionField: this.context.collectionField }))}
|
|
71
|
+
</Typography.Text>
|
|
72
|
+
);
|
|
65
73
|
return textContent;
|
|
66
74
|
}
|
|
67
75
|
}
|
|
@@ -12,25 +12,55 @@ import { Select, Tag, Tooltip } from 'antd';
|
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { FieldModel } from '../base/FieldModel';
|
|
14
14
|
import { MobileSelect } from './mobile-components/MobileSelect';
|
|
15
|
+
import { enumToOptions, getSelectedEnumLabels } from '../../internal/utils/enumOptionsUtils';
|
|
16
|
+
|
|
17
|
+
const getOriginalEnumOptions = (model: SelectFieldModel) => {
|
|
18
|
+
const fromEnum = enumToOptions(model.context.collectionField?.uiSchema?.enum, (text) => text) || [];
|
|
19
|
+
if (fromEnum.length > 0) {
|
|
20
|
+
return fromEnum.map((option) => ({ ...option }));
|
|
21
|
+
}
|
|
22
|
+
const current = Array.isArray(model.props.options) ? model.props.options : [];
|
|
23
|
+
return current.map((option) => ({ ...option }));
|
|
24
|
+
};
|
|
15
25
|
|
|
16
26
|
export class SelectFieldModel extends FieldModel {
|
|
17
27
|
render() {
|
|
28
|
+
const fallbackOptions = getOriginalEnumOptions(this);
|
|
18
29
|
const options = this.props.options?.map((v) => {
|
|
19
30
|
return {
|
|
20
31
|
...v,
|
|
21
32
|
label: this.translate(v.label),
|
|
22
33
|
};
|
|
23
34
|
});
|
|
35
|
+
const selectedLabels = getSelectedEnumLabels(this.props.value, fallbackOptions).map((item) => ({
|
|
36
|
+
...item,
|
|
37
|
+
label: this.translate(item.label),
|
|
38
|
+
}));
|
|
39
|
+
const value = Array.isArray(this.props.value)
|
|
40
|
+
? selectedLabels
|
|
41
|
+
: selectedLabels.length > 0
|
|
42
|
+
? selectedLabels[0]
|
|
43
|
+
: this.props.value;
|
|
24
44
|
|
|
25
45
|
// TODO: 移动端相关的代码需迁移到单独的插件中
|
|
26
46
|
if (this.context.isMobileLayout) {
|
|
27
|
-
return <MobileSelect {...this.props} options={options} />;
|
|
47
|
+
return <MobileSelect {...this.props} options={options} displayValue={value} />;
|
|
28
48
|
}
|
|
29
49
|
|
|
30
50
|
return (
|
|
31
51
|
<Select
|
|
32
52
|
{...this.props}
|
|
53
|
+
value={value}
|
|
54
|
+
labelInValue
|
|
55
|
+
onChange={(nextValue) => {
|
|
56
|
+
if (Array.isArray(nextValue)) {
|
|
57
|
+
this.props.onChange?.(nextValue.map((item: any) => item?.value));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.props.onChange?.(nextValue?.value);
|
|
61
|
+
}}
|
|
33
62
|
options={options}
|
|
63
|
+
labelRender={(item) => item.label}
|
|
34
64
|
maxTagCount="responsive"
|
|
35
65
|
maxTagPlaceholder={(omittedValues) => (
|
|
36
66
|
<Tooltip
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
import { describe, expect, it, vi } from 'vitest';
|
|
11
11
|
import { FlowEngine } from '@nocobase/flow-engine';
|
|
12
|
+
import { render, screen } from '@testing-library/react';
|
|
12
13
|
import { ClickableFieldModel } from '../ClickableFieldModel';
|
|
14
|
+
import { DisplayTextFieldModel } from '../DisplayTextFieldModel';
|
|
13
15
|
|
|
14
16
|
function createRolesFieldModel(sourceRecord: Record<string, any>) {
|
|
15
17
|
const engine = new FlowEngine();
|
|
@@ -84,4 +86,25 @@ describe('ClickableFieldModel', () => {
|
|
|
84
86
|
{ debounce: true },
|
|
85
87
|
);
|
|
86
88
|
});
|
|
89
|
+
|
|
90
|
+
it('renders object title field values by configured target title field', () => {
|
|
91
|
+
const engine = new FlowEngine();
|
|
92
|
+
engine.registerModels({ DisplayTextFieldModel });
|
|
93
|
+
|
|
94
|
+
const model = engine.createModel<DisplayTextFieldModel>({
|
|
95
|
+
use: DisplayTextFieldModel,
|
|
96
|
+
uid: 'display-text-association-title',
|
|
97
|
+
});
|
|
98
|
+
model.context.defineProperty('collectionField', {
|
|
99
|
+
value: {
|
|
100
|
+
targetCollectionTitleFieldName: 'code',
|
|
101
|
+
isAssociationField: () => true,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
render(model.renderInDisplayStyle({ id: 2, name: 'Sales', code: 'S-001' }, { id: 1 }, false));
|
|
106
|
+
|
|
107
|
+
expect(screen.getByText('S-001')).toBeInTheDocument();
|
|
108
|
+
expect(screen.queryByText('Sales')).not.toBeInTheDocument();
|
|
109
|
+
});
|
|
87
110
|
});
|
|
@@ -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, onChangeComplete, disabled, options = [], mode } = props;
|
|
16
|
+
const { value, displayValue, onChange, onChangeComplete, disabled, options = [], mode } = props;
|
|
17
17
|
const ctx = useFlowModelContext();
|
|
18
18
|
const t = ctx.t;
|
|
19
19
|
const [visible, setVisible] = useState(false);
|
|
@@ -44,6 +44,7 @@ export function MobileSelect(props) {
|
|
|
44
44
|
<div onClick={() => !disabled && setVisible(true)}>
|
|
45
45
|
<Select
|
|
46
46
|
{...props}
|
|
47
|
+
value={displayValue ?? value}
|
|
47
48
|
open={false}
|
|
48
49
|
dropdownStyle={{ display: 'none' }}
|
|
49
50
|
showSearch={false}
|
|
@@ -178,6 +178,13 @@ describe('MobileSelect', () => {
|
|
|
178
178
|
clickTrigger();
|
|
179
179
|
expect(screen.queryByTestId('popup')).not.toBeInTheDocument();
|
|
180
180
|
});
|
|
181
|
+
|
|
182
|
+
it('prefers displayValue for trigger rendering', () => {
|
|
183
|
+
const displayValue = [{ label: 'Published', value: 'published' }];
|
|
184
|
+
renderMobileSelect({ value: ['published'], displayValue, mode: 'multiple' });
|
|
185
|
+
|
|
186
|
+
expect(mockState.selectProps?.value).toEqual(displayValue);
|
|
187
|
+
});
|
|
181
188
|
});
|
|
182
189
|
|
|
183
190
|
function SubTableCellHarness({ value, onCommit, mode }: { value: any; onCommit: (value: any) => void; mode?: string }) {
|
|
@@ -0,0 +1,57 @@
|
|
|
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 { isValidElement } from 'react';
|
|
11
|
+
|
|
12
|
+
export function hasDisplayValue(value: any) {
|
|
13
|
+
return value !== undefined && value !== null && value !== '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getTitleFieldName(collectionField: any) {
|
|
17
|
+
const targetCollection = collectionField?.targetCollection;
|
|
18
|
+
return (
|
|
19
|
+
collectionField?.targetCollectionTitleFieldName ||
|
|
20
|
+
targetCollection?.titleCollectionField?.name ||
|
|
21
|
+
targetCollection?.titleField ||
|
|
22
|
+
targetCollection?.options?.titleField
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getTitleCollectionField(collectionField: any, titleFieldName: string) {
|
|
27
|
+
return (
|
|
28
|
+
collectionField?.targetCollection?.getField?.(titleFieldName) ||
|
|
29
|
+
collectionField?.targetCollection?.titleCollectionField
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function normalizeDisplayValue(value: any, options: { collectionField?: any } = {}): any {
|
|
34
|
+
if (!hasDisplayValue(value) || isValidElement(value)) {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
if (['string', 'number', 'boolean'].includes(typeof value)) {
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
const parts = value.map((item) => normalizeDisplayValue(item, options)).filter(hasDisplayValue);
|
|
42
|
+
return parts.length ? parts.map(String).join(', ') : undefined;
|
|
43
|
+
}
|
|
44
|
+
if (value instanceof Date) {
|
|
45
|
+
return value.toISOString();
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === 'object') {
|
|
48
|
+
const titleFieldName = getTitleFieldName(options.collectionField);
|
|
49
|
+
if (titleFieldName) {
|
|
50
|
+
return normalizeDisplayValue(value[titleFieldName], {
|
|
51
|
+
collectionField: getTitleCollectionField(options.collectionField, titleFieldName),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { useFlowEngine } from '@nocobase/flow-engine';
|
|
10
11
|
import { useApp } from '../../flow-compat';
|
|
11
|
-
import
|
|
12
|
+
import languageCodes from '../../locale/languageCodes';
|
|
13
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* 读取系统设置并兼容旧 hook 的返回结构。
|
|
@@ -25,7 +27,19 @@ import { useEffect, useState } from 'react';
|
|
|
25
27
|
export const useSystemSettings = () => {
|
|
26
28
|
const app = useApp();
|
|
27
29
|
const source = app.systemSettings;
|
|
30
|
+
const flowEngine = useFlowEngine({ throwError: false });
|
|
28
31
|
const [, forceUpdate] = useState(0);
|
|
32
|
+
const enabledLanguages = source.data?.data?.enabledLanguages;
|
|
33
|
+
const languageOptions = useMemo(
|
|
34
|
+
() =>
|
|
35
|
+
(Array.isArray(enabledLanguages) ? enabledLanguages : [])
|
|
36
|
+
.filter((code) => languageCodes[code])
|
|
37
|
+
.map((code) => ({
|
|
38
|
+
label: languageCodes[code].label,
|
|
39
|
+
value: code,
|
|
40
|
+
})),
|
|
41
|
+
[enabledLanguages],
|
|
42
|
+
);
|
|
29
43
|
|
|
30
44
|
useEffect(() => {
|
|
31
45
|
void source.load();
|
|
@@ -34,6 +48,27 @@ export const useSystemSettings = () => {
|
|
|
34
48
|
});
|
|
35
49
|
}, [source]);
|
|
36
50
|
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!flowEngine) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
flowEngine.context.defineProperty('locale', {
|
|
57
|
+
get: (ctx) => ctx.api?.auth?.locale || ctx.i18n?.language,
|
|
58
|
+
cache: false,
|
|
59
|
+
meta: {
|
|
60
|
+
type: 'string',
|
|
61
|
+
title: '{{t("Current language")}}',
|
|
62
|
+
sort: 970,
|
|
63
|
+
interface: 'select',
|
|
64
|
+
uiSchema: {
|
|
65
|
+
enum: languageOptions,
|
|
66
|
+
'x-component': 'Select',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}, [flowEngine, languageOptions]);
|
|
71
|
+
|
|
37
72
|
return {
|
|
38
73
|
loading: source.loading,
|
|
39
74
|
data: source.data,
|
package/src/utils/globalDeps.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { dayjs } from '@nocobase/utils/client';
|
|
|
19
19
|
import * as nocobaseFlowEngine from '@nocobase/flow-engine';
|
|
20
20
|
import * as ahooks from 'ahooks';
|
|
21
21
|
import * as antd from 'antd';
|
|
22
|
+
import axios from 'axios';
|
|
22
23
|
import * as i18next from 'i18next';
|
|
23
24
|
import lodash from 'lodash';
|
|
24
25
|
import React from 'react';
|
|
@@ -68,6 +69,7 @@ export function defineGlobalDeps(requirejs: RequireJS) {
|
|
|
68
69
|
|
|
69
70
|
// utils
|
|
70
71
|
requirejs.define('ahooks', () => ahooks);
|
|
72
|
+
requirejs.define('axios', () => axios);
|
|
71
73
|
requirejs.define('dayjs', () => dayjs);
|
|
72
74
|
requirejs.define('lodash', () => lodash);
|
|
73
75
|
requirejs.define('@emotion/css', () => emotionCss);
|
|
@@ -12,33 +12,16 @@ import type { PluginClass } from '../PluginManager';
|
|
|
12
12
|
import type { PluginData } from '../PluginManager';
|
|
13
13
|
import type { RequireJS } from './requirejs';
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*/
|
|
18
|
-
export function defineDevPlugins(plugins: Record<string, PluginClass>) {
|
|
19
|
-
Object.entries(plugins).forEach(([packageName, plugin]) => {
|
|
20
|
-
window.define(`${packageName}/client-v2`, () => plugin);
|
|
21
|
-
});
|
|
15
|
+
function getClientV2ModuleId(packageName: string) {
|
|
16
|
+
return `${packageName}/client-v2`;
|
|
22
17
|
}
|
|
23
18
|
|
|
24
19
|
/**
|
|
25
20
|
* @internal
|
|
26
21
|
*/
|
|
27
|
-
export function
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
value: true,
|
|
31
|
-
});
|
|
32
|
-
Object.keys(_pluginExports).forEach(function (key) {
|
|
33
|
-
if (key === '__esModule') return;
|
|
34
|
-
if (key in _exports && _exports[key] === _pluginExports[key]) return;
|
|
35
|
-
Object.defineProperty(_exports, key, {
|
|
36
|
-
enumerable: true,
|
|
37
|
-
get: function () {
|
|
38
|
-
return _pluginExports[key];
|
|
39
|
-
},
|
|
40
|
-
});
|
|
41
|
-
});
|
|
22
|
+
export function defineDevPlugins(plugins: Record<string, PluginClass>) {
|
|
23
|
+
Object.entries(plugins).forEach(([packageName, plugin]) => {
|
|
24
|
+
window.define(getClientV2ModuleId(packageName), () => plugin);
|
|
42
25
|
});
|
|
43
26
|
}
|
|
44
27
|
|
|
@@ -49,7 +32,7 @@ export function configRequirejs(requirejs: any, pluginData: PluginData[]) {
|
|
|
49
32
|
requirejs.requirejs.config({
|
|
50
33
|
waitSeconds: 120,
|
|
51
34
|
paths: pluginData.reduce<Record<string, string>>((acc, cur) => {
|
|
52
|
-
acc[cur.packageName] = cur.url;
|
|
35
|
+
acc[getClientV2ModuleId(cur.packageName)] = cur.url;
|
|
53
36
|
return acc;
|
|
54
37
|
}, {}),
|
|
55
38
|
});
|
|
@@ -85,10 +68,7 @@ export function processRemotePlugins(pluginData: PluginData[], resolve: (plugins
|
|
|
85
68
|
export function getRemotePlugins(requirejs: any, pluginData: PluginData[] = []): Promise<Array<[string, PluginClass]>> {
|
|
86
69
|
configRequirejs(requirejs, pluginData);
|
|
87
70
|
|
|
88
|
-
const packageNames = pluginData.map((item) => item.packageName);
|
|
89
|
-
packageNames.forEach((packageName) => {
|
|
90
|
-
definePluginClient(packageName);
|
|
91
|
-
});
|
|
71
|
+
const packageNames = pluginData.map((item) => getClientV2ModuleId(item.packageName));
|
|
92
72
|
|
|
93
73
|
return new Promise((resolve, reject) => {
|
|
94
74
|
requirejs.requirejs(packageNames, processRemotePlugins(pluginData, resolve), reject);
|