@nocobase/client-v2 2.2.0-beta.2 → 2.2.0-beta.5
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/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +2 -0
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +6 -0
- package/es/flow/utils/dataScopeRowSnapshot.d.ts +59 -0
- package/es/flow/utils/dateTimeDisplayProps.d.ts +49 -0
- package/es/flow/utils/formValueDeps.d.ts +32 -0
- package/es/index.mjs +113 -93
- package/lib/index.js +104 -84
- package/package.json +7 -7
- package/src/components/AppComponents.tsx +19 -3
- package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +1022 -0
- package/src/flow/actions/__tests__/subFormFieldLinkageRules.inputArgs.test.ts +2 -2
- package/src/flow/actions/dateTimeFormat.tsx +42 -28
- package/src/flow/actions/linkageRules.tsx +1 -1
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +22 -130
- package/src/flow/admin-shell/admin-layout/HelpLite.tsx +1 -3
- package/src/flow/components/DynamicFlowsIcon.tsx +447 -126
- package/src/flow/components/__tests__/DynamicFlowsIcon.test.tsx +148 -2
- 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/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx +56 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +36 -3
- package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +159 -1
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +16 -4
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +141 -19
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/__tests__/SubTableColumnModel.rowRecord.test.ts +197 -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 +48 -2
- package/src/flow/models/fields/SelectFieldModel.tsx +6 -4
- package/src/flow/models/fields/TextareaFieldModel.tsx +49 -3
- package/src/flow/models/fields/__tests__/DisplayDateTimeFieldModel.test.tsx +96 -0
- package/src/flow/models/fields/__tests__/InputFieldModel.test.tsx +100 -1
- package/src/flow/models/fields/__tests__/SelectFieldModel.test.tsx +43 -0
- package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +100 -0
- package/src/flow/utils/__tests__/dateTimeFormat.test.ts +258 -0
- package/src/flow/utils/dataScopeFormValueClear.ts +218 -81
- package/src/flow/utils/dataScopeRowSnapshot.ts +616 -0
- package/src/flow/utils/dateTimeDisplayProps.ts +135 -0
- package/src/flow/utils/formValueDeps.ts +170 -0
- package/src/settings-center/AdminSettingsLayout.tsx +7 -2
|
@@ -10,19 +10,24 @@
|
|
|
10
10
|
import React from 'react';
|
|
11
11
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
12
12
|
import { render, screen, userEvent, waitFor } from '@nocobase/test/client';
|
|
13
|
-
import { ActionScene, FlowEngine, FlowModel, GLOBAL_EMBED_CONTAINER_ID } from '@nocobase/flow-engine';
|
|
13
|
+
import { ActionScene, FlowEngine, FlowModel, GLOBAL_EMBED_CONTAINER_ID, useFlowContext } from '@nocobase/flow-engine';
|
|
14
14
|
import { DynamicFlowsIcon } from '../DynamicFlowsIcon';
|
|
15
15
|
|
|
16
16
|
const mockState = vi.hoisted(() => ({
|
|
17
17
|
capturedSelectProps: [] as any[],
|
|
18
|
+
capturedTabsProps: [] as any[],
|
|
18
19
|
flowContextValue: undefined as any,
|
|
19
20
|
}));
|
|
20
21
|
|
|
21
22
|
vi.mock('@nocobase/flow-engine', async (importOriginal) => {
|
|
22
23
|
const actual = await importOriginal<typeof import('@nocobase/flow-engine')>();
|
|
24
|
+
const React = await import('react');
|
|
23
25
|
return {
|
|
24
26
|
...actual,
|
|
25
|
-
useFlowContext: () =>
|
|
27
|
+
useFlowContext: () => {
|
|
28
|
+
const context = React.useContext(actual.FlowReactContext);
|
|
29
|
+
return context?.view ? context : mockState.flowContextValue;
|
|
30
|
+
},
|
|
26
31
|
};
|
|
27
32
|
});
|
|
28
33
|
|
|
@@ -62,6 +67,33 @@ vi.mock('antd', async (importOriginal) => {
|
|
|
62
67
|
return <div data-testid={String(props.placeholder || 'select')} />;
|
|
63
68
|
},
|
|
64
69
|
Space: ({ children }: any) => <div>{children}</div>,
|
|
70
|
+
Tabs: (props: any) => {
|
|
71
|
+
mockState.capturedTabsProps.push(props);
|
|
72
|
+
const { items, activeKey, onChange, tabBarStyle } = props;
|
|
73
|
+
const currentKey = activeKey || items?.[0]?.key;
|
|
74
|
+
return (
|
|
75
|
+
<div data-testid="dynamic-flow-source-tabs">
|
|
76
|
+
<div role="tablist" style={tabBarStyle}>
|
|
77
|
+
{items?.map((item: any) => (
|
|
78
|
+
<button
|
|
79
|
+
key={item.key}
|
|
80
|
+
role="tab"
|
|
81
|
+
aria-selected={item.key === currentKey}
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={() => onChange?.(item.key)}
|
|
84
|
+
>
|
|
85
|
+
{item.label}
|
|
86
|
+
</button>
|
|
87
|
+
))}
|
|
88
|
+
</div>
|
|
89
|
+
{items?.map((item: any) => (
|
|
90
|
+
<div key={item.key} hidden={item.key !== currentKey}>
|
|
91
|
+
{item.children}
|
|
92
|
+
</div>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
},
|
|
65
97
|
Tooltip: ({ children }: any) => <>{children}</>,
|
|
66
98
|
};
|
|
67
99
|
});
|
|
@@ -83,6 +115,8 @@ const openDynamicFlowsEditor = async (model: FlowModel) => {
|
|
|
83
115
|
expect(trigger).not.toBeNull();
|
|
84
116
|
await userEvent.click(trigger as Element);
|
|
85
117
|
|
|
118
|
+
await waitFor(() => expect(model.context.viewer.embed).toHaveBeenCalled());
|
|
119
|
+
|
|
86
120
|
const embedCall = (model.context.viewer.embed as any).mock.calls.at(-1)?.[0];
|
|
87
121
|
expect(embedCall?.content).toBeTruthy();
|
|
88
122
|
|
|
@@ -163,6 +197,38 @@ const createModel = (options: { preventClose?: boolean; hiddenClose?: boolean; s
|
|
|
163
197
|
return model;
|
|
164
198
|
};
|
|
165
199
|
|
|
200
|
+
const createPeerModel = (model: FlowModel, uid: string, saveStepParams = vi.fn(async () => undefined)) => {
|
|
201
|
+
const PeerModel = model.constructor as typeof FlowModel;
|
|
202
|
+
const peer = new PeerModel({
|
|
203
|
+
uid,
|
|
204
|
+
use: PeerModel,
|
|
205
|
+
flowEngine: model.flowEngine,
|
|
206
|
+
stepParams: {},
|
|
207
|
+
subModels: {},
|
|
208
|
+
flowRegistry: {
|
|
209
|
+
flow1: {
|
|
210
|
+
title: 'Template event flow',
|
|
211
|
+
on: 'click',
|
|
212
|
+
steps: {
|
|
213
|
+
notifyStep: {
|
|
214
|
+
use: 'notify',
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
} as any);
|
|
220
|
+
|
|
221
|
+
peer.context.defineProperty('message', {
|
|
222
|
+
value: {
|
|
223
|
+
error: vi.fn(),
|
|
224
|
+
success: vi.fn(),
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
vi.spyOn(peer, 'saveStepParams').mockImplementation(saveStepParams);
|
|
228
|
+
|
|
229
|
+
return peer;
|
|
230
|
+
};
|
|
231
|
+
|
|
166
232
|
const createView = () => {
|
|
167
233
|
let destroyed = false;
|
|
168
234
|
let closingPromise: Promise<boolean | void> | undefined;
|
|
@@ -189,6 +255,9 @@ const createView = () => {
|
|
|
189
255
|
destroyed = true;
|
|
190
256
|
}),
|
|
191
257
|
beforeClose: undefined as any,
|
|
258
|
+
inputArgs: {
|
|
259
|
+
pageActive: true,
|
|
260
|
+
},
|
|
192
261
|
};
|
|
193
262
|
return view;
|
|
194
263
|
};
|
|
@@ -196,6 +265,7 @@ const createView = () => {
|
|
|
196
265
|
describe('DynamicFlowsIcon', () => {
|
|
197
266
|
beforeEach(() => {
|
|
198
267
|
mockState.capturedSelectProps.length = 0;
|
|
268
|
+
mockState.capturedTabsProps.length = 0;
|
|
199
269
|
mockState.flowContextValue = {
|
|
200
270
|
modal: {
|
|
201
271
|
confirm: vi.fn(),
|
|
@@ -377,4 +447,80 @@ describe('DynamicFlowsIcon', () => {
|
|
|
377
447
|
expect(view.destroy).toHaveBeenCalledTimes(1);
|
|
378
448
|
expect(model.context.message.success).toHaveBeenCalledWith('Configuration saved');
|
|
379
449
|
});
|
|
450
|
+
|
|
451
|
+
it('renders source tabs and closes the editor when saving one source', async () => {
|
|
452
|
+
const model = createModel();
|
|
453
|
+
const targetSaveStepParams = vi.fn(async () => undefined);
|
|
454
|
+
const target = createPeerModel(model, 'target-model', targetSaveStepParams);
|
|
455
|
+
|
|
456
|
+
model.flowEngine.flowSettings.registerDynamicFlowSourceProvider({
|
|
457
|
+
key: 'test-source-provider',
|
|
458
|
+
getSources: () => [{ key: 'referenced-template', label: 'Referenced template', model: target }],
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const { view } = await openDynamicFlowsEditor(model);
|
|
462
|
+
|
|
463
|
+
expect(screen.getByRole('tab', { name: 'Current block' })).toHaveAttribute('aria-selected', 'true');
|
|
464
|
+
expect(mockState.capturedTabsProps.at(-1)?.tabBarStyle?.paddingLeft).toBeGreaterThan(0);
|
|
465
|
+
await userEvent.click(screen.getByRole('tab', { name: 'Referenced template' }));
|
|
466
|
+
|
|
467
|
+
await userEvent.click(screen.getByRole('button', { name: 'Add event flow' }));
|
|
468
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
|
469
|
+
|
|
470
|
+
expect(targetSaveStepParams).toHaveBeenCalledTimes(1);
|
|
471
|
+
expect(target.flowRegistry.getFlows().size).toBe(2);
|
|
472
|
+
expect(view.destroy).toHaveBeenCalledTimes(1);
|
|
473
|
+
expect(target.context.message.success).toHaveBeenCalledWith('Configuration saved');
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it('saves all dirty sources without discard confirmation', async () => {
|
|
477
|
+
const saveStepParams = vi.fn(async () => undefined);
|
|
478
|
+
const model = createModel({ saveStepParams });
|
|
479
|
+
const targetSaveStepParams = vi.fn(async () => undefined);
|
|
480
|
+
const target = createPeerModel(model, 'target-model', targetSaveStepParams);
|
|
481
|
+
mockState.flowContextValue.modal.confirm.mockResolvedValue(false);
|
|
482
|
+
|
|
483
|
+
model.flowEngine.flowSettings.registerDynamicFlowSourceProvider({
|
|
484
|
+
key: 'test-source-unsaved-provider',
|
|
485
|
+
getSources: () => [{ key: 'referenced-template', label: 'Referenced template', model: target }],
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const { view } = await openDynamicFlowsEditor(model);
|
|
489
|
+
|
|
490
|
+
await userEvent.click(screen.getByRole('tab', { name: 'Referenced template' }));
|
|
491
|
+
await userEvent.click(screen.getByRole('button', { name: 'Add event flow' }));
|
|
492
|
+
await userEvent.click(screen.getByRole('tab', { name: 'Current block' }));
|
|
493
|
+
await userEvent.click(screen.getByRole('button', { name: 'Add event flow' }));
|
|
494
|
+
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
|
|
495
|
+
|
|
496
|
+
expect(saveStepParams).toHaveBeenCalledTimes(1);
|
|
497
|
+
expect(targetSaveStepParams).toHaveBeenCalledTimes(1);
|
|
498
|
+
expect(mockState.flowContextValue.modal.confirm).not.toHaveBeenCalled();
|
|
499
|
+
expect(view.close).toHaveBeenCalledTimes(1);
|
|
500
|
+
expect(view.destroy).toHaveBeenCalledTimes(1);
|
|
501
|
+
expect(model.flowRegistry.getFlows().size).toBe(2);
|
|
502
|
+
expect(target.flowRegistry.getFlows().size).toBe(2);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
it('provides the active source model context inside source tabs', async () => {
|
|
506
|
+
const model = createModel();
|
|
507
|
+
const target = createPeerModel(model, 'target-model');
|
|
508
|
+
const seenModelUids: string[] = [];
|
|
509
|
+
const ContextProbe = () => {
|
|
510
|
+
const ctx = useFlowContext<any>();
|
|
511
|
+
seenModelUids.push(ctx?.model?.uid);
|
|
512
|
+
return null;
|
|
513
|
+
};
|
|
514
|
+
model.flowEngine.flowSettings.renderStepForm = vi.fn(() => <ContextProbe />) as any;
|
|
515
|
+
|
|
516
|
+
model.flowEngine.flowSettings.registerDynamicFlowSourceProvider({
|
|
517
|
+
key: 'test-source-context-provider',
|
|
518
|
+
getSources: () => [{ key: 'referenced-template', label: 'Referenced template', model: target }],
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
await openDynamicFlowsEditor(model);
|
|
522
|
+
await userEvent.click(screen.getByRole('tab', { name: 'Referenced template' }));
|
|
523
|
+
|
|
524
|
+
await waitFor(() => expect(seenModelUids).toContain('target-model'));
|
|
525
|
+
});
|
|
380
526
|
});
|
|
@@ -70,7 +70,7 @@ export const editMarkdownFlow = defineFlow<FlowModel>({
|
|
|
70
70
|
{t('References')}:
|
|
71
71
|
</span>
|
|
72
72
|
<a
|
|
73
|
-
href={`https://
|
|
73
|
+
href={`https://docs.nocobase.com/interface-builder/blocks/other-blocks/markdown`}
|
|
74
74
|
target="_blank"
|
|
75
75
|
rel="noreferrer"
|
|
76
76
|
>
|
package/src/flow/index.ts
CHANGED
|
@@ -49,7 +49,7 @@ export class PluginFlowEngine<TApp extends BaseApplication<any> = BaseApplicatio
|
|
|
49
49
|
key: 'dynamic-flows-editor',
|
|
50
50
|
component: DynamicFlowsIcon,
|
|
51
51
|
visible(model) {
|
|
52
|
-
return model.getEvents().size > 0;
|
|
52
|
+
return model.getEvents().size > 0 || model.flowEngine.flowSettings.hasDynamicFlowSourceProvider(model);
|
|
53
53
|
},
|
|
54
54
|
sort: 0,
|
|
55
55
|
});
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import React, { type ComponentType, type CSSProperties, type ReactNode } from 'react';
|
|
11
11
|
import { DateFilterDynamicComponent } from '../../models/blocks/filter-form/fields/date-time/components/DateFilterDynamicComponent';
|
|
12
|
+
import { translateOptions } from './enumOptionsUtils';
|
|
12
13
|
|
|
13
14
|
type OperatorMeta = {
|
|
14
15
|
value?: string;
|
|
@@ -26,6 +27,7 @@ type OperatorComponentFieldModel = {
|
|
|
26
27
|
props?: Record<string, unknown>;
|
|
27
28
|
render?: () => ReactNode;
|
|
28
29
|
__originalRender?: () => ReactNode;
|
|
30
|
+
translate?: (text: string) => string;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
type OperatorComponentRenderOptions = {
|
|
@@ -93,6 +95,17 @@ export function restoreOperatorComponentRender(fieldModel?: unknown) {
|
|
|
93
95
|
return true;
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
function translateComponentOptions(props: Record<string, unknown>, translate: ((text: string) => string) | undefined) {
|
|
99
|
+
if (!Array.isArray(props.options) || typeof translate !== 'function') {
|
|
100
|
+
return props;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
...props,
|
|
105
|
+
options: translateOptions(props.options, translate),
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
96
109
|
export function applyOperatorComponentRender({
|
|
97
110
|
app,
|
|
98
111
|
fieldModel,
|
|
@@ -118,8 +131,9 @@ export function applyOperatorComponentRender({
|
|
|
118
131
|
mutableFieldModel.render = () => {
|
|
119
132
|
const fieldProps = mutableFieldModel.props || {};
|
|
120
133
|
const fieldStyle = pickOperatorStyle(fieldProps.style);
|
|
121
|
-
const
|
|
134
|
+
const mergedProps =
|
|
122
135
|
propsPriority === 'operator' ? { ...fieldProps, ...operatorProps } : { ...operatorProps, ...fieldProps };
|
|
136
|
+
const componentProps = translateComponentOptions(mergedProps, mutableFieldModel.translate);
|
|
123
137
|
const componentStyle =
|
|
124
138
|
propsPriority === 'operator' ? { ...fieldStyle, ...operatorStyle } : { ...operatorStyle, ...fieldStyle };
|
|
125
139
|
const mergedStyle = { ...(style || {}), ...componentStyle };
|
package/src/flow/models/blocks/filter-manager/flow-actions/__tests__/customizeFilterRender.test.tsx
CHANGED
|
@@ -18,12 +18,23 @@ type TestFieldModel = {
|
|
|
18
18
|
setupReactiveRender: ReturnType<typeof vi.fn>;
|
|
19
19
|
_reactiveWrapperCache?: unknown;
|
|
20
20
|
__originalRender?: () => React.ReactNode;
|
|
21
|
+
translate?: (text: string) => string;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
const MultipleKeywordsInput = (props: Record<string, unknown>) => {
|
|
24
25
|
return <div data-testid="multiple-keywords-input" {...props} />;
|
|
25
26
|
};
|
|
26
27
|
|
|
28
|
+
const Select = (props: Record<string, unknown>) => {
|
|
29
|
+
return <div data-testid="select" {...props} />;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function mockT(text: string) {
|
|
33
|
+
if (text === '{{t("Yes")}}') return '是';
|
|
34
|
+
if (text === '{{t("No")}}') return '否';
|
|
35
|
+
return text;
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
function createFieldModel() {
|
|
28
39
|
return {
|
|
29
40
|
props: {
|
|
@@ -52,7 +63,10 @@ function createFilterItemModel({
|
|
|
52
63
|
collectionField,
|
|
53
64
|
context: {
|
|
54
65
|
app: {
|
|
55
|
-
getComponent: (name: string) =>
|
|
66
|
+
getComponent: (name: string) => {
|
|
67
|
+
if (name === 'MultipleKeywordsInput') return MultipleKeywordsInput;
|
|
68
|
+
if (name === 'Select') return Select;
|
|
69
|
+
},
|
|
56
70
|
},
|
|
57
71
|
collectionField,
|
|
58
72
|
},
|
|
@@ -112,6 +126,47 @@ describe('customizeFilterRender action', () => {
|
|
|
112
126
|
expect((rerenderedElement as React.ReactElement).props.placeholder).toBe('updated runtime placeholder');
|
|
113
127
|
});
|
|
114
128
|
|
|
129
|
+
it('translates operator schema select options', () => {
|
|
130
|
+
const fieldModel = {
|
|
131
|
+
...createFieldModel(),
|
|
132
|
+
translate: mockT,
|
|
133
|
+
};
|
|
134
|
+
const model = createFilterItemModel({
|
|
135
|
+
fieldModel,
|
|
136
|
+
operator: '$isTruly',
|
|
137
|
+
collectionField: {
|
|
138
|
+
interface: 'checkbox',
|
|
139
|
+
type: 'boolean',
|
|
140
|
+
filterable: {
|
|
141
|
+
operators: [
|
|
142
|
+
{
|
|
143
|
+
label: '{{t("Yes")}}',
|
|
144
|
+
value: '$isTruly',
|
|
145
|
+
schema: {
|
|
146
|
+
'x-component': 'Select',
|
|
147
|
+
'x-component-props': {
|
|
148
|
+
options: [
|
|
149
|
+
{ label: '{{t("Yes")}}', value: true },
|
|
150
|
+
{ label: '{{t("No")}}', value: false },
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
customizeFilterRender.handler?.({ model } as any);
|
|
161
|
+
|
|
162
|
+
const element = fieldModel.render();
|
|
163
|
+
expect((element as React.ReactElement).type).toBe(Select);
|
|
164
|
+
expect((element as React.ReactElement).props.options).toEqual([
|
|
165
|
+
{ label: '是', value: true },
|
|
166
|
+
{ label: '否', value: false },
|
|
167
|
+
]);
|
|
168
|
+
});
|
|
169
|
+
|
|
115
170
|
it('restores original render when switching to an operator without a schema component', () => {
|
|
116
171
|
const fieldModel = createFieldModel();
|
|
117
172
|
const originalRender = fieldModel.render;
|
|
@@ -35,6 +35,7 @@ import { ErrorBoundary } from 'react-error-boundary';
|
|
|
35
35
|
import { getRowKey } from './utils';
|
|
36
36
|
import { getSavedAssociationTitleField, getTableColumnSortField } from './sortUtils';
|
|
37
37
|
import { getFieldBindingUse, rebuildFieldSubModel } from '../../../internal/utils/rebuildFieldSubModel';
|
|
38
|
+
import { getSavedDateTimeFormatParams, resolveDateTimeDisplayProps } from '../../../utils/dateTimeDisplayProps';
|
|
38
39
|
|
|
39
40
|
export function FieldDeletePlaceholder(props: any) {
|
|
40
41
|
const { t } = useTranslation();
|
|
@@ -128,6 +129,15 @@ export const CustomWidth = ({ setOpen, t, handleChange, defaultValue }) => {
|
|
|
128
129
|
);
|
|
129
130
|
};
|
|
130
131
|
|
|
132
|
+
const resetDateTimeDisplayProps = {
|
|
133
|
+
dateOnly: undefined,
|
|
134
|
+
dateFormat: undefined,
|
|
135
|
+
format: undefined,
|
|
136
|
+
picker: undefined,
|
|
137
|
+
showTime: undefined,
|
|
138
|
+
timeFormat: undefined,
|
|
139
|
+
};
|
|
140
|
+
|
|
131
141
|
export class TableColumnModel extends DisplayItemModel {
|
|
132
142
|
// 标记:该类的 render 返回函数, 避免错误的reactive封装
|
|
133
143
|
static renderMode: ModelRenderMode = ModelRenderMode.RenderFunction;
|
|
@@ -353,11 +363,22 @@ TableColumnModel.registerFlow({
|
|
|
353
363
|
return;
|
|
354
364
|
}
|
|
355
365
|
const titleField = getSavedAssociationTitleField(ctx.model);
|
|
366
|
+
const fieldModel = Array.isArray(ctx.model.subModels.field) ? undefined : ctx.model.subModels.field;
|
|
367
|
+
const targetCollectionField = collectionField.targetCollection?.getField?.(titleField);
|
|
368
|
+
const savedDateTimeDisplayProps = getSavedDateTimeFormatParams(fieldModel)
|
|
369
|
+
? resolveDateTimeDisplayProps({
|
|
370
|
+
model: fieldModel,
|
|
371
|
+
collectionField,
|
|
372
|
+
titleField,
|
|
373
|
+
currentProps: ctx.model.props,
|
|
374
|
+
})
|
|
375
|
+
: undefined;
|
|
356
376
|
const componentProps =
|
|
357
377
|
collectionField.isAssociationField() && titleField
|
|
358
378
|
? {
|
|
359
379
|
...collectionField.getComponentProps(),
|
|
360
|
-
...
|
|
380
|
+
...targetCollectionField?.getComponentProps?.(),
|
|
381
|
+
...savedDateTimeDisplayProps,
|
|
361
382
|
}
|
|
362
383
|
: collectionField.getComponentProps();
|
|
363
384
|
ctx.model.setProps('title', collectionField.title);
|
|
@@ -539,21 +560,33 @@ TableColumnModel.registerFlow({
|
|
|
539
560
|
typeof binding?.defaultProps === 'function'
|
|
540
561
|
? binding.defaultProps(ctx, targetCollectionField)
|
|
541
562
|
: binding?.defaultProps;
|
|
563
|
+
const componentProps = targetCollectionField.getComponentProps?.() || {};
|
|
564
|
+
const nextFieldProps = {
|
|
565
|
+
...(defaultProps || {}),
|
|
566
|
+
...componentProps,
|
|
567
|
+
titleField: params.label,
|
|
568
|
+
};
|
|
569
|
+
const nextColumnProps = {
|
|
570
|
+
...resetDateTimeDisplayProps,
|
|
571
|
+
...nextFieldProps,
|
|
572
|
+
};
|
|
542
573
|
if (targetUse && targetUse !== currentUse) {
|
|
543
574
|
await rebuildFieldSubModel({
|
|
544
575
|
parentModel: ctx.model as any,
|
|
545
576
|
targetUse,
|
|
546
|
-
defaultProps,
|
|
577
|
+
defaultProps: nextFieldProps,
|
|
547
578
|
fieldSettingsInit,
|
|
548
579
|
});
|
|
549
580
|
} else if (fieldModel) {
|
|
581
|
+
fieldModel.setProps(nextColumnProps);
|
|
550
582
|
fieldModel.setStepParams('fieldSettings', 'init', fieldSettingsInit);
|
|
551
583
|
await fieldModel.dispatchEvent('beforeRender', undefined, { useCache: false });
|
|
584
|
+
await fieldModel.save();
|
|
552
585
|
}
|
|
553
586
|
if (targetUse) {
|
|
554
587
|
ctx.model.setStepParams('tableColumnSettings', 'model', { use: targetUse });
|
|
555
588
|
}
|
|
556
|
-
ctx.model.setProps(
|
|
589
|
+
ctx.model.setProps(nextColumnProps);
|
|
557
590
|
},
|
|
558
591
|
defaultParams: (ctx: any) => {
|
|
559
592
|
const titleField = ctx.model?.context?.collectionField?.targetCollectionTitleFieldName;
|
|
@@ -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,
|
|
@@ -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,
|