@nocobase/client-v2 2.2.0-beta.3 → 2.2.0-beta.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/RouteRepository.d.ts +21 -6
- package/es/components/form/JsonTextArea.d.ts +2 -1
- package/es/components/form/VariableJsonTextArea.d.ts +19 -0
- package/es/components/form/index.d.ts +1 -0
- package/es/components/index.d.ts +1 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.d.ts +2 -1
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +2 -0
- package/es/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.d.ts +3 -2
- package/es/flow/admin-shell/admin-layout/AdminLayoutSlotModels.d.ts +4 -0
- package/es/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.d.ts +6 -0
- package/es/flow/components/FieldAssignRulesEditor.d.ts +3 -1
- package/es/flow/models/base/GridModel.d.ts +1 -1
- package/es/flow/models/blocks/filter-form/FilterFormBlockModel.d.ts +5 -0
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +4 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/runtime.d.ts +21 -0
- package/es/flow/models/blocks/form/value-runtime/types.d.ts +2 -2
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +1 -0
- package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
- package/es/flow-compat/FieldValidation.d.ts +2 -1
- package/es/index.d.ts +2 -1
- package/es/index.mjs +167 -121
- package/lib/index.js +167 -121
- package/package.json +7 -7
- package/src/RouteRepository.ts +126 -24
- package/src/__tests__/RouteRepository.test.ts +216 -0
- package/src/__tests__/exports.test.ts +16 -0
- package/src/__tests__/settings-center.test.tsx +33 -0
- package/src/components/form/JsonTextArea.tsx +21 -11
- package/src/components/form/VariableJsonTextArea.tsx +175 -0
- package/src/components/form/__tests__/JsonTextArea.test.tsx +43 -0
- package/src/components/form/__tests__/VariableJsonTextArea.test.tsx +86 -0
- package/src/components/form/index.tsx +1 -0
- package/src/components/index.ts +1 -0
- package/src/flow/__tests__/FlowRoute.test.tsx +443 -5
- package/src/flow/actions/__tests__/fieldLinkageRules.scopeDepth.test.ts +150 -1
- package/src/flow/actions/__tests__/linkageAssignField.legacy.test.ts +135 -0
- package/src/flow/actions/__tests__/linkageRules.hiddenSync.restore.test.ts +19 -5
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +92 -0
- package/src/flow/actions/customVariable.tsx +1 -2
- package/src/flow/actions/dateTimeFormat.tsx +42 -28
- package/src/flow/actions/linkageRules.tsx +122 -34
- package/src/flow/actions/runjs.tsx +2 -14
- package/src/flow/actions/validation.tsx +62 -30
- package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +33 -10
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +310 -0
- package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +44 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuFlowUtils.ts +5 -3
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuModels.tsx +61 -9
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +71 -8
- package/src/flow/admin-shell/admin-layout/AdminLayoutModel.tsx +1 -1
- package/src/flow/admin-shell/admin-layout/AdminLayoutSlotModels.tsx +21 -8
- package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +188 -0
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +99 -0
- package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +166 -2
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.test.ts +20 -0
- package/src/flow/admin-shell/admin-layout/resolveAdminRouteRuntimeTarget.ts +33 -5
- package/src/flow/components/DefaultValue.tsx +1 -2
- package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
- package/src/flow/components/FieldAssignRulesEditor.tsx +62 -12
- package/src/flow/components/FlowRoute.tsx +56 -11
- package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
- package/src/flow/components/__tests__/FieldAssignRulesEditor.test.tsx +81 -4
- package/src/flow/components/code-editor/__tests__/useCodeRunner.test.tsx +2 -17
- package/src/flow/components/code-editor/hooks/useCodeRunner.ts +2 -14
- package/src/flow/components/code-editor/runjsDiagnostics.ts +8 -45
- package/src/flow/flows/editMarkdownFlow.tsx +1 -1
- package/src/flow/index.ts +1 -1
- package/src/flow/internal/utils/operatorSchemaHelper.ts +15 -1
- package/src/flow/models/actions/JSActionModel.tsx +2 -7
- package/src/flow/models/actions/JSCollectionActionModel.tsx +2 -7
- package/src/flow/models/actions/JSItemActionModel.tsx +2 -7
- package/src/flow/models/actions/JSRecordActionModel.tsx +2 -7
- package/src/flow/models/base/ActionModel.tsx +12 -1
- package/src/flow/models/base/GridModel.tsx +38 -7
- package/src/flow/models/base/PageModel/PageModel.tsx +4 -1
- package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +28 -4
- package/src/flow/models/base/__tests__/ActionModel.test.ts +83 -0
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +239 -1
- package/src/flow/models/base/__tests__/transformRowsToSingleColumn.test.ts +48 -0
- package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +31 -3
- package/src/flow/models/blocks/filter-form/FilterFormJSActionModel.tsx +2 -7
- package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +77 -0
- package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
- package/src/flow/models/blocks/form/EditFormModel.tsx +1 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +7 -0
- package/src/flow/models/blocks/form/JSFormActionModel.tsx +2 -7
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +17 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +880 -102
- package/src/flow/models/blocks/form/value-runtime/rules.ts +445 -13
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +257 -13
- package/src/flow/models/blocks/form/value-runtime/types.ts +2 -2
- package/src/flow/models/blocks/js-block/JSBlock.tsx +2 -7
- package/src/flow/models/blocks/table/JSColumnModel.tsx +1 -9
- package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +1 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +13 -3
- package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
- package/src/flow/models/blocks/table/__tests__/TableActionsColumnModel.test.tsx +54 -0
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.mobileSettingsButtons.test.tsx +78 -0
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
- package/src/flow/models/fields/AssociationFieldModel/AssociationFieldModel.tsx +1 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +29 -6
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +24 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +13 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +12 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableFieldModel.reset.test.ts +180 -0
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/index.tsx +6 -4
- package/src/flow/models/fields/AssociationFieldModel/__tests__/AssociationFieldModel.updateAssociation.test.ts +72 -0
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +23 -0
- package/src/flow/models/fields/ClickableFieldModel.tsx +5 -0
- package/src/flow/models/fields/DisplayDateTimeFieldModel.tsx +29 -1
- package/src/flow/models/fields/InputFieldModel.tsx +14 -22
- package/src/flow/models/fields/JSEditableFieldModel.tsx +2 -11
- package/src/flow/models/fields/JSFieldModel.tsx +2 -7
- package/src/flow/models/fields/JSItemModel.tsx +2 -14
- package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
- package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +17 -0
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
- package/src/flow/models/topbar/TopbarActionModel.tsx +93 -10
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
- package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
- package/src/flow-compat/FieldValidation.tsx +122 -60
- package/src/flow-compat/Popover.tsx +43 -4
- package/src/flow-compat/__tests__/Popover.test.tsx +34 -0
- package/src/index.ts +8 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +41 -1
- package/src/settings-center/AdminSettingsLayout.tsx +3 -0
- package/src/settings-center/plugin-manager/index.tsx +3 -0
|
@@ -19,11 +19,16 @@ const capturedDroppableUids: string[] = [];
|
|
|
19
19
|
const capturedRendererProps: any[] = [];
|
|
20
20
|
let latestOnDragEnd: ((event: any) => void) | undefined;
|
|
21
21
|
const capturedDndProviders: Array<{ persist: boolean }> = [];
|
|
22
|
+
const capturedAddSubModelButtonProps: any[] = [];
|
|
22
23
|
|
|
23
24
|
vi.mock('@nocobase/flow-engine', async () => {
|
|
24
25
|
const actual = await vi.importActual<any>('@nocobase/flow-engine');
|
|
25
26
|
return {
|
|
26
27
|
...actual,
|
|
28
|
+
AddSubModelButton: (props) => {
|
|
29
|
+
capturedAddSubModelButtonProps.push(props);
|
|
30
|
+
return <button data-testid="add-sub-model-button">{props.children}</button>;
|
|
31
|
+
},
|
|
27
32
|
DndProvider: ({ children, onDragEnd, persist = true, ...restProps }) => {
|
|
28
33
|
const engine = actual.useFlowEngine();
|
|
29
34
|
const handleDragEnd = (event: any) => {
|
|
@@ -50,6 +55,17 @@ vi.mock('@nocobase/flow-engine', async () => {
|
|
|
50
55
|
const ActualFlowModelRenderer = actual.FlowModelRenderer;
|
|
51
56
|
return <ActualFlowModelRenderer {...props} />;
|
|
52
57
|
},
|
|
58
|
+
FlowsFloatContextMenu: ({ children, extraToolbarItems = [], model }) => {
|
|
59
|
+
return (
|
|
60
|
+
<div data-test-flow-context-menu>
|
|
61
|
+
{children}
|
|
62
|
+
{extraToolbarItems.map((item) => {
|
|
63
|
+
const Component = item.component;
|
|
64
|
+
return <Component key={item.key} model={model} />;
|
|
65
|
+
})}
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
},
|
|
53
69
|
};
|
|
54
70
|
});
|
|
55
71
|
|
|
@@ -165,6 +181,7 @@ describe('TableActionsColumnModel: drag integration', () => {
|
|
|
165
181
|
capturedDroppableUids.length = 0;
|
|
166
182
|
capturedRendererProps.length = 0;
|
|
167
183
|
capturedDndProviders.length = 0;
|
|
184
|
+
capturedAddSubModelButtonProps.length = 0;
|
|
168
185
|
latestOnDragEnd = undefined;
|
|
169
186
|
});
|
|
170
187
|
|
|
@@ -177,6 +194,7 @@ describe('TableActionsColumnModel: drag integration', () => {
|
|
|
177
194
|
props: { width: 200, title: 'Actions' },
|
|
178
195
|
subModels: { actions: [{ use: 'TestViewActionModel' }, { use: 'TestViewActionModel' }] },
|
|
179
196
|
});
|
|
197
|
+
actionsCol.context.defineMethod('getModelClassName', (className: string) => className);
|
|
180
198
|
|
|
181
199
|
const colProps = actionsCol.getColumnProps();
|
|
182
200
|
const record = { id: 1, phone: '000000' } as any;
|
|
@@ -279,4 +297,40 @@ describe('TableActionsColumnModel: drag integration', () => {
|
|
|
279
297
|
|
|
280
298
|
expect(moveModelSpy).toHaveBeenCalledWith(firstUid, secondUid, { persist: true });
|
|
281
299
|
});
|
|
300
|
+
|
|
301
|
+
it('disables icon-only mode when adding row actions without icons', async () => {
|
|
302
|
+
const engine = new FlowEngine();
|
|
303
|
+
engine.registerModels({ TableActionsColumnModel, TestViewActionModel });
|
|
304
|
+
|
|
305
|
+
const actionsCol = engine.createModel<TableActionsColumnModel>({
|
|
306
|
+
use: 'TableActionsColumnModel',
|
|
307
|
+
props: { width: 200, title: 'Actions' },
|
|
308
|
+
});
|
|
309
|
+
actionsCol.context.defineMethod('getModelClassName', (className: string) => className);
|
|
310
|
+
|
|
311
|
+
render(
|
|
312
|
+
<FlowEngineProvider engine={engine}>
|
|
313
|
+
<ConfigProvider>
|
|
314
|
+
<App>{actionsCol.getColumnProps().title as React.ReactNode}</App>
|
|
315
|
+
</ConfigProvider>
|
|
316
|
+
</FlowEngineProvider>,
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const addButtonProps = capturedAddSubModelButtonProps.find(
|
|
320
|
+
(props) => props.subModelKey === 'actions' && props.subModelBaseClass === 'RecordActionGroupModel',
|
|
321
|
+
);
|
|
322
|
+
expect(addButtonProps).toBeDefined();
|
|
323
|
+
expect(addButtonProps.afterSubModelInit).toBeTypeOf('function');
|
|
324
|
+
|
|
325
|
+
const actionModel = {
|
|
326
|
+
setStepParams: vi.fn(),
|
|
327
|
+
};
|
|
328
|
+
await addButtonProps.afterSubModelInit(actionModel);
|
|
329
|
+
|
|
330
|
+
expect(actionModel.setStepParams).toHaveBeenCalledWith('buttonSettings', 'general', {
|
|
331
|
+
type: 'link',
|
|
332
|
+
icon: null,
|
|
333
|
+
iconOnly: false,
|
|
334
|
+
});
|
|
335
|
+
});
|
|
282
336
|
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { FlowEngine } from '@nocobase/flow-engine';
|
|
11
|
+
import '@nocobase/client';
|
|
12
|
+
import React from 'react';
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import { TableBlockModel } from '../TableBlockModel';
|
|
15
|
+
|
|
16
|
+
function createTableModel(options: { isMobileLayout?: boolean; flowSettingsEnabled?: boolean } = {}) {
|
|
17
|
+
const engine = new FlowEngine();
|
|
18
|
+
engine.registerModels({ TableBlockModel });
|
|
19
|
+
engine.context.defineProperty('isMobileLayout', { value: !!options.isMobileLayout });
|
|
20
|
+
engine.context.defineProperty('flowSettingsEnabled', { value: !!options.flowSettingsEnabled });
|
|
21
|
+
|
|
22
|
+
const ds = engine.dataSourceManager.getDataSource('main');
|
|
23
|
+
ds.addCollection({
|
|
24
|
+
name: 'posts',
|
|
25
|
+
filterTargetKey: 'id',
|
|
26
|
+
fields: [
|
|
27
|
+
{ name: 'id', type: 'integer', interface: 'number' },
|
|
28
|
+
{ name: 'title', type: 'string', interface: 'input' },
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return engine.createModel<TableBlockModel>({
|
|
33
|
+
uid: `posts-table-${options.isMobileLayout ? 'mobile' : 'desktop'}`,
|
|
34
|
+
use: 'TableBlockModel',
|
|
35
|
+
stepParams: {
|
|
36
|
+
resourceSettings: {
|
|
37
|
+
init: {
|
|
38
|
+
dataSourceKey: 'main',
|
|
39
|
+
collectionName: 'posts',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getSettingsButton(element: React.ReactElement) {
|
|
47
|
+
return element.props.children as React.ReactElement;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('TableBlockModel mobile settings buttons', () => {
|
|
51
|
+
it('renders table actions and fields settings as icon-only buttons in mobile layouts', () => {
|
|
52
|
+
const model = createTableModel({ isMobileLayout: true, flowSettingsEnabled: true });
|
|
53
|
+
const actionButton = getSettingsButton(model.renderConfigureActions() as React.ReactElement);
|
|
54
|
+
const addColumn = model.getColumns().find((column) => column.key === 'addColumn');
|
|
55
|
+
const fieldsButton = getSettingsButton(
|
|
56
|
+
(addColumn?.title as React.ReactElement).type({ model }) as React.ReactElement,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(actionButton.props.children).toBeNull();
|
|
60
|
+
expect(actionButton.props['aria-label']).toBe('Actions');
|
|
61
|
+
expect(fieldsButton.props.children).toBeNull();
|
|
62
|
+
expect(fieldsButton.props['aria-label']).toBe('Fields');
|
|
63
|
+
expect(addColumn?.width).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('keeps table actions and fields settings labels outside mobile layouts', () => {
|
|
67
|
+
const model = createTableModel({ flowSettingsEnabled: true });
|
|
68
|
+
const actionButton = getSettingsButton(model.renderConfigureActions() as React.ReactElement);
|
|
69
|
+
const addColumn = model.getColumns().find((column) => column.key === 'addColumn');
|
|
70
|
+
const fieldsButton = getSettingsButton(
|
|
71
|
+
(addColumn?.title as React.ReactElement).type({ model }) as React.ReactElement,
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
expect(actionButton.props.children).toBe('Actions');
|
|
75
|
+
expect(fieldsButton.props.children).toBe('Fields');
|
|
76
|
+
expect(addColumn?.width).toBe(100);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -130,9 +130,14 @@ describe('TableColumnModel sorter settings', () => {
|
|
|
130
130
|
const titleFieldStep = model.getFlow('tableColumnSettings')?.steps?.fieldNames as any;
|
|
131
131
|
const setStepParams = vi.fn();
|
|
132
132
|
const setProps = vi.fn();
|
|
133
|
+
const setFieldProps = vi.fn();
|
|
133
134
|
const dispatchEvent = vi.fn();
|
|
135
|
+
const saveFieldModel = vi.fn();
|
|
134
136
|
const targetCollectionField = {
|
|
135
|
-
getComponentProps: () => ({
|
|
137
|
+
getComponentProps: () => ({
|
|
138
|
+
format: 'hh:mm:ss a',
|
|
139
|
+
timeFormat: 'hh:mm:ss a',
|
|
140
|
+
}),
|
|
136
141
|
};
|
|
137
142
|
|
|
138
143
|
await titleFieldStep.beforeParamsSave(
|
|
@@ -156,8 +161,10 @@ describe('TableColumnModel sorter settings', () => {
|
|
|
156
161
|
subModels: {
|
|
157
162
|
field: {
|
|
158
163
|
use: 'DisplayTextFieldModel',
|
|
164
|
+
setProps: setFieldProps,
|
|
159
165
|
setStepParams,
|
|
160
166
|
dispatchEvent,
|
|
167
|
+
save: saveFieldModel,
|
|
161
168
|
},
|
|
162
169
|
},
|
|
163
170
|
setStepParams,
|
|
@@ -169,6 +176,155 @@ describe('TableColumnModel sorter settings', () => {
|
|
|
169
176
|
);
|
|
170
177
|
|
|
171
178
|
expect(setStepParams).toHaveBeenCalledWith('tableColumnSettings', 'model', { use: 'DisplayTextFieldModel' });
|
|
179
|
+
expect(setFieldProps).toHaveBeenCalledWith(
|
|
180
|
+
expect.objectContaining({
|
|
181
|
+
format: 'hh:mm:ss a',
|
|
182
|
+
timeFormat: 'hh:mm:ss a',
|
|
183
|
+
titleField: 'code',
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
expect(setProps).toHaveBeenCalledWith(
|
|
187
|
+
expect.objectContaining({
|
|
188
|
+
format: 'hh:mm:ss a',
|
|
189
|
+
timeFormat: 'hh:mm:ss a',
|
|
190
|
+
titleField: 'code',
|
|
191
|
+
}),
|
|
192
|
+
);
|
|
193
|
+
expect(saveFieldModel).toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('clears stale datetime display props when association title field changes to date only', async () => {
|
|
197
|
+
const engine = new FlowEngine();
|
|
198
|
+
const model = new TableColumnModel({ uid: 'table-column-title-field-date-only', flowEngine: engine } as any);
|
|
199
|
+
const titleFieldStep = model.getFlow('tableColumnSettings')?.steps?.fieldNames as any;
|
|
200
|
+
const setStepParams = vi.fn();
|
|
201
|
+
const setProps = vi.fn();
|
|
202
|
+
const setFieldProps = vi.fn();
|
|
203
|
+
const targetCollectionField = {
|
|
204
|
+
getComponentProps: () => ({
|
|
205
|
+
dateOnly: true,
|
|
206
|
+
showTime: false,
|
|
207
|
+
}),
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
await titleFieldStep.beforeParamsSave(
|
|
211
|
+
{
|
|
212
|
+
collectionField: {
|
|
213
|
+
isAssociationField: () => true,
|
|
214
|
+
targetCollection: {
|
|
215
|
+
name: 'departments',
|
|
216
|
+
getField: () => targetCollectionField,
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
model: {
|
|
220
|
+
collectionField: {
|
|
221
|
+
dataSourceKey: 'main',
|
|
222
|
+
},
|
|
223
|
+
constructor: {
|
|
224
|
+
getDefaultBindingByField: () => ({
|
|
225
|
+
modelName: 'DisplayDateTimeFieldModel',
|
|
226
|
+
}),
|
|
227
|
+
},
|
|
228
|
+
subModels: {
|
|
229
|
+
field: {
|
|
230
|
+
use: 'DisplayDateTimeFieldModel',
|
|
231
|
+
setProps: setFieldProps,
|
|
232
|
+
setStepParams,
|
|
233
|
+
dispatchEvent: vi.fn(),
|
|
234
|
+
save: vi.fn(),
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
setStepParams,
|
|
238
|
+
setProps,
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
{ label: 'dateOnly' },
|
|
242
|
+
{ label: 'time' },
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
expect(setFieldProps).toHaveBeenCalledWith(
|
|
246
|
+
expect.objectContaining({
|
|
247
|
+
dateOnly: true,
|
|
248
|
+
format: undefined,
|
|
249
|
+
showTime: false,
|
|
250
|
+
timeFormat: undefined,
|
|
251
|
+
titleField: 'dateOnly',
|
|
252
|
+
}),
|
|
253
|
+
);
|
|
254
|
+
expect(setProps).toHaveBeenCalledWith(
|
|
255
|
+
expect.objectContaining({
|
|
256
|
+
dateOnly: true,
|
|
257
|
+
format: undefined,
|
|
258
|
+
showTime: false,
|
|
259
|
+
timeFormat: undefined,
|
|
260
|
+
titleField: 'dateOnly',
|
|
261
|
+
}),
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('keeps saved association title datetime format when table column initializes again', async () => {
|
|
266
|
+
const engine = new FlowEngine();
|
|
267
|
+
const model = new TableColumnModel({
|
|
268
|
+
uid: 'table-column-title-field-saved-datetime-format',
|
|
269
|
+
flowEngine: engine,
|
|
270
|
+
} as any);
|
|
271
|
+
const initStep = model.getFlow('tableColumnSettings')?.steps?.init as any;
|
|
272
|
+
const setProps = vi.fn();
|
|
273
|
+
const targetCollectionField = {
|
|
274
|
+
getComponentProps: () => ({
|
|
275
|
+
dateFormat: 'YYYY-MM-DD',
|
|
276
|
+
format: 'YYYY-MM-DD HH:mm:ss',
|
|
277
|
+
showTime: true,
|
|
278
|
+
timeFormat: 'HH:mm:ss',
|
|
279
|
+
}),
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
await initStep.handler({
|
|
283
|
+
model: {
|
|
284
|
+
context: {
|
|
285
|
+
collectionField: {
|
|
286
|
+
title: 'Shipments',
|
|
287
|
+
name: 'shipments',
|
|
288
|
+
isAssociationField: () => true,
|
|
289
|
+
getComponentProps: () => ({
|
|
290
|
+
fieldNames: {
|
|
291
|
+
label: 'shipmentsDatetime',
|
|
292
|
+
},
|
|
293
|
+
}),
|
|
294
|
+
targetCollection: {
|
|
295
|
+
getField: () => targetCollectionField,
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
props: {
|
|
300
|
+
titleField: 'shipmentsDatetime',
|
|
301
|
+
},
|
|
302
|
+
subModels: {
|
|
303
|
+
field: {
|
|
304
|
+
getStepParams: (flowKey, stepKey) =>
|
|
305
|
+
flowKey === 'datetimeSettings' && stepKey === 'dateFormat'
|
|
306
|
+
? {
|
|
307
|
+
picker: 'date',
|
|
308
|
+
dateFormat: 'YYYY-MM-DD',
|
|
309
|
+
showTime: true,
|
|
310
|
+
timeFormat: 'h:mm a',
|
|
311
|
+
}
|
|
312
|
+
: undefined,
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
applySubModelsBeforeRenderFlows: vi.fn(),
|
|
316
|
+
setProps,
|
|
317
|
+
},
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
expect(setProps).toHaveBeenCalledWith(
|
|
321
|
+
expect.objectContaining({
|
|
322
|
+
dateFormat: 'YYYY-MM-DD',
|
|
323
|
+
format: 'YYYY-MM-DD h:mm a',
|
|
324
|
+
showTime: true,
|
|
325
|
+
timeFormat: 'h:mm a',
|
|
326
|
+
}),
|
|
327
|
+
);
|
|
172
328
|
});
|
|
173
329
|
|
|
174
330
|
it('does not update field component setting when title field refresh fails', async () => {
|
|
@@ -203,8 +359,10 @@ describe('TableColumnModel sorter settings', () => {
|
|
|
203
359
|
subModels: {
|
|
204
360
|
field: {
|
|
205
361
|
use: 'DisplayTextFieldModel',
|
|
362
|
+
setProps: vi.fn(),
|
|
206
363
|
setStepParams: vi.fn(),
|
|
207
364
|
dispatchEvent: vi.fn().mockRejectedValue(new Error('beforeRender failed')),
|
|
365
|
+
save: vi.fn(),
|
|
208
366
|
},
|
|
209
367
|
},
|
|
210
368
|
setStepParams,
|
|
@@ -11,7 +11,7 @@ import { FormItemModel } from '../../blocks/form/FormItemModel';
|
|
|
11
11
|
import { FieldModel } from '../../base/FieldModel';
|
|
12
12
|
export class AssociationFieldModel extends FieldModel {
|
|
13
13
|
operator = '$eq';
|
|
14
|
-
updateAssociation =
|
|
14
|
+
updateAssociation = false;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
AssociationFieldModel.registerFlow({
|
|
@@ -203,6 +203,8 @@ const DisplayTable = (props) => {
|
|
|
203
203
|
onSelectExitRecordClick,
|
|
204
204
|
resetPage,
|
|
205
205
|
allowCreate,
|
|
206
|
+
formValuesChangeEmitter,
|
|
207
|
+
onResetFieldValue,
|
|
206
208
|
} = props;
|
|
207
209
|
const [currentPage, setCurrentPage] = useState(1);
|
|
208
210
|
const [currentPageSize, setCurrentPageSize] = useState(pageSize);
|
|
@@ -223,6 +225,18 @@ const DisplayTable = (props) => {
|
|
|
223
225
|
resetPage && setCurrentPage(1);
|
|
224
226
|
}, [resetPage]);
|
|
225
227
|
|
|
228
|
+
useEffect(() => {
|
|
229
|
+
if (!formValuesChangeEmitter?.on || !formValuesChangeEmitter?.off || !onResetFieldValue) return;
|
|
230
|
+
const listener = () => {
|
|
231
|
+
onResetFieldValue();
|
|
232
|
+
setTableData([]);
|
|
233
|
+
};
|
|
234
|
+
formValuesChangeEmitter.on('onFieldReset', listener);
|
|
235
|
+
return () => {
|
|
236
|
+
formValuesChangeEmitter.off('onFieldReset', listener);
|
|
237
|
+
};
|
|
238
|
+
}, [formValuesChangeEmitter, onResetFieldValue]);
|
|
239
|
+
|
|
226
240
|
const pagination = useMemo(() => {
|
|
227
241
|
return {
|
|
228
242
|
current: currentPage, // 当前页码
|
|
@@ -237,7 +251,7 @@ const DisplayTable = (props) => {
|
|
|
237
251
|
return t('Total {{count}} items', { count: total });
|
|
238
252
|
},
|
|
239
253
|
} as any;
|
|
240
|
-
}, [currentPage, currentPageSize, tableData]);
|
|
254
|
+
}, [currentPage, currentPageSize, tableData, t]);
|
|
241
255
|
|
|
242
256
|
const columns = useMemo(() => {
|
|
243
257
|
const cols = adjustColumnOrder(
|
|
@@ -421,10 +435,6 @@ export class PopupSubTableFieldModel extends AssociationFieldModel {
|
|
|
421
435
|
currentPageSize,
|
|
422
436
|
});
|
|
423
437
|
};
|
|
424
|
-
// 监听表单reset
|
|
425
|
-
this.context.blockModel.emitter.on('onFieldReset', () => {
|
|
426
|
-
this.props?.onChange([]);
|
|
427
|
-
});
|
|
428
438
|
}
|
|
429
439
|
}
|
|
430
440
|
|
|
@@ -457,8 +467,21 @@ export class PopupSubTableFieldModel extends AssociationFieldModel {
|
|
|
457
467
|
}
|
|
458
468
|
|
|
459
469
|
public render() {
|
|
470
|
+
const fieldPathArray = this.context.fieldPathArray ?? this.parent?.context?.fieldPathArray;
|
|
471
|
+
const onResetFieldValue = () => {
|
|
472
|
+
const value = [];
|
|
473
|
+
this.setProps({ value });
|
|
474
|
+
this.context.blockModel?.setFieldValue?.(fieldPathArray, value);
|
|
475
|
+
};
|
|
460
476
|
return (
|
|
461
|
-
<DisplayTable
|
|
477
|
+
<DisplayTable
|
|
478
|
+
{...this.props}
|
|
479
|
+
collection={this.collection}
|
|
480
|
+
baseColumns={this.getBaseColumns(this)}
|
|
481
|
+
model={this}
|
|
482
|
+
formValuesChangeEmitter={this.context.blockModel?.emitter}
|
|
483
|
+
onResetFieldValue={onResetFieldValue}
|
|
484
|
+
/>
|
|
462
485
|
);
|
|
463
486
|
}
|
|
464
487
|
}
|
|
@@ -99,6 +99,18 @@ export function collectAssociationHydrationCandidates(options: {
|
|
|
99
99
|
return candidates;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
export function getAssociationHydrationNamePath(model: any) {
|
|
103
|
+
return model?.context?.fieldPathArray ?? model?.context?.fieldPath ?? model?.props?.name;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function getAssociationHydrationSetterContext(model: any) {
|
|
107
|
+
if (typeof model?.context?.setFormValue === 'function') {
|
|
108
|
+
return model.context;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return model?.context?.blockModel?.context;
|
|
112
|
+
}
|
|
113
|
+
|
|
102
114
|
function markAssociationHydrationDone(statusMap: Map<string, HydrateStatus>, tkKey: string | null | undefined) {
|
|
103
115
|
if (!tkKey) return;
|
|
104
116
|
statusMap.set(tkKey, 'done');
|
|
@@ -245,8 +257,8 @@ const LazySelect = (props: Readonly<LazySelectProps>) => {
|
|
|
245
257
|
});
|
|
246
258
|
if (!candidates.length) return;
|
|
247
259
|
|
|
248
|
-
const namePath = model
|
|
249
|
-
const
|
|
260
|
+
const namePath = getAssociationHydrationNamePath(model);
|
|
261
|
+
const setterCtx: any = getAssociationHydrationSetterContext(model);
|
|
250
262
|
|
|
251
263
|
candidates.forEach(({ item, tk, tkKey }) => {
|
|
252
264
|
void (async () => {
|
|
@@ -268,8 +280,8 @@ const LazySelect = (props: Readonly<LazySelectProps>) => {
|
|
|
268
280
|
})
|
|
269
281
|
: merge;
|
|
270
282
|
|
|
271
|
-
if (
|
|
272
|
-
await
|
|
283
|
+
if (setterCtx && typeof setterCtx.setFormValue === 'function' && namePath != null) {
|
|
284
|
+
await setterCtx.setFormValue(namePath, nextValue, {
|
|
273
285
|
source: 'default',
|
|
274
286
|
markExplicit: false,
|
|
275
287
|
triggerEvent: false,
|
package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx
CHANGED
|
@@ -231,6 +231,20 @@ export function getLatestSubTableRowRecord(form: any, fieldIndex: unknown, fallb
|
|
|
231
231
|
return typeof latestRecord === 'undefined' ? fallbackRecord : latestRecord;
|
|
232
232
|
}
|
|
233
233
|
|
|
234
|
+
export function buildSubTableColumnNamePath(
|
|
235
|
+
fieldPath: Array<string | number>,
|
|
236
|
+
rowIdx: number,
|
|
237
|
+
namePath: string | number,
|
|
238
|
+
rowFieldIndex: unknown,
|
|
239
|
+
): Array<string | number> {
|
|
240
|
+
const currentRowPath = buildRowPathFromFieldIndex(rowFieldIndex);
|
|
241
|
+
if (currentRowPath?.length) {
|
|
242
|
+
return [...currentRowPath, namePath];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return [...fieldPath, rowIdx, namePath];
|
|
246
|
+
}
|
|
247
|
+
|
|
234
248
|
function shouldCommitImmediately(value: any) {
|
|
235
249
|
if (Array.isArray(value)) {
|
|
236
250
|
return true;
|
|
@@ -372,9 +386,18 @@ const MemoCell: React.FC<CellProps> = React.memo(
|
|
|
372
386
|
{parent.mapSubModels('field', (action: FieldModel) => {
|
|
373
387
|
const fieldPath = action.context.fieldPath.split('.');
|
|
374
388
|
const namePath = fieldPath.pop();
|
|
389
|
+
if (typeof namePath === 'undefined') {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
const fieldNamePath = buildSubTableColumnNamePath(fieldPath, rowIdx, namePath, rowFork?.context?.fieldIndex);
|
|
393
|
+
const formItemName = buildDynamicNamePath([...fieldPath, rowIdx, namePath], parentFieldIndex);
|
|
375
394
|
|
|
376
395
|
const fork: any = action.createFork({}, `${id}`);
|
|
377
396
|
fork.context.defineProperty('currentObject', { get: () => record });
|
|
397
|
+
fork.context.defineProperty('fieldPathArray', {
|
|
398
|
+
get: () => fieldNamePath,
|
|
399
|
+
cache: false,
|
|
400
|
+
});
|
|
378
401
|
if (rowFork) {
|
|
379
402
|
const itemOptions = rowFork.context.getPropertyOptions?.('item');
|
|
380
403
|
const { value: _value, ...itemOptionsWithoutValue } = (itemOptions || {}) as any;
|
|
@@ -415,7 +438,7 @@ const MemoCell: React.FC<CellProps> = React.memo(
|
|
|
415
438
|
<FormItem
|
|
416
439
|
{...parent.props}
|
|
417
440
|
key={id}
|
|
418
|
-
name={
|
|
441
|
+
name={formItemName}
|
|
419
442
|
style={{ marginBottom: 0 }}
|
|
420
443
|
showLabel={false}
|
|
421
444
|
disabled={
|
|
@@ -70,6 +70,7 @@ export function SubTableField(props) {
|
|
|
70
70
|
getCurrentValue,
|
|
71
71
|
fieldPathArray,
|
|
72
72
|
formValuesChangeEmitter,
|
|
73
|
+
onResetFieldValue,
|
|
73
74
|
} = props;
|
|
74
75
|
const [currentPage, setCurrentPage] = useState(1);
|
|
75
76
|
const [currentPageSize, setCurrentPageSize] = useState(pageSize);
|
|
@@ -97,6 +98,17 @@ export function SubTableField(props) {
|
|
|
97
98
|
formValuesChangeEmitter.off('formValuesChange', listener);
|
|
98
99
|
};
|
|
99
100
|
}, [fieldPathArray, formValuesChangeEmitter]);
|
|
101
|
+
useEffect(() => {
|
|
102
|
+
if (!formValuesChangeEmitter?.on || !formValuesChangeEmitter?.off || !onResetFieldValue) return;
|
|
103
|
+
const listener = () => {
|
|
104
|
+
onResetFieldValue();
|
|
105
|
+
forceRefresh((v) => v + 1);
|
|
106
|
+
};
|
|
107
|
+
formValuesChangeEmitter.on('onFieldReset', listener);
|
|
108
|
+
return () => {
|
|
109
|
+
formValuesChangeEmitter.off('onFieldReset', listener);
|
|
110
|
+
};
|
|
111
|
+
}, [formValuesChangeEmitter, onResetFieldValue]);
|
|
100
112
|
const applyValue = React.useCallback((nextValue: any) => onChange?.(normalizeSubTableRows(nextValue)), [onChange]);
|
|
101
113
|
const getLatestValue = React.useCallback(() => normalizeSubTableRows(getCurrentValue()), [getCurrentValue]);
|
|
102
114
|
useEffect(() => {
|
|
@@ -125,7 +137,7 @@ export function SubTableField(props) {
|
|
|
125
137
|
return t('Total {{count}} items', { count: total });
|
|
126
138
|
},
|
|
127
139
|
} as any;
|
|
128
|
-
}, [currentPage, currentPageSize, currentValue.length]);
|
|
140
|
+
}, [currentPage, currentPageSize, currentValue.length, t]);
|
|
129
141
|
|
|
130
142
|
// 新增一行
|
|
131
143
|
const handleAdd = () => {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
SubTableColumnModel,
|
|
16
16
|
getLatestSubTableRowRecord,
|
|
17
17
|
buildRowPathFromFieldIndex,
|
|
18
|
+
buildSubTableColumnNamePath,
|
|
18
19
|
isSubTableColumnConfiguredReadPretty,
|
|
19
20
|
getSubTableColumnTitleField,
|
|
20
21
|
getSubTableColumnReadPrettyFieldProps,
|
|
@@ -47,6 +48,17 @@ describe('SubTableColumnModel row record helpers', () => {
|
|
|
47
48
|
expect(buildRowPathFromFieldIndex(['users:1', 'roles:2'])).toEqual(['users', 1, 'roles', 2]);
|
|
48
49
|
});
|
|
49
50
|
|
|
51
|
+
it('builds absolute form item paths for nested sub-table columns', () => {
|
|
52
|
+
expect(buildSubTableColumnNamePath(['roles'], 0, 'name', ['roles:0'])).toEqual(['roles', 0, 'name']);
|
|
53
|
+
expect(buildSubTableColumnNamePath(['orders', 'lines'], 1, 'product', ['orders:0', 'lines:1'])).toEqual([
|
|
54
|
+
'orders',
|
|
55
|
+
0,
|
|
56
|
+
'lines',
|
|
57
|
+
1,
|
|
58
|
+
'product',
|
|
59
|
+
]);
|
|
60
|
+
});
|
|
61
|
+
|
|
50
62
|
it('prefers the latest row value from form over the fallback record', () => {
|
|
51
63
|
const form = {
|
|
52
64
|
getFieldValue: vi.fn((path: any) => {
|