@nocobase/client-v2 2.4.0-alpha.4 → 2.4.0-alpha.6
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/components/form/ScanInput/useCodeScanner.d.ts +12 -2
- package/es/components/form/ScanInput/zxingWasmDecoder.d.ts +9 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +2 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +1 -0
- package/es/flow/components/filter/VariableFilterItem.d.ts +7 -0
- package/es/index.mjs +18 -18
- package/lib/index.js +111 -111
- package/package.json +9 -8
- package/src/collection-manager/__tests__/field-configure.test.ts +52 -0
- package/src/collection-manager/field-configure.ts +2 -2
- package/src/components/form/ScanInput/CodeScanner.tsx +3 -1
- package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +7 -1
- package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +182 -8
- package/src/components/form/ScanInput/__tests__/zxingWasmDecoder.test.ts +78 -0
- package/src/components/form/ScanInput/useCodeScanner.ts +135 -20
- package/src/components/form/ScanInput/zxingWasmDecoder.ts +64 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +39 -0
- package/src/flow/components/FieldAssignValueInput.tsx +4 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +21 -12
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +9 -0
- package/src/flow/components/filter/VariableFilterItem.tsx +11 -3
- package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
- package/src/flow/models/base/GridModel.tsx +0 -1
- package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +14 -3
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
- package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
- package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +26 -5
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +145 -0
- package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
- package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
- package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
|
@@ -77,6 +77,7 @@ async function setupFormModel() {
|
|
|
77
77
|
ds.addCollection({
|
|
78
78
|
name: 'levels',
|
|
79
79
|
filterTargetKey: 'id',
|
|
80
|
+
titleField: 'name',
|
|
80
81
|
fields: [
|
|
81
82
|
{ name: 'id', type: 'integer', interface: 'number' },
|
|
82
83
|
{ name: 'name', type: 'string', interface: 'text' },
|
|
@@ -549,6 +550,150 @@ describe('FormBlockModel (form/formValues injection & server resolve anchors)',
|
|
|
549
550
|
expect(api.request).toHaveBeenCalledTimes(1);
|
|
550
551
|
});
|
|
551
552
|
|
|
553
|
+
it('resolves a configured nested association from the server when the local value is only its key', async () => {
|
|
554
|
+
const model = await setupFormModel();
|
|
555
|
+
const api = {
|
|
556
|
+
request: vi.fn(async (config: any) => {
|
|
557
|
+
const batch = config?.data?.values?.batch || [];
|
|
558
|
+
const item = batch[0] || {};
|
|
559
|
+
expect(item.template).toEqual({ level: '{{ ctx.formValues.customer.level }}' });
|
|
560
|
+
expect(Object.keys(item.contextParams || {})).toEqual(['formValues.customer']);
|
|
561
|
+
expect(item.contextParams['formValues.customer']).toMatchObject({
|
|
562
|
+
collection: 'customers',
|
|
563
|
+
filterByTk: 9,
|
|
564
|
+
});
|
|
565
|
+
return {
|
|
566
|
+
data: {
|
|
567
|
+
data: {
|
|
568
|
+
results: [{ id: item.id, data: { level: { id: 'level-1', name: 'Level 1' } } }],
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
} as any;
|
|
572
|
+
}),
|
|
573
|
+
} as any;
|
|
574
|
+
(model.flowEngine.context as any).defineProperty('api', { value: api });
|
|
575
|
+
|
|
576
|
+
function HookCaller() {
|
|
577
|
+
model.useHooksBeforeRender();
|
|
578
|
+
return null;
|
|
579
|
+
}
|
|
580
|
+
render(React.createElement(HookCaller));
|
|
581
|
+
|
|
582
|
+
const mem: Record<string, any> = {};
|
|
583
|
+
const fakeForm = {
|
|
584
|
+
setFieldsValue: (values: Record<string, any>) => Object.assign(mem, values),
|
|
585
|
+
getFieldsValue: () => ({ ...mem }),
|
|
586
|
+
getFieldValue: (namePath: any) => getByPath(mem, namePath),
|
|
587
|
+
setFieldValue: (key: string, value: any) => (mem[key] = value),
|
|
588
|
+
};
|
|
589
|
+
(model.context as any).defineProperty('form', { value: fakeForm });
|
|
590
|
+
fakeForm.setFieldsValue({ customer: { id: 9, level: 'level-1' } });
|
|
591
|
+
mockFormGridEnabledFields(model, ['customer']);
|
|
592
|
+
|
|
593
|
+
const output = await (model.context as any).resolveJsonTemplate({
|
|
594
|
+
level: '{{ ctx.formValues.customer.level }}',
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
expect(api.request).toHaveBeenCalledTimes(1);
|
|
598
|
+
expect(output).toEqual({ level: { id: 'level-1', name: 'Level 1' } });
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
it('resolves a configured nested association when its local record does not include the title field', async () => {
|
|
602
|
+
const model = await setupFormModel();
|
|
603
|
+
const api = {
|
|
604
|
+
request: vi.fn(async (config: any) => {
|
|
605
|
+
const item = config?.data?.values?.batch?.[0] || {};
|
|
606
|
+
return {
|
|
607
|
+
data: {
|
|
608
|
+
data: {
|
|
609
|
+
results: [{ id: item.id, data: { level: { id: 'level-1', name: 'Level 1' } } }],
|
|
610
|
+
},
|
|
611
|
+
},
|
|
612
|
+
} as any;
|
|
613
|
+
}),
|
|
614
|
+
} as any;
|
|
615
|
+
(model.flowEngine.context as any).defineProperty('api', { value: api });
|
|
616
|
+
|
|
617
|
+
function HookCaller() {
|
|
618
|
+
model.useHooksBeforeRender();
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
render(React.createElement(HookCaller));
|
|
622
|
+
|
|
623
|
+
const mem: Record<string, any> = {};
|
|
624
|
+
const fakeForm = {
|
|
625
|
+
setFieldsValue: (values: Record<string, any>) => Object.assign(mem, values),
|
|
626
|
+
getFieldsValue: () => ({ ...mem }),
|
|
627
|
+
getFieldValue: (namePath: any) => getByPath(mem, namePath),
|
|
628
|
+
setFieldValue: (key: string, value: any) => (mem[key] = value),
|
|
629
|
+
};
|
|
630
|
+
(model.context as any).defineProperty('form', { value: fakeForm });
|
|
631
|
+
fakeForm.setFieldsValue({ customer: { id: 9, level: { id: 'level-1' } } });
|
|
632
|
+
mockFormGridEnabledFields(model, ['customer']);
|
|
633
|
+
|
|
634
|
+
const output = await (model.context as any).resolveJsonTemplate({
|
|
635
|
+
level: '{{ ctx.formValues.customer.level }}',
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
expect(api.request).toHaveBeenCalledTimes(1);
|
|
639
|
+
expect(output).toEqual({ level: { id: 'level-1', name: 'Level 1' } });
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('uses a configured nested association locally when its record is already loaded', async () => {
|
|
643
|
+
const model = await setupFormModel();
|
|
644
|
+
const api = { request: vi.fn(async () => ({ data: {} }) as any) } as any;
|
|
645
|
+
(model.flowEngine.context as any).defineProperty('api', { value: api });
|
|
646
|
+
|
|
647
|
+
function HookCaller() {
|
|
648
|
+
model.useHooksBeforeRender();
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
render(React.createElement(HookCaller));
|
|
652
|
+
|
|
653
|
+
const mem: Record<string, any> = {};
|
|
654
|
+
const fakeForm = {
|
|
655
|
+
setFieldsValue: (values: Record<string, any>) => Object.assign(mem, values),
|
|
656
|
+
getFieldsValue: () => ({ ...mem }),
|
|
657
|
+
getFieldValue: (namePath: any) => getByPath(mem, namePath),
|
|
658
|
+
setFieldValue: (key: string, value: any) => (mem[key] = value),
|
|
659
|
+
};
|
|
660
|
+
(model.context as any).defineProperty('form', { value: fakeForm });
|
|
661
|
+
const level = { id: 'level-1', name: 'Level 1' };
|
|
662
|
+
fakeForm.setFieldsValue({ customer: { id: 9, level } });
|
|
663
|
+
mockFormGridEnabledFields(model, ['customer']);
|
|
664
|
+
|
|
665
|
+
const output = await (model.context as any).resolveJsonTemplate({
|
|
666
|
+
level: '{{ ctx.formValues.customer.level }}',
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
expect(output).toEqual({ level });
|
|
670
|
+
expect(api.request).not.toHaveBeenCalled();
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
it('keeps indexed paths local when the nested association record is already loaded', async () => {
|
|
674
|
+
const model = await setupFormModel();
|
|
675
|
+
|
|
676
|
+
function HookCaller() {
|
|
677
|
+
model.useHooksBeforeRender();
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
render(React.createElement(HookCaller));
|
|
681
|
+
|
|
682
|
+
const mem: Record<string, any> = {};
|
|
683
|
+
const fakeForm = {
|
|
684
|
+
setFieldsValue: (values: Record<string, any>) => Object.assign(mem, values),
|
|
685
|
+
getFieldsValue: () => ({ ...mem }),
|
|
686
|
+
getFieldValue: (namePath: any) => getByPath(mem, namePath),
|
|
687
|
+
setFieldValue: (key: string, value: any) => (mem[key] = value),
|
|
688
|
+
};
|
|
689
|
+
(model.context as any).defineProperty('form', { value: fakeForm });
|
|
690
|
+
fakeForm.setFieldsValue({ assignees: [{ id: 3, org: { id: 1, name: 'Org 1' } }] });
|
|
691
|
+
mockFormGridEnabledFields(model, ['assignees']);
|
|
692
|
+
|
|
693
|
+
const options = (model.context as any).getPropertyOptions('formValues');
|
|
694
|
+
expect(options.resolveOnServer('assignees[0].org.name')).toBe(false);
|
|
695
|
+
});
|
|
696
|
+
|
|
552
697
|
it('configured toMany dot aggregation path uses local value and skips server', async () => {
|
|
553
698
|
const model = await setupFormModel();
|
|
554
699
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React from 'react';
|
|
11
|
+
import { act, render, screen, waitFor, cleanup } from '@testing-library/react';
|
|
12
|
+
import { Form, Input } from 'antd';
|
|
13
|
+
import { afterEach, expect, it, vi } from 'vitest';
|
|
14
|
+
import { FlowEngine, SingleRecordResource } from '@nocobase/flow-engine';
|
|
15
|
+
import { generateFlowModelRdFromToken } from '@nocobase/utils/client';
|
|
16
|
+
import { CreateFormModel, EditFormModel, FormGridModel, FormItemModel, FormComponent } from '../../../..';
|
|
17
|
+
import { fieldLinkageRules, linkageAssignField } from '../../../../actions/linkageRules';
|
|
18
|
+
|
|
19
|
+
afterEach(cleanup);
|
|
20
|
+
|
|
21
|
+
it.each([
|
|
22
|
+
{ use: 'CreateFormModel', mode: 'default', expected: 'STAFF-001' },
|
|
23
|
+
{ use: 'EditFormModel', mode: 'default', expected: '' },
|
|
24
|
+
{ use: 'EditFormModel', mode: 'override', expected: 'STAFF-001' },
|
|
25
|
+
])('$use respects $mode linkage after a delayed popup variable response', async ({ use, mode, expected }) => {
|
|
26
|
+
const uid = `popup-${use}-${mode}`;
|
|
27
|
+
const engine = new FlowEngine();
|
|
28
|
+
engine.registerModels({ CreateFormModel, EditFormModel, FormGridModel, FormItemModel });
|
|
29
|
+
engine.registerActions({ fieldLinkageRules, linkageAssignField });
|
|
30
|
+
engine.context.dataSourceManager.getDataSource('main').addCollection({
|
|
31
|
+
name: 't1_user',
|
|
32
|
+
filterTargetKey: 'id',
|
|
33
|
+
fields: [
|
|
34
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
35
|
+
{ name: 'staffseq', type: 'string', interface: 'input' },
|
|
36
|
+
{ name: 'staffname', type: 'string', interface: 'input' },
|
|
37
|
+
],
|
|
38
|
+
});
|
|
39
|
+
const form = engine.createModel<CreateFormModel | EditFormModel>({
|
|
40
|
+
uid,
|
|
41
|
+
use,
|
|
42
|
+
stepParams: { resourceSettings: { init: { dataSourceKey: 'main', collectionName: 't1_user', filterByTk: 1 } } },
|
|
43
|
+
subModels: {
|
|
44
|
+
grid: {
|
|
45
|
+
uid: `${uid}-grid`,
|
|
46
|
+
use: 'FormGridModel',
|
|
47
|
+
subModels: {
|
|
48
|
+
items: [
|
|
49
|
+
{
|
|
50
|
+
uid: `${uid}-staffname`,
|
|
51
|
+
use: 'FormItemModel',
|
|
52
|
+
props: { name: 'staffname' },
|
|
53
|
+
stepParams: {
|
|
54
|
+
fieldSettings: { init: { dataSourceKey: 'main', collectionName: 't1_user', fieldPath: 'staffname' } },
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
const configured = '{{ ctx.popup.record.staffseq }}';
|
|
63
|
+
const rules = {
|
|
64
|
+
value: [
|
|
65
|
+
{
|
|
66
|
+
key: 'rule',
|
|
67
|
+
title: 'Rule',
|
|
68
|
+
enable: true,
|
|
69
|
+
condition: { logic: '$and', items: [] },
|
|
70
|
+
actions: [
|
|
71
|
+
{
|
|
72
|
+
key: 'assign',
|
|
73
|
+
name: 'linkageAssignField',
|
|
74
|
+
params: {
|
|
75
|
+
value: [
|
|
76
|
+
{
|
|
77
|
+
key: 'field',
|
|
78
|
+
enable: true,
|
|
79
|
+
mode,
|
|
80
|
+
condition: { logic: '$and', items: [] },
|
|
81
|
+
targetPath: 'staffname',
|
|
82
|
+
value: configured,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
],
|
|
90
|
+
};
|
|
91
|
+
form.setStepParams('eventSettings', 'linkageRules', rules);
|
|
92
|
+
const saved = JSON.parse(JSON.stringify(form.serialize()));
|
|
93
|
+
expect(saved.subModels.grid.stepParams.eventSettings.linkageRules).toEqual(rules);
|
|
94
|
+
expect(saved.stepParams.eventSettings?.linkageRules).toBeUndefined();
|
|
95
|
+
|
|
96
|
+
const token = `test.${Buffer.from(JSON.stringify({ userId: 2, signInTime: 'member-form' })).toString('base64url')}.sig`;
|
|
97
|
+
type Item = { id: string; rd: string; template: unknown; contextParams: unknown };
|
|
98
|
+
let resolveResponse: (() => void) | undefined;
|
|
99
|
+
const responseReady = new Promise<void>((resolve) => {
|
|
100
|
+
resolveResponse = resolve;
|
|
101
|
+
});
|
|
102
|
+
const request = vi.fn(async ({ data }: { data: { values: { batch: Item[] } } }) => {
|
|
103
|
+
const batch = data.values.batch;
|
|
104
|
+
expect(batch[0].rd).toBe(generateFlowModelRdFromToken(form.uid, token));
|
|
105
|
+
expect(batch[0].contextParams).toEqual({
|
|
106
|
+
'popup.record': { collection: 't1_user', dataSourceKey: 'main', filterByTk: '1' },
|
|
107
|
+
});
|
|
108
|
+
await responseReady;
|
|
109
|
+
return {
|
|
110
|
+
data: {
|
|
111
|
+
data: {
|
|
112
|
+
results: batch.map((item) => ({
|
|
113
|
+
id: item.id,
|
|
114
|
+
data: JSON.parse(JSON.stringify(item.template).replaceAll(configured, 'STAFF-001')),
|
|
115
|
+
})),
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
engine.context.defineProperty('api', { value: { auth: { token, role: 'member' }, request } });
|
|
121
|
+
form.context.defineProperty('popup', {
|
|
122
|
+
value: { record: { id: 1 } },
|
|
123
|
+
resolveOnServer: true,
|
|
124
|
+
meta: {
|
|
125
|
+
type: 'object',
|
|
126
|
+
buildVariablesParams: () => ({ record: { collection: 't1_user', dataSourceKey: 'main', filterByTk: '1' } }),
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
const resource = form.resource as SingleRecordResource;
|
|
130
|
+
resource.setData(use === 'EditFormModel' ? { id: 1, staffname: null } : {});
|
|
131
|
+
function View() {
|
|
132
|
+
form.useHooksBeforeRender();
|
|
133
|
+
return (
|
|
134
|
+
<FormComponent model={form}>
|
|
135
|
+
<Form.Item name="staffname">
|
|
136
|
+
<Input aria-label="staffname" />
|
|
137
|
+
</Form.Item>
|
|
138
|
+
</FormComponent>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const dispatchEvent = vi.spyOn(form, 'dispatchEvent');
|
|
142
|
+
const view = render(<View />);
|
|
143
|
+
try {
|
|
144
|
+
form.formValueRuntime?.mount({ sync: true });
|
|
145
|
+
// Simulate the field mounting, without involving the page layout and field-renderer plugins.
|
|
146
|
+
engine.emitter.emit('model:mounted', { model: form.subModels.grid.subModels.items[0] });
|
|
147
|
+
let pending: Promise<unknown> | undefined;
|
|
148
|
+
await act(async () => {
|
|
149
|
+
pending = form.applyFlow('eventSettings');
|
|
150
|
+
});
|
|
151
|
+
await waitFor(() => expect(request).toHaveBeenCalled());
|
|
152
|
+
await act(async () => {
|
|
153
|
+
resolveResponse?.();
|
|
154
|
+
await pending;
|
|
155
|
+
});
|
|
156
|
+
if (mode === 'default') {
|
|
157
|
+
// Resolving a default value must not overwrite an existing edit record.
|
|
158
|
+
expect(form.subModels.grid.subModels.items[0].props.initialValue).toBe('STAFF-001');
|
|
159
|
+
}
|
|
160
|
+
await waitFor(() => expect((screen.getByLabelText('staffname') as HTMLInputElement).value).toBe(expected));
|
|
161
|
+
expect(form.form.getFieldValue('staffname')).toBe(expected || null);
|
|
162
|
+
} finally {
|
|
163
|
+
resolveResponse?.();
|
|
164
|
+
await act(async () => {
|
|
165
|
+
view.unmount();
|
|
166
|
+
form.formValueRuntime?.dispose();
|
|
167
|
+
// Drain automatic linkage refreshes before another engine starts its variable batch.
|
|
168
|
+
await Promise.all(
|
|
169
|
+
dispatchEvent.mock.results.filter((result) => result.type === 'return').map((result) => result.value),
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
dispatchEvent.mockRestore();
|
|
173
|
+
engine.disposeScheduler();
|
|
174
|
+
}
|
|
175
|
+
});
|
|
@@ -17,14 +17,16 @@ import {
|
|
|
17
17
|
observer,
|
|
18
18
|
} from '@nocobase/flow-engine';
|
|
19
19
|
import { Table } from 'antd';
|
|
20
|
+
import type { TableProps } from 'antd';
|
|
20
21
|
import classNames from 'classnames';
|
|
21
22
|
import { DragEndEvent } from '@dnd-kit/core';
|
|
22
23
|
import { css } from '@emotion/css';
|
|
23
|
-
import { isEmpty } from 'lodash';
|
|
24
|
+
import { get, isEmpty, orderBy } from 'lodash';
|
|
24
25
|
import React, { useEffect, useMemo, useState, useCallback } from 'react';
|
|
25
26
|
import { useTranslation } from 'react-i18next';
|
|
26
27
|
import { FieldModel } from '../../base';
|
|
27
28
|
import { DetailsItemModel } from '../../blocks/details/DetailsItemModel';
|
|
29
|
+
import { FormAssociationItemModel } from '../../blocks/form/FormAssociationItemModel';
|
|
28
30
|
import { adjustColumnOrder } from '../../blocks/table/utils';
|
|
29
31
|
|
|
30
32
|
const HeaderWrapperComponent = React.memo((props) => {
|
|
@@ -65,11 +67,31 @@ const AddFieldColumn = ({ model }) => {
|
|
|
65
67
|
};
|
|
66
68
|
|
|
67
69
|
const DisplayTable = (props) => {
|
|
68
|
-
const { pageSize, value, size, collection, baseColumns, enableIndexColumn = true, model } = props;
|
|
70
|
+
const { pageSize, value: rawValue, size, collection, baseColumns, enableIndexColumn = true, model } = props;
|
|
71
|
+
const isFormAssociation = model.parent instanceof FormAssociationItemModel;
|
|
69
72
|
const [currentPage, setCurrentPage] = useState(1);
|
|
70
73
|
const [currentPageSize, setCurrentPageSize] = useState(pageSize);
|
|
74
|
+
const [localSort, setLocalSort] = useState<{ field: string; order: 'asc' | 'desc' }>();
|
|
71
75
|
const { t } = useTranslation();
|
|
72
76
|
|
|
77
|
+
const value = useMemo(() => {
|
|
78
|
+
if (!isFormAssociation || Array.isArray(rawValue)) return rawValue;
|
|
79
|
+
if (rawValue && Array.isArray(rawValue.rows)) return rawValue.rows;
|
|
80
|
+
return rawValue && typeof rawValue === 'object' ? [rawValue] : [];
|
|
81
|
+
}, [isFormAssociation, rawValue]);
|
|
82
|
+
|
|
83
|
+
const sortedValue = useMemo(
|
|
84
|
+
() =>
|
|
85
|
+
isFormAssociation && localSort
|
|
86
|
+
? orderBy(value, [(record) => get(record, localSort.field)], [localSort.order])
|
|
87
|
+
: value,
|
|
88
|
+
[isFormAssociation, localSort, value],
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
if (isFormAssociation) setCurrentPage(1);
|
|
93
|
+
}, [isFormAssociation, rawValue]);
|
|
94
|
+
|
|
73
95
|
useEffect(() => {
|
|
74
96
|
setCurrentPageSize(pageSize);
|
|
75
97
|
}, [pageSize]);
|
|
@@ -89,7 +111,7 @@ const DisplayTable = (props) => {
|
|
|
89
111
|
return t('Total {{count}} items', { count: total });
|
|
90
112
|
},
|
|
91
113
|
} as any;
|
|
92
|
-
}, [currentPage, currentPageSize, value]);
|
|
114
|
+
}, [currentPage, currentPageSize, value, t]);
|
|
93
115
|
|
|
94
116
|
const getColumns = () => {
|
|
95
117
|
const cols = adjustColumnOrder(
|
|
@@ -119,8 +141,20 @@ const DisplayTable = (props) => {
|
|
|
119
141
|
}
|
|
120
142
|
return cols;
|
|
121
143
|
};
|
|
122
|
-
const handleChange = useCallback(
|
|
123
|
-
async (pagination, filters,
|
|
144
|
+
const handleChange = useCallback<NonNullable<TableProps<Record<string, unknown>>['onChange']>>(
|
|
145
|
+
async (pagination, filters, sorters, extra) => {
|
|
146
|
+
const sorter = Array.isArray(sorters) ? sorters[0] : sorters;
|
|
147
|
+
if (isFormAssociation) {
|
|
148
|
+
if (extra.action !== 'sort') return;
|
|
149
|
+
const column = sorter?.column as { sortField?: string } | undefined;
|
|
150
|
+
const sortField = column?.sortField || sorter?.field;
|
|
151
|
+
const fullPath = Array.isArray(sortField) ? sortField.join('.') : String(sortField ?? '');
|
|
152
|
+
const prefix = `${model.context.fieldPath}.`;
|
|
153
|
+
const field = fullPath.startsWith(prefix) ? fullPath.slice(prefix.length) : fullPath;
|
|
154
|
+
setLocalSort(sorter?.order && field ? { field, order: sorter.order === 'ascend' ? 'asc' : 'desc' } : undefined);
|
|
155
|
+
setCurrentPage(1);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
124
158
|
//支持列点击排序
|
|
125
159
|
if (!isEmpty(sorter)) {
|
|
126
160
|
const resource = model.context.blockModel.resource;
|
|
@@ -138,7 +172,7 @@ const DisplayTable = (props) => {
|
|
|
138
172
|
await resource.refresh();
|
|
139
173
|
}
|
|
140
174
|
},
|
|
141
|
-
[model],
|
|
175
|
+
[isFormAssociation, model],
|
|
142
176
|
);
|
|
143
177
|
|
|
144
178
|
return (
|
|
@@ -147,7 +181,7 @@ const DisplayTable = (props) => {
|
|
|
147
181
|
size={size}
|
|
148
182
|
rowKey={collection.filterTargetKey}
|
|
149
183
|
scroll={{ x: 'max-content' }}
|
|
150
|
-
dataSource={
|
|
184
|
+
dataSource={sortedValue}
|
|
151
185
|
columns={getColumns()}
|
|
152
186
|
pagination={pagination}
|
|
153
187
|
onChange={handleChange}
|
|
@@ -269,3 +303,4 @@ DisplaySubTableFieldModel.define({
|
|
|
269
303
|
});
|
|
270
304
|
|
|
271
305
|
DetailsItemModel.bindModelToInterface('DisplaySubTableFieldModel', ['m2m', 'o2m', 'mbm']);
|
|
306
|
+
FormAssociationItemModel.bindModelToInterface('DisplaySubTableFieldModel', ['m2m', 'o2m', 'mbm']);
|