@nocobase/client-v2 2.1.0-alpha.34 → 2.1.0-alpha.35
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/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/shared/legacyDefaultValueMigrationBase.d.ts +1 -0
- package/es/flow/models/blocks/table/TableSelectModel.d.ts +8 -0
- package/es/flow/models/fields/DisplayEnumFieldModel.d.ts +6 -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 +117 -106
- package/es/utils/remotePlugins.d.ts +0 -4
- package/lib/index.js +122 -111
- package/package.json +8 -5
- package/src/BaseApplication.tsx +14 -8
- package/src/PluginManager.ts +1 -0
- package/src/__tests__/app.test.tsx +28 -1
- package/src/__tests__/remotePlugins.test.ts +29 -18
- 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/actions/__tests__/dataScopeFilter.test.ts +92 -13
- package/src/flow/actions/__tests__/formAssignRules.legacyMigration.test.tsx +173 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +476 -1
- package/src/flow/actions/filterFormDefaultValues.tsx +30 -9
- package/src/flow/actions/formAssignRules.tsx +24 -9
- package/src/flow/actions/linkageRules.tsx +240 -258
- package/src/flow/actions/setTargetDataScope.tsx +32 -3
- package/src/flow/actions/titleField.tsx +1 -1
- package/src/flow/actions/validation.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +0 -4
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +13 -5
- package/src/flow/components/FieldAssignRulesEditor.tsx +2 -0
- 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/base/PageModel/RootPageModel.tsx +1 -1
- 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-form/__tests__/legacyDefaultValueMigration.test.ts +3 -0
- 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/FormItemModel.tsx +1 -1
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +30 -0
- package/src/flow/models/blocks/form/__tests__/legacyDefaultValueMigration.test.ts +3 -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/shared/legacyDefaultValueMigrationBase.ts +21 -5
- package/src/flow/models/blocks/table/TableBlockModel.tsx +11 -6
- package/src/flow/models/blocks/table/TableColumnModel.tsx +8 -2
- 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 +132 -1
- package/src/flow/models/blocks/table/__tests__/TableSelectModel.test.ts +41 -0
- package/src/flow/models/fields/ClickableFieldModel.tsx +9 -4
- package/src/flow/models/fields/DisplayEnumFieldModel.tsx +44 -0
- 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/__tests__/DisplayEnumFieldModel.test.tsx +39 -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/utils/globalDeps.ts +8 -0
- package/src/utils/remotePlugins.ts +7 -27
|
@@ -14,6 +14,11 @@ import { FlowEngine, FlowModel, SingleRecordResource } from '@nocobase/flow-engi
|
|
|
14
14
|
// 直接从 models 聚合导入,避免局部文件相互引用顺序导致的循环依赖
|
|
15
15
|
import { FormBlockContent, FormBlockModel, FormComponent } from '../../../..';
|
|
16
16
|
import { Form } from 'antd';
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.useRealTimers();
|
|
20
|
+
});
|
|
21
|
+
|
|
17
22
|
// -----------------------------
|
|
18
23
|
// Helpers
|
|
19
24
|
// -----------------------------
|
|
@@ -319,6 +324,31 @@ describe('FormBlockModel (form/formValues injection & server resolve anchors)',
|
|
|
319
324
|
expect(flows.has('eventSettings')).toBe(true);
|
|
320
325
|
});
|
|
321
326
|
|
|
327
|
+
it('replays linkage rules after the settings rerender tick', async () => {
|
|
328
|
+
vi.useFakeTimers();
|
|
329
|
+
const engine = new FlowEngine();
|
|
330
|
+
const TestFormModel = await createTestFormModelSubclass();
|
|
331
|
+
const model = new TestFormModel({ uid: 'form-linkage-save', flowEngine: engine } as any);
|
|
332
|
+
const applyFlow = vi.spyOn(model, 'applyFlow').mockResolvedValue(undefined as any);
|
|
333
|
+
const flow = model.getFlow('eventSettings') as any;
|
|
334
|
+
const afterParamsSave = flow?.steps?.linkageRules?.afterParamsSave;
|
|
335
|
+
const ctx: any = {
|
|
336
|
+
model,
|
|
337
|
+
form: {
|
|
338
|
+
getFieldsValue: vi.fn(() => ({ status: 'draft' })),
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
afterParamsSave(ctx);
|
|
343
|
+
|
|
344
|
+
expect(applyFlow).not.toHaveBeenCalled();
|
|
345
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
346
|
+
expect(applyFlow).toHaveBeenCalledWith('eventSettings', {
|
|
347
|
+
changedValues: {},
|
|
348
|
+
allValues: { status: 'draft' },
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
322
352
|
it('delegates layout/assignRules/linkageRules stepParams to grid model', async () => {
|
|
323
353
|
const model = await setupFormModel();
|
|
324
354
|
const engine = model.flowEngine as FlowEngine;
|
|
@@ -18,6 +18,7 @@ function createMockFieldModel(options: { uid: string; props?: Record<string, any
|
|
|
18
18
|
const model: any = {
|
|
19
19
|
uid: options.uid,
|
|
20
20
|
props: { ...(options.props || {}) },
|
|
21
|
+
_options: { props: { ...(options.props || {}) } },
|
|
21
22
|
stepParams: { ...(options.stepParams || {}) },
|
|
22
23
|
emitter: { emit: vi.fn() },
|
|
23
24
|
setProps(patch: any) {
|
|
@@ -124,7 +125,9 @@ describe('legacyDefaultValueMigration', () => {
|
|
|
124
125
|
|
|
125
126
|
// field1: props.initialValue removed, stepParams cleared for initialValue
|
|
126
127
|
expect(field1.props.initialValue).toBeUndefined();
|
|
128
|
+
expect(field1._options.props.initialValue).toBeUndefined();
|
|
127
129
|
expect(field1.props.keep).toBe(true);
|
|
130
|
+
expect(field1._options.props.keep).toBe(true);
|
|
128
131
|
expect(field1.stepParams.editItemSettings?.initialValue).toBeUndefined();
|
|
129
132
|
expect(field1.stepParams.otherFlow?.s?.x).toBe(1);
|
|
130
133
|
// field2: legacy flow cleared
|
|
@@ -1508,16 +1508,21 @@ export class RuleEngine {
|
|
|
1508
1508
|
private getRowTargetKey(baseCtx: any, rowPath: NamePath): string | string[] {
|
|
1509
1509
|
let collection = this.getRootCollection() || this.getCollectionFromContext(baseCtx);
|
|
1510
1510
|
let field: any;
|
|
1511
|
+
let lastAssociationField: any;
|
|
1511
1512
|
for (const seg of rowPath) {
|
|
1512
1513
|
if (typeof seg === 'number') continue;
|
|
1513
1514
|
if (typeof seg !== 'string' || !seg || !collection?.getField) break;
|
|
1514
1515
|
|
|
1515
1516
|
field = collection?.getField?.(seg);
|
|
1516
1517
|
if (!field?.isAssociationField?.()) break;
|
|
1518
|
+
lastAssociationField = field;
|
|
1517
1519
|
collection = field?.targetCollection;
|
|
1518
1520
|
}
|
|
1519
1521
|
|
|
1520
|
-
const raw =
|
|
1522
|
+
const raw =
|
|
1523
|
+
lastAssociationField?.targetCollection?.filterTargetKey ??
|
|
1524
|
+
lastAssociationField?.targetCollection?.filterByTk ??
|
|
1525
|
+
lastAssociationField?.targetKey;
|
|
1521
1526
|
if (Array.isArray(raw)) {
|
|
1522
1527
|
const keys = raw.filter((key): key is string => typeof key === 'string' && !!key);
|
|
1523
1528
|
return keys.length ? keys : 'id';
|
|
@@ -394,16 +394,21 @@ export class FormValueRuntime {
|
|
|
394
394
|
private getArrayItemTargetKey(arrayPath?: NamePath): string | string[] {
|
|
395
395
|
let collection = this.model?.context?.collection;
|
|
396
396
|
let field: any;
|
|
397
|
+
let lastAssociationField: any;
|
|
397
398
|
for (const seg of arrayPath || []) {
|
|
398
399
|
if (typeof seg === 'number') continue;
|
|
399
400
|
if (typeof seg !== 'string' || !collection?.getField) break;
|
|
400
401
|
|
|
401
402
|
field = collection?.getField?.(seg);
|
|
402
403
|
if (!field?.isAssociationField?.()) break;
|
|
404
|
+
lastAssociationField = field;
|
|
403
405
|
collection = field?.targetCollection;
|
|
404
406
|
}
|
|
405
407
|
|
|
406
|
-
const raw =
|
|
408
|
+
const raw =
|
|
409
|
+
lastAssociationField?.targetCollection?.filterTargetKey ??
|
|
410
|
+
lastAssociationField?.targetCollection?.filterByTk ??
|
|
411
|
+
lastAssociationField?.targetKey;
|
|
407
412
|
if (Array.isArray(raw)) {
|
|
408
413
|
const keys = raw.filter((key): key is string => typeof key === 'string' && !!key);
|
|
409
414
|
return keys.length ? keys : 'id';
|
|
@@ -23,6 +23,11 @@ export interface LegacyClearer {
|
|
|
23
23
|
(model: any): void;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export function hasPersistedAssignRulesValue(model: any, flowKey: string, stepKey: string): boolean {
|
|
27
|
+
const params = model?.getStepParams?.(flowKey, stepKey);
|
|
28
|
+
return !!params && Object.prototype.hasOwnProperty.call(params, 'value');
|
|
29
|
+
}
|
|
30
|
+
|
|
26
31
|
function getPropsInitialValue(model: any): any | undefined {
|
|
27
32
|
if (!model) return undefined;
|
|
28
33
|
const props = typeof model.getProps === 'function' ? model.getProps() : model.props;
|
|
@@ -70,15 +75,26 @@ function deleteStepParams(model: any, flowKey: string, stepKey: string) {
|
|
|
70
75
|
model.emitter?.emit?.('onStepParamsChanged');
|
|
71
76
|
}
|
|
72
77
|
|
|
78
|
+
function deletePropsInitialValue(model: any) {
|
|
79
|
+
if (!model) return;
|
|
80
|
+
|
|
81
|
+
model.setProps?.({ initialValue: undefined });
|
|
82
|
+
|
|
83
|
+
if (model.props && Object.prototype.hasOwnProperty.call(model.props, 'initialValue')) {
|
|
84
|
+
delete model.props.initialValue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const optionsProps = model._options?.props;
|
|
88
|
+
if (optionsProps && Object.prototype.hasOwnProperty.call(optionsProps, 'initialValue')) {
|
|
89
|
+
delete optionsProps.initialValue;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
export function createLegacyClearer(flowKeys: string[]): LegacyClearer {
|
|
74
94
|
return (model: any): void => {
|
|
75
95
|
if (!model) return;
|
|
76
96
|
|
|
77
|
-
model
|
|
78
|
-
|
|
79
|
-
if (model.props && Object.prototype.hasOwnProperty.call(model.props, 'initialValue')) {
|
|
80
|
-
delete model.props.initialValue;
|
|
81
|
-
}
|
|
97
|
+
deletePropsInitialValue(model);
|
|
82
98
|
|
|
83
99
|
for (const flowKey of flowKeys) {
|
|
84
100
|
deleteStepParams(model, flowKey, 'initialValue');
|
|
@@ -932,12 +932,12 @@ const HighPerformanceTable = React.memo(
|
|
|
932
932
|
|
|
933
933
|
return {
|
|
934
934
|
onClick: async (event) => {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
935
|
+
const selected = highlightedRowKey !== rowKey;
|
|
936
|
+
defineClickedRowRecordVariable(model, selected ? record : null);
|
|
937
|
+
try {
|
|
938
|
+
await model.dispatchEvent('rowClick', { record, rowIndex, event, selected });
|
|
939
|
+
} finally {
|
|
938
940
|
removeClickedRowRecordVariable(model);
|
|
939
|
-
} else {
|
|
940
|
-
await model.dispatchEvent('rowClick', { record, rowIndex, event });
|
|
941
941
|
}
|
|
942
942
|
},
|
|
943
943
|
rowIndex,
|
|
@@ -1072,7 +1072,12 @@ TableBlockModel.registerEvents({
|
|
|
1072
1072
|
|
|
1073
1073
|
const model = ctx.model as TableBlockModel;
|
|
1074
1074
|
const rowKey = getRowKey(ctx.inputArgs.record, model.collection.filterTargetKey);
|
|
1075
|
-
|
|
1075
|
+
const selected = ctx.inputArgs.selected;
|
|
1076
|
+
if (selected === true) {
|
|
1077
|
+
model.highlightRow(ctx.inputArgs.record);
|
|
1078
|
+
} else if (selected === false) {
|
|
1079
|
+
model.clearHighlight();
|
|
1080
|
+
} else if (model.props.highlightedRowKey !== rowKey) {
|
|
1076
1081
|
model.highlightRow(ctx.inputArgs.record);
|
|
1077
1082
|
} else {
|
|
1078
1083
|
model.clearHighlight();
|
|
@@ -449,6 +449,9 @@ TableColumnModel.registerFlow({
|
|
|
449
449
|
quickEdit: {
|
|
450
450
|
title: tExpr('Enable quick edit'),
|
|
451
451
|
uiMode: { type: 'switch', key: 'editable' },
|
|
452
|
+
hideInSettings(ctx) {
|
|
453
|
+
return !!ctx.model.associationPathName;
|
|
454
|
+
},
|
|
452
455
|
defaultParams(ctx) {
|
|
453
456
|
if (ctx.model.collectionField.readonly || ctx.model.associationPathName) {
|
|
454
457
|
return {
|
|
@@ -460,7 +463,7 @@ TableColumnModel.registerFlow({
|
|
|
460
463
|
};
|
|
461
464
|
},
|
|
462
465
|
handler(ctx, params) {
|
|
463
|
-
ctx.model.setProps('editable', params.editable);
|
|
466
|
+
ctx.model.setProps('editable', ctx.model.associationPathName ? false : params.editable);
|
|
464
467
|
},
|
|
465
468
|
},
|
|
466
469
|
model: {
|
|
@@ -535,10 +538,13 @@ TableColumnModel.registerFlow({
|
|
|
535
538
|
fieldModel.setStepParams('fieldSettings', 'init', fieldSettingsInit);
|
|
536
539
|
await fieldModel.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
537
540
|
}
|
|
541
|
+
if (targetUse) {
|
|
542
|
+
ctx.model.setStepParams('tableColumnSettings', 'model', { use: targetUse });
|
|
543
|
+
}
|
|
538
544
|
ctx.model.setProps(targetCollectionField.getComponentProps());
|
|
539
545
|
},
|
|
540
546
|
defaultParams: (ctx: any) => {
|
|
541
|
-
const titleField = ctx.model
|
|
547
|
+
const titleField = ctx.model?.context?.collectionField?.targetCollectionTitleFieldName;
|
|
542
548
|
return {
|
|
543
549
|
label: getSavedAssociationTitleField(ctx.model) || titleField,
|
|
544
550
|
};
|
|
@@ -12,45 +12,55 @@ import { castArray } from 'lodash';
|
|
|
12
12
|
import { BlockSceneEnum } from '../../base/BlockModel';
|
|
13
13
|
import { TableBlockModel } from './TableBlockModel';
|
|
14
14
|
|
|
15
|
+
export function getAssociationSelectForeignKeyFilter(collectionField: any) {
|
|
16
|
+
const isOToAny = ['oho', 'o2m'].includes(collectionField?.interface);
|
|
17
|
+
const foreignKey = collectionField?.foreignKey;
|
|
18
|
+
if (!isOToAny || !foreignKey) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
[foreignKey]: {
|
|
23
|
+
$is: null,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getAssociationSelectAssociatedRecordsFilter(collection: any, associatedRecords: any[] = []) {
|
|
29
|
+
const targetKey = collection?.filterTargetKey || 'id';
|
|
30
|
+
const filterKeys = associatedRecords
|
|
31
|
+
.map((record) => record?.[targetKey])
|
|
32
|
+
.filter((value) => value !== undefined && value !== null && value !== '');
|
|
33
|
+
|
|
34
|
+
if (!filterKeys.length) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
[`${targetKey}.$ne`]: filterKeys,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
15
43
|
export class TableSelectModel extends TableBlockModel {
|
|
16
44
|
static scene = BlockSceneEnum.select;
|
|
17
45
|
rowSelectionProps: any = observable.deep({});
|
|
18
46
|
onInit(options: any) {
|
|
19
47
|
super.onInit(options);
|
|
20
48
|
const collectionField = this.context.view.inputArgs.collectionField || {};
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
if (isOToAny) {
|
|
24
|
-
const foreignKey = collectionField.foreignKey;
|
|
49
|
+
const foreignKeyFilter = getAssociationSelectForeignKeyFilter(collectionField);
|
|
50
|
+
if (foreignKeyFilter) {
|
|
25
51
|
const filterGroupKey = `${this.uid}-${collectionField.name}`;
|
|
26
|
-
|
|
27
|
-
if (sourceId != null) {
|
|
28
|
-
this.resource.addFilterGroup(filterGroupKey, {
|
|
29
|
-
$or: [{ [foreignKey]: { $is: null } }, { [foreignKey]: { $eq: sourceId } }],
|
|
30
|
-
});
|
|
31
|
-
} else {
|
|
32
|
-
this.resource.addFilterGroup(filterGroupKey, {
|
|
33
|
-
[foreignKey]: {
|
|
34
|
-
$is: null,
|
|
35
|
-
},
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
}
|
|
52
|
+
this.resource.addFilterGroup(filterGroupKey, foreignKeyFilter);
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
Object.assign(this.rowSelectionProps, this.context.view.inputArgs.rowSelectionProps || {});
|
|
42
56
|
|
|
43
57
|
const getSelectedRows = this.rowSelectionProps?.defaultSelectedRows;
|
|
44
58
|
const selectData = typeof getSelectedRows === 'function' ? getSelectedRows() : getSelectedRows;
|
|
45
|
-
const data = (selectData
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.filter(Boolean);
|
|
51
|
-
this.resource.addFilterGroup(`${this.uid}-select`, {
|
|
52
|
-
[`${this.collection.filterTargetKey}.$ne`]: filterKeys,
|
|
53
|
-
});
|
|
59
|
+
const data = [...castArray(selectData || []), ...castArray(this.context.view.inputArgs.associatedRecords || [])];
|
|
60
|
+
const associatedRecordsFilter = getAssociationSelectAssociatedRecordsFilter(this.collection, data);
|
|
61
|
+
if (associatedRecordsFilter) {
|
|
62
|
+
this.resource.addFilterGroup(`${this.uid}-select`, associatedRecordsFilter);
|
|
63
|
+
}
|
|
54
64
|
}
|
|
55
65
|
}
|
|
56
66
|
|
|
@@ -0,0 +1,69 @@
|
|
|
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 } from 'vitest';
|
|
12
|
+
import '@nocobase/client';
|
|
13
|
+
import { TableBlockModel } from '../TableBlockModel';
|
|
14
|
+
|
|
15
|
+
function createTableModel() {
|
|
16
|
+
const engine = new FlowEngine();
|
|
17
|
+
engine.registerModels({ TableBlockModel });
|
|
18
|
+
|
|
19
|
+
const ds = engine.dataSourceManager.getDataSource('main');
|
|
20
|
+
ds.addCollection({
|
|
21
|
+
name: 'posts',
|
|
22
|
+
filterTargetKey: 'id',
|
|
23
|
+
fields: [
|
|
24
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
25
|
+
{ name: 'title', type: 'string', interface: 'input' },
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return engine.createModel<TableBlockModel>({
|
|
30
|
+
uid: 'posts-table',
|
|
31
|
+
use: 'TableBlockModel',
|
|
32
|
+
stepParams: {
|
|
33
|
+
resourceSettings: {
|
|
34
|
+
init: {
|
|
35
|
+
dataSourceKey: 'main',
|
|
36
|
+
collectionName: 'posts',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('TableBlockModel rowClick event', () => {
|
|
44
|
+
it('highlights the clicked row when selected is true', async () => {
|
|
45
|
+
const model = createTableModel();
|
|
46
|
+
const record = { id: 1, title: 'first post' };
|
|
47
|
+
const rowClick = model.getEvent('rowClick');
|
|
48
|
+
|
|
49
|
+
await rowClick?.handler({ model, inputArgs: { record, selected: true } } as any, {
|
|
50
|
+
condition: { logic: '$and', items: [] },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(model.props.highlightedRowKey).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('clears the highlighted row when selected is false', async () => {
|
|
57
|
+
const model = createTableModel();
|
|
58
|
+
const record = { id: 1, title: 'first post' };
|
|
59
|
+
const rowClick = model.getEvent('rowClick');
|
|
60
|
+
|
|
61
|
+
model.highlightRow(record);
|
|
62
|
+
|
|
63
|
+
await rowClick?.handler({ model, inputArgs: { record, selected: false } } as any, {
|
|
64
|
+
condition: { logic: '$and', items: [] },
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(model.props.highlightedRowKey).toBeNull();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -8,10 +8,46 @@
|
|
|
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', () => {
|
|
15
|
+
it('hides quick edit setting for relation path columns added from association groups', async () => {
|
|
16
|
+
const engine = new FlowEngine();
|
|
17
|
+
const model = new TableColumnModel({ uid: 'table-column-relation-path-quick-edit', flowEngine: engine } as any);
|
|
18
|
+
const quickEditStep = model.getFlow('tableColumnSettings')?.steps?.quickEdit as any;
|
|
19
|
+
|
|
20
|
+
const hidden = await quickEditStep.hideInSettings({
|
|
21
|
+
model: {
|
|
22
|
+
associationPathName: 'department',
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(hidden).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('keeps quick edit disabled for relation path columns even when params enable it', () => {
|
|
30
|
+
const engine = new FlowEngine();
|
|
31
|
+
const model = new TableColumnModel({
|
|
32
|
+
uid: 'table-column-relation-path-disable-quick-edit',
|
|
33
|
+
flowEngine: engine,
|
|
34
|
+
} as any);
|
|
35
|
+
const quickEditStep = model.getFlow('tableColumnSettings')?.steps?.quickEdit as any;
|
|
36
|
+
const setProps = vi.fn();
|
|
37
|
+
|
|
38
|
+
quickEditStep.handler(
|
|
39
|
+
{
|
|
40
|
+
model: {
|
|
41
|
+
associationPathName: 'department',
|
|
42
|
+
setProps,
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{ editable: true },
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(setProps).toHaveBeenCalledWith('editable', false);
|
|
49
|
+
});
|
|
50
|
+
|
|
15
51
|
it('hides sortable setting for association fields', async () => {
|
|
16
52
|
const engine = new FlowEngine();
|
|
17
53
|
const model = new TableColumnModel({ uid: 'table-column-association-sorter', flowEngine: engine } as any);
|
|
@@ -69,4 +105,99 @@ describe('TableColumnModel sorter settings', () => {
|
|
|
69
105
|
|
|
70
106
|
expect(hidden).toBe(true);
|
|
71
107
|
});
|
|
108
|
+
|
|
109
|
+
it('updates field component setting when association title field changes', async () => {
|
|
110
|
+
const engine = new FlowEngine();
|
|
111
|
+
const model = new TableColumnModel({ uid: 'table-column-title-field-component', flowEngine: engine } as any);
|
|
112
|
+
const titleFieldStep = model.getFlow('tableColumnSettings')?.steps?.fieldNames as any;
|
|
113
|
+
const setStepParams = vi.fn();
|
|
114
|
+
const setProps = vi.fn();
|
|
115
|
+
const dispatchEvent = vi.fn();
|
|
116
|
+
const targetCollectionField = {
|
|
117
|
+
getComponentProps: () => ({}),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
await titleFieldStep.beforeParamsSave(
|
|
121
|
+
{
|
|
122
|
+
collectionField: {
|
|
123
|
+
isAssociationField: () => true,
|
|
124
|
+
targetCollection: {
|
|
125
|
+
name: 'departments',
|
|
126
|
+
getField: () => targetCollectionField,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
model: {
|
|
130
|
+
collectionField: {
|
|
131
|
+
dataSourceKey: 'main',
|
|
132
|
+
},
|
|
133
|
+
constructor: {
|
|
134
|
+
getDefaultBindingByField: () => ({
|
|
135
|
+
modelName: 'DisplayTextFieldModel',
|
|
136
|
+
}),
|
|
137
|
+
},
|
|
138
|
+
subModels: {
|
|
139
|
+
field: {
|
|
140
|
+
use: 'DisplayTextFieldModel',
|
|
141
|
+
setStepParams,
|
|
142
|
+
dispatchEvent,
|
|
143
|
+
},
|
|
144
|
+
},
|
|
145
|
+
setStepParams,
|
|
146
|
+
setProps,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{ label: 'code' },
|
|
150
|
+
{ label: 'name' },
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(setStepParams).toHaveBeenCalledWith('tableColumnSettings', 'model', { use: 'DisplayTextFieldModel' });
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('does not update field component setting when title field refresh fails', async () => {
|
|
157
|
+
const engine = new FlowEngine();
|
|
158
|
+
const model = new TableColumnModel({ uid: 'table-column-title-field-component-failed', flowEngine: engine } as any);
|
|
159
|
+
const titleFieldStep = model.getFlow('tableColumnSettings')?.steps?.fieldNames as any;
|
|
160
|
+
const setStepParams = vi.fn();
|
|
161
|
+
const setProps = vi.fn();
|
|
162
|
+
const targetCollectionField = {
|
|
163
|
+
getComponentProps: () => ({}),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
await expect(
|
|
167
|
+
titleFieldStep.beforeParamsSave(
|
|
168
|
+
{
|
|
169
|
+
collectionField: {
|
|
170
|
+
isAssociationField: () => true,
|
|
171
|
+
targetCollection: {
|
|
172
|
+
name: 'departments',
|
|
173
|
+
getField: () => targetCollectionField,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
model: {
|
|
177
|
+
collectionField: {
|
|
178
|
+
dataSourceKey: 'main',
|
|
179
|
+
},
|
|
180
|
+
constructor: {
|
|
181
|
+
getDefaultBindingByField: () => ({
|
|
182
|
+
modelName: 'DisplayTextFieldModel',
|
|
183
|
+
}),
|
|
184
|
+
},
|
|
185
|
+
subModels: {
|
|
186
|
+
field: {
|
|
187
|
+
use: 'DisplayTextFieldModel',
|
|
188
|
+
setStepParams: vi.fn(),
|
|
189
|
+
dispatchEvent: vi.fn().mockRejectedValue(new Error('beforeRender failed')),
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
setStepParams,
|
|
193
|
+
setProps,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{ label: 'code' },
|
|
197
|
+
{ label: 'name' },
|
|
198
|
+
),
|
|
199
|
+
).rejects.toThrow('beforeRender failed');
|
|
200
|
+
|
|
201
|
+
expect(setStepParams).not.toHaveBeenCalledWith('tableColumnSettings', 'model', expect.anything());
|
|
202
|
+
});
|
|
72
203
|
});
|
|
@@ -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
|
+
});
|
|
@@ -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>
|
|
@@ -48,6 +48,13 @@ export class DisplayEnumFieldModel extends ClickableFieldModel {
|
|
|
48
48
|
return value === null || value === undefined || value === '';
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
private isEmptyEnumValue(value: any) {
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
return value.length === 0;
|
|
54
|
+
}
|
|
55
|
+
return this.isEmpty(value);
|
|
56
|
+
}
|
|
57
|
+
|
|
51
58
|
public renderComponent(value) {
|
|
52
59
|
const { options = [], dataSource } = this.props;
|
|
53
60
|
const currentOptions = getCurrentOptions(value, dataSource || options, fieldNames);
|
|
@@ -61,6 +68,43 @@ export class DisplayEnumFieldModel extends ClickableFieldModel {
|
|
|
61
68
|
</Tag>
|
|
62
69
|
));
|
|
63
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Keep array values for multipleSelect/checkboxGroup.
|
|
74
|
+
* ClickableFieldModel will normalize arrays to joined strings, which breaks option label mapping.
|
|
75
|
+
*/
|
|
76
|
+
renderInDisplayStyle(value, record?, isToMany?, wrap?) {
|
|
77
|
+
const { clickToOpen = false, ...restProps } = this.props;
|
|
78
|
+
void wrap;
|
|
79
|
+
|
|
80
|
+
const result = this.renderComponent(value);
|
|
81
|
+
const display = record ? (this.isEmptyEnumValue(value) ? 'N/A' : result) : result;
|
|
82
|
+
|
|
83
|
+
const commonStyle = {
|
|
84
|
+
cursor: clickToOpen ? 'pointer' : 'default',
|
|
85
|
+
alignItems: 'center',
|
|
86
|
+
gap: 4,
|
|
87
|
+
display: isToMany && 'inline-block',
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const handleClick = (e) => {
|
|
91
|
+
clickToOpen && this.onClick(e, record);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (clickToOpen) {
|
|
95
|
+
return (
|
|
96
|
+
<a {...restProps} style={commonStyle} onClick={handleClick}>
|
|
97
|
+
{display}
|
|
98
|
+
</a>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<span {...restProps} style={commonStyle} className={restProps.className}>
|
|
104
|
+
{display}
|
|
105
|
+
</span>
|
|
106
|
+
);
|
|
107
|
+
}
|
|
64
108
|
}
|
|
65
109
|
DisplayEnumFieldModel.define({
|
|
66
110
|
label: tExpr('Select'),
|