@nocobase/client-v2 2.3.0-beta.2 → 2.3.0-beta.21
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/RouteRepository.d.ts +6 -0
- package/es/components/form/ScanInput/useCodeScanner.d.ts +12 -2
- package/es/components/form/ScanInput/zxingWasmDecoder.d.ts +9 -0
- package/es/flow/admin-shell/admin-layout/MobileTopbarActionsContext.d.ts +10 -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 +10 -1
- package/es/flow/internal/utils/quickEditDataScope.d.ts +48 -0
- package/es/flow/internal/utils/quickEditViewContainer.d.ts +27 -0
- package/es/flow/models/actions/LinkActionUtils.d.ts +1 -0
- package/es/flow/models/blocks/assign-form/assignFormValue.d.ts +29 -0
- package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +1 -0
- package/es/index.mjs +100 -100
- package/lib/index.js +102 -102
- package/package.json +9 -8
- package/src/BaseApplication.tsx +2 -0
- package/src/RouteRepository.ts +14 -0
- package/src/__tests__/RouteRepository.test.ts +51 -0
- package/src/__tests__/app.test.tsx +13 -0
- 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/actions/__tests__/validation.test.ts +48 -0
- package/src/flow/admin-shell/admin-layout/MobileTopbarActionsContext.ts +13 -0
- package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +13 -2
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.mobile.test.tsx +205 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
- 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 +14 -4
- package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
- package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
- package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
- package/src/flow/internal/utils/__tests__/quickEditDataScope.test.ts +156 -0
- package/src/flow/internal/utils/__tests__/quickEditViewContainer.test.ts +52 -0
- package/src/flow/internal/utils/quickEditDataScope.tsx +177 -0
- package/src/flow/internal/utils/quickEditViewContainer.ts +46 -0
- package/src/flow/models/actions/JSItemActionModel.tsx +19 -3
- package/src/flow/models/actions/LinkActionModel.tsx +11 -9
- package/src/flow/models/actions/LinkActionUtils.ts +4 -2
- package/src/flow/models/actions/__tests__/JSItemActionModel.test.tsx +47 -1
- package/src/flow/models/actions/__tests__/LinkActionModel.test.ts +14 -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 +40 -12
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
- package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.multipleSelect.test.tsx +216 -0
- package/src/flow/models/blocks/assign-form/__tests__/assignFormValue.test.ts +41 -0
- package/src/flow/models/blocks/assign-form/assignFormValue.ts +64 -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/QuickEditFormModel.tsx +45 -11
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +145 -0
- package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +120 -0
- package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
- package/src/flow/models/blocks/js-block/JSBlock.tsx +5 -1
- package/src/flow/models/blocks/js-block/__tests__/JSBlockModel.test.tsx +23 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +31 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +64 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +11 -2
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +4 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +6 -3
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +13 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +47 -17
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +6 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.largeField.test.tsx +178 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableField.disabled.test.tsx +90 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +26 -2
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordSelectFieldModel.quickCreatePopup.test.tsx +183 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/associationSelect.search.test.ts +113 -0
- package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
- package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
- package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
- package/src/flow/models/fields/InputFieldModel.tsx +1 -1
- package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
- package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
- package/src/flow/models/fields/SelectFieldModel.tsx +5 -0
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +16 -0
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +29 -1
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +1 -0
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +20 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +5 -5
- package/src/flow/models/topbar/UserCenterTopbarActionModel.tsx +8 -4
|
@@ -0,0 +1,183 @@
|
|
|
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 {
|
|
11
|
+
FlowContext,
|
|
12
|
+
FlowEngine,
|
|
13
|
+
FlowEngineProvider,
|
|
14
|
+
FlowViewContextProvider,
|
|
15
|
+
type FlowModel,
|
|
16
|
+
} from '@nocobase/flow-engine';
|
|
17
|
+
import { cleanup, render, screen, waitFor } from '@testing-library/react';
|
|
18
|
+
import React from 'react';
|
|
19
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
20
|
+
import '../../../index';
|
|
21
|
+
import { BlockGridModel } from '../../../base/BlockGridModel';
|
|
22
|
+
import { RecordSelectFieldModel } from '../RecordSelectFieldModel';
|
|
23
|
+
|
|
24
|
+
type OpenViewHandler = (ctx: unknown, params: Record<string, unknown>) => void;
|
|
25
|
+
|
|
26
|
+
function getOpenViewHandler(): OpenViewHandler {
|
|
27
|
+
const flow = RecordSelectFieldModel.globalFlowRegistry.getFlow('popupSettings');
|
|
28
|
+
const handler = flow?.getStep('openView')?.serialize().handler;
|
|
29
|
+
|
|
30
|
+
if (!handler) {
|
|
31
|
+
throw new Error('popupSettings.openView handler is not registered');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return handler as OpenViewHandler;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createContext(record: Record<string, unknown>) {
|
|
38
|
+
const open = vi.fn();
|
|
39
|
+
const sourceCollection = {
|
|
40
|
+
getFilterByTK: vi.fn((sourceRecord: Record<string, unknown>) => sourceRecord.id),
|
|
41
|
+
};
|
|
42
|
+
const model = {
|
|
43
|
+
uid: 'record-select-uid',
|
|
44
|
+
props: {
|
|
45
|
+
quickCreate: 'modalAdd',
|
|
46
|
+
allowMultiple: true,
|
|
47
|
+
},
|
|
48
|
+
context: {
|
|
49
|
+
inputArgs: {},
|
|
50
|
+
flowSettingsEnabled: false,
|
|
51
|
+
},
|
|
52
|
+
flowEngine: {
|
|
53
|
+
context: {
|
|
54
|
+
themeToken: {
|
|
55
|
+
colorBgLayout: '#fff',
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
open,
|
|
63
|
+
context: {
|
|
64
|
+
inputArgs: { onChange: vi.fn() },
|
|
65
|
+
view: {
|
|
66
|
+
inputArgs: {
|
|
67
|
+
viewUid: 'approval-popup-uid',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
viewer: { open },
|
|
71
|
+
layoutContentElement: {},
|
|
72
|
+
model,
|
|
73
|
+
collection: {
|
|
74
|
+
dataSourceKey: 'main',
|
|
75
|
+
filterTargetKey: 'id',
|
|
76
|
+
},
|
|
77
|
+
collectionField: {
|
|
78
|
+
type: 'hasMany',
|
|
79
|
+
target: 'orgs',
|
|
80
|
+
resourceName: 'users.orgs',
|
|
81
|
+
collection: sourceCollection,
|
|
82
|
+
},
|
|
83
|
+
record,
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
describe('RecordSelectFieldModel quick create popup', () => {
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
cleanup();
|
|
91
|
+
vi.restoreAllMocks();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe.each(['drawer', 'dialog'])('%s configuration mode', (mode) => {
|
|
95
|
+
it.each([
|
|
96
|
+
{ scenario: 'configuration', modelEnabled: true, globalEnabled: false },
|
|
97
|
+
{ scenario: 'global configuration', modelEnabled: true, globalEnabled: true },
|
|
98
|
+
{ scenario: 'runtime', modelEnabled: false, globalEnabled: false },
|
|
99
|
+
{ scenario: 'read-only configuration', modelEnabled: false, globalEnabled: true },
|
|
100
|
+
])('uses the field configuration state in $scenario', async ({ modelEnabled, globalEnabled }) => {
|
|
101
|
+
const engine = new FlowEngine();
|
|
102
|
+
engine.registerModels({ BlockGridModel });
|
|
103
|
+
engine.flowSettings.enabled = globalEnabled;
|
|
104
|
+
engine.context.defineProperty('themeToken', {
|
|
105
|
+
value: { marginBlock: 16, marginSM: 8, paddingLG: 24 },
|
|
106
|
+
});
|
|
107
|
+
engine.context.defineProperty('t', { value: (key: string) => key });
|
|
108
|
+
const save = vi.fn(async (model: FlowModel) => model.serialize());
|
|
109
|
+
engine.setModelRepository({
|
|
110
|
+
findOne: vi.fn(async () => null),
|
|
111
|
+
save,
|
|
112
|
+
destroy: vi.fn(async () => true),
|
|
113
|
+
move: vi.fn(async () => undefined),
|
|
114
|
+
duplicate: vi.fn(async () => null),
|
|
115
|
+
});
|
|
116
|
+
const load = vi.spyOn(engine, 'loadOrCreateModel');
|
|
117
|
+
const { context, open } = createContext({});
|
|
118
|
+
context.model.context.flowSettingsEnabled = modelEnabled;
|
|
119
|
+
getOpenViewHandler()(context, { mode, size: 'medium' });
|
|
120
|
+
const config = open.mock.calls[0][0];
|
|
121
|
+
expect(config.inheritContext).toBe(false);
|
|
122
|
+
|
|
123
|
+
// Detached popups inherit the engine context, not the approval form's model context.
|
|
124
|
+
const popupContext = new FlowContext();
|
|
125
|
+
popupContext.addDelegate(engine.context);
|
|
126
|
+
popupContext.defineProperty('engine', { value: engine });
|
|
127
|
+
popupContext.defineProperty('view', {
|
|
128
|
+
value: { type: mode, inputArgs: config.inputArgs, Header: () => null, close: vi.fn() },
|
|
129
|
+
});
|
|
130
|
+
const { container } = render(
|
|
131
|
+
<FlowEngineProvider engine={engine}>
|
|
132
|
+
<FlowViewContextProvider context={popupContext}>{config.content()}</FlowViewContextProvider>
|
|
133
|
+
</FlowEngineProvider>,
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
await waitFor(() => expect(container.querySelector('.nb-block-grid')).toBeInTheDocument());
|
|
137
|
+
if (modelEnabled) {
|
|
138
|
+
expect(screen.getByRole('button', { name: /Add block/ })).toBeInTheDocument();
|
|
139
|
+
expect(save).toHaveBeenCalledOnce();
|
|
140
|
+
} else {
|
|
141
|
+
expect(screen.queryByRole('button', { name: /Add block/ })).not.toBeInTheDocument();
|
|
142
|
+
expect(save).not.toHaveBeenCalled();
|
|
143
|
+
}
|
|
144
|
+
expect(load).toHaveBeenCalledWith(
|
|
145
|
+
expect.objectContaining({ use: 'BlockGridModel', parentId: context.model.uid }),
|
|
146
|
+
{ delegateToParent: false, delegate: popupContext, skipSave: !modelEnabled },
|
|
147
|
+
);
|
|
148
|
+
expect(engine.context.flowSettingsEnabled).toBe(globalEnabled);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('passes the parent record reference when the parent record has a primary key', () => {
|
|
153
|
+
const { context, open } = createContext({ id: 1, nickname: 'Saved user' });
|
|
154
|
+
|
|
155
|
+
getOpenViewHandler()(context, { mode: 'drawer', size: 'medium' });
|
|
156
|
+
|
|
157
|
+
expect(open).toHaveBeenCalledOnce();
|
|
158
|
+
const { inputArgs } = open.mock.calls[0][0];
|
|
159
|
+
expect(inputArgs).toMatchObject({
|
|
160
|
+
scene: 'create',
|
|
161
|
+
collectionName: 'orgs',
|
|
162
|
+
associationName: 'users.orgs',
|
|
163
|
+
sourceId: 1,
|
|
164
|
+
});
|
|
165
|
+
expect(inputArgs).not.toHaveProperty('sourceAssociationName');
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('only passes sourceAssociationName when the parent record has no primary key yet', () => {
|
|
169
|
+
const { context, open } = createContext({});
|
|
170
|
+
|
|
171
|
+
getOpenViewHandler()(context, { mode: 'drawer', size: 'medium' });
|
|
172
|
+
|
|
173
|
+
expect(open).toHaveBeenCalledOnce();
|
|
174
|
+
const { inputArgs } = open.mock.calls[0][0];
|
|
175
|
+
expect(inputArgs).toMatchObject({
|
|
176
|
+
scene: 'create',
|
|
177
|
+
collectionName: 'orgs',
|
|
178
|
+
sourceAssociationName: 'users.orgs',
|
|
179
|
+
});
|
|
180
|
+
expect(inputArgs).not.toHaveProperty('associationName');
|
|
181
|
+
expect(inputArgs).not.toHaveProperty('sourceId');
|
|
182
|
+
});
|
|
183
|
+
});
|
package/src/flow/models/fields/AssociationFieldModel/__tests__/associationSelect.search.test.ts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
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 { CollectionField } from '@nocobase/flow-engine';
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
|
+
import '../../../index';
|
|
13
|
+
import { createTypedFilterable } from '../../../../../collection-manager/filter-operators';
|
|
14
|
+
import { CascadeSelectInnerFieldModel } from '../CascadeSelectFieldModel';
|
|
15
|
+
import { RecordSelectFieldModel } from '../RecordSelectFieldModel';
|
|
16
|
+
|
|
17
|
+
function createContext(fieldOptions: Record<string, unknown>, searchText = ' 3 ') {
|
|
18
|
+
const field = new CollectionField({ name: 'display_name', ...fieldOptions });
|
|
19
|
+
const filterable = createTypedFilterable<{ dataType?: string }>(
|
|
20
|
+
[
|
|
21
|
+
{ types: ['boolean'], operators: 'boolean' },
|
|
22
|
+
{ types: ['string'], operators: 'string' },
|
|
23
|
+
{ types: ['integer'], operators: 'number' },
|
|
24
|
+
],
|
|
25
|
+
(meta) => meta.dataType || 'string',
|
|
26
|
+
);
|
|
27
|
+
vi.spyOn(field, 'getInterfaceOptions').mockReturnValue({ filterable });
|
|
28
|
+
const records = [{ id: 3, display_name: '3 Jackson Hughes' }];
|
|
29
|
+
const resource = {
|
|
30
|
+
setPage: vi.fn(),
|
|
31
|
+
addFilterGroup: vi.fn(),
|
|
32
|
+
removeFilterGroup: vi.fn(),
|
|
33
|
+
refresh: vi.fn().mockResolvedValue(undefined),
|
|
34
|
+
getData: vi.fn(() => records),
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
field,
|
|
38
|
+
inputArgs: { searchText },
|
|
39
|
+
model: {
|
|
40
|
+
collectionField: { targetCollection: { getField: vi.fn(() => field) } },
|
|
41
|
+
props: { fieldNames: { label: 'display_name', value: 'id' } },
|
|
42
|
+
resource,
|
|
43
|
+
setDataSource: vi.fn(),
|
|
44
|
+
setProps: vi.fn(),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe.each([
|
|
50
|
+
{ name: 'RecordSelect', model: RecordSelectFieldModel, searchGroup: '__search__display_name' },
|
|
51
|
+
{ name: 'CascadeSelect', model: CascadeSelectInnerFieldModel, searchGroup: 'display_name' },
|
|
52
|
+
])('$name search', ({ model, searchGroup }) => {
|
|
53
|
+
beforeEach(() => vi.useFakeTimers());
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
vi.clearAllTimers();
|
|
56
|
+
vi.useRealTimers();
|
|
57
|
+
vi.restoreAllMocks();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
async function search(ctx: ReturnType<typeof createContext>) {
|
|
61
|
+
const handler = model.globalFlowRegistry.getFlow('searchSettings').getStep('step1').serialize().handler as (
|
|
62
|
+
ctx: ReturnType<typeof createContext>,
|
|
63
|
+
params: Record<string, unknown>,
|
|
64
|
+
) => unknown;
|
|
65
|
+
handler(ctx, {});
|
|
66
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
it.each([{ type: 'formula', dataType: 'string' }, { type: 'string' }])(
|
|
70
|
+
'uses partial text matching for $type labels',
|
|
71
|
+
async (options) => {
|
|
72
|
+
const ctx = createContext(options);
|
|
73
|
+
|
|
74
|
+
await search(ctx);
|
|
75
|
+
|
|
76
|
+
expect(ctx.model.resource.addFilterGroup).toHaveBeenCalledWith(searchGroup, { 'display_name.$includes': '3' });
|
|
77
|
+
expect(ctx.model.resource.setPage).toHaveBeenCalledWith(1);
|
|
78
|
+
expect(ctx.model.resource.refresh).toHaveBeenCalledOnce();
|
|
79
|
+
expect(ctx.model.setDataSource).toHaveBeenCalledWith([
|
|
80
|
+
expect.objectContaining({ display_name: '3 Jackson Hughes' }),
|
|
81
|
+
]);
|
|
82
|
+
expect(ctx.model.setProps).toHaveBeenCalledWith({ searchText: '3' });
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
it('preserves the interface operator for non-string labels', async () => {
|
|
87
|
+
const ctx = createContext({ type: 'integer' });
|
|
88
|
+
vi.spyOn(ctx.field, 'getInterfaceOptions').mockReturnValue({ filterable: { operators: [{ value: '$eq' }] } });
|
|
89
|
+
|
|
90
|
+
await search(ctx);
|
|
91
|
+
|
|
92
|
+
expect(ctx.model.resource.addFilterGroup).toHaveBeenCalledWith(searchGroup, { 'display_name.$eq': '3' });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('falls back to partial text matching without an interface', async () => {
|
|
96
|
+
const ctx = createContext({ type: 'string' });
|
|
97
|
+
vi.spyOn(ctx.field, 'getInterfaceOptions').mockReturnValue(undefined);
|
|
98
|
+
|
|
99
|
+
await search(ctx);
|
|
100
|
+
|
|
101
|
+
expect(ctx.model.resource.addFilterGroup).toHaveBeenCalledWith(searchGroup, { 'display_name.$includes': '3' });
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('removes the search filter when the search is cleared', async () => {
|
|
105
|
+
const ctx = createContext({ type: 'formula', dataType: 'string' }, ' ');
|
|
106
|
+
|
|
107
|
+
await search(ctx);
|
|
108
|
+
|
|
109
|
+
expect(ctx.model.resource.removeFilterGroup).toHaveBeenCalledWith(searchGroup);
|
|
110
|
+
expect(ctx.model.resource.addFilterGroup).not.toHaveBeenCalled();
|
|
111
|
+
expect(ctx.model.resource.refresh).toHaveBeenCalledOnce();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -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']);
|