@nocobase/client-v2 2.1.0-beta.25 → 2.1.0-beta.26
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/actions/linkageRulesFormValueRefresh.d.ts +10 -0
- package/es/flow/index.d.ts +1 -0
- package/es/flow/models/actions/AssociateActionModel.d.ts +19 -0
- package/es/flow/models/actions/AssociationActionUtils.d.ts +17 -0
- package/es/flow/models/actions/DisassociateActionModel.d.ts +16 -0
- package/es/flow/models/actions/index.d.ts +3 -0
- package/es/flow/models/base/GridModel.d.ts +3 -1
- package/es/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.d.ts +9 -0
- package/es/index.d.ts +1 -0
- package/es/index.mjs +80 -80
- package/lib/index.js +87 -87
- package/package.json +5 -5
- package/src/__tests__/globalDeps.test.ts +5 -0
- package/src/flow/actions/__tests__/linkageRules.formValueDrivenRefresh.test.ts +438 -0
- package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +42 -0
- package/src/flow/actions/linkageRules.tsx +8 -1
- package/src/flow/actions/linkageRulesFormValueRefresh.ts +492 -0
- package/src/flow/actions/linkageRulesRefresh.tsx +4 -2
- package/src/flow/index.ts +1 -0
- package/src/flow/models/actions/AssociateActionModel.tsx +196 -0
- package/src/flow/models/actions/AssociationActionUtils.ts +90 -0
- package/src/flow/models/actions/DisassociateActionModel.tsx +57 -0
- package/src/flow/models/actions/__tests__/AssociationActionModel.test.ts +250 -0
- package/src/flow/models/actions/index.ts +3 -0
- package/src/flow/models/base/GridModel.tsx +21 -1
- package/src/flow/models/base/__tests__/GridModel.dragSnapshotContainer.test.ts +98 -0
- package/src/flow/models/blocks/details/DetailsItemModel.tsx +3 -0
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +5 -1
- package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableColumnModel.tsx +21 -5
- package/src/flow/models/fields/AssociationFieldModel/recordSelectSettingsUtils.ts +20 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +11 -3
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +235 -0
- package/src/index.ts +1 -0
- package/src/utils/globalDeps.ts +6 -0
|
@@ -13,7 +13,7 @@ import { Button, CheckList, Popup, SearchBar } from 'antd-mobile';
|
|
|
13
13
|
import React, { useEffect, useMemo, useState } from 'react';
|
|
14
14
|
|
|
15
15
|
export function MobileSelect(props) {
|
|
16
|
-
const { value, onChange, disabled, options = [], mode } = props;
|
|
16
|
+
const { value, onChange, onChangeComplete, disabled, options = [], mode } = props;
|
|
17
17
|
const ctx = useFlowModelContext();
|
|
18
18
|
const t = ctx.t;
|
|
19
19
|
const [visible, setVisible] = useState(false);
|
|
@@ -28,6 +28,7 @@ export function MobileSelect(props) {
|
|
|
28
28
|
|
|
29
29
|
const handleConfirm = () => {
|
|
30
30
|
onChange(selected);
|
|
31
|
+
onChangeComplete?.();
|
|
31
32
|
setVisible(false);
|
|
32
33
|
};
|
|
33
34
|
useEffect(() => {
|
|
@@ -36,12 +37,18 @@ export function MobileSelect(props) {
|
|
|
36
37
|
} else {
|
|
37
38
|
setSearchText(null);
|
|
38
39
|
}
|
|
39
|
-
}, [visible]);
|
|
40
|
+
}, [visible, value]);
|
|
40
41
|
|
|
41
42
|
return (
|
|
42
43
|
<>
|
|
43
44
|
<div onClick={() => !disabled && setVisible(true)}>
|
|
44
|
-
<Select
|
|
45
|
+
<Select
|
|
46
|
+
{...props}
|
|
47
|
+
open={false}
|
|
48
|
+
dropdownStyle={{ display: 'none' }}
|
|
49
|
+
showSearch={false}
|
|
50
|
+
style={{ pointerEvents: 'none', width: '100%' }}
|
|
51
|
+
/>
|
|
45
52
|
</div>
|
|
46
53
|
<Popup
|
|
47
54
|
visible={visible}
|
|
@@ -71,6 +78,7 @@ export function MobileSelect(props) {
|
|
|
71
78
|
} else {
|
|
72
79
|
setSelected(val[0]);
|
|
73
80
|
onChange(val[0]);
|
|
81
|
+
onChangeComplete?.();
|
|
74
82
|
setVisible(false);
|
|
75
83
|
}
|
|
76
84
|
}}
|
|
@@ -0,0 +1,235 @@
|
|
|
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 { beforeEach, describe, it, expect, vi } from 'vitest';
|
|
12
|
+
import { act, fireEvent, render, screen } from '@nocobase/test/client';
|
|
13
|
+
import { MobileSelect } from '../MobileSelect';
|
|
14
|
+
|
|
15
|
+
const DEFAULT_OPTIONS = [
|
|
16
|
+
{ label: 'Option A', value: 'a' },
|
|
17
|
+
{ label: 'Option B', value: 'b' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const mockState = vi.hoisted(() => ({
|
|
21
|
+
selectProps: undefined as any,
|
|
22
|
+
popupProps: undefined as any,
|
|
23
|
+
checklistProps: undefined as any,
|
|
24
|
+
confirmButtonProps: undefined as any,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
function resetMockState() {
|
|
28
|
+
mockState.selectProps = undefined;
|
|
29
|
+
mockState.popupProps = undefined;
|
|
30
|
+
mockState.checklistProps = undefined;
|
|
31
|
+
mockState.confirmButtonProps = undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function clickTrigger() {
|
|
35
|
+
const trigger = screen.getByTestId('antd-select').parentElement as HTMLElement | null;
|
|
36
|
+
expect(trigger).toBeTruthy();
|
|
37
|
+
act(() => {
|
|
38
|
+
fireEvent.click(trigger as HTMLElement);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function openPopup() {
|
|
43
|
+
clickTrigger();
|
|
44
|
+
expect(screen.getByTestId('popup')).toBeInTheDocument();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function selectValues(values: string[]) {
|
|
48
|
+
act(() => {
|
|
49
|
+
mockState.checklistProps?.onChange?.(values);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function confirmSelection() {
|
|
54
|
+
act(() => {
|
|
55
|
+
mockState.confirmButtonProps?.onClick?.();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function renderMobileSelect(props: Record<string, any> = {}) {
|
|
60
|
+
const onChange = props.onChange ?? vi.fn();
|
|
61
|
+
const onChangeComplete = props.onChangeComplete ?? vi.fn();
|
|
62
|
+
|
|
63
|
+
render(
|
|
64
|
+
<MobileSelect
|
|
65
|
+
value={undefined}
|
|
66
|
+
options={DEFAULT_OPTIONS}
|
|
67
|
+
onChange={onChange}
|
|
68
|
+
onChangeComplete={onChangeComplete}
|
|
69
|
+
{...props}
|
|
70
|
+
/>,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return { onChange, onChangeComplete };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
vi.mock('@nocobase/flow-engine', async () => {
|
|
77
|
+
const actual = await vi.importActual<any>('@nocobase/flow-engine');
|
|
78
|
+
return {
|
|
79
|
+
...actual,
|
|
80
|
+
useFlowModelContext: () => ({
|
|
81
|
+
t: (value: string) => value,
|
|
82
|
+
}),
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
vi.mock('antd', async () => {
|
|
87
|
+
const actual = await vi.importActual<any>('antd');
|
|
88
|
+
return {
|
|
89
|
+
...actual,
|
|
90
|
+
Select: (props: any) => {
|
|
91
|
+
mockState.selectProps = props;
|
|
92
|
+
return <div data-testid="antd-select" />;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
vi.mock('antd-mobile', () => {
|
|
98
|
+
const MockCheckList: any = (props: any) => {
|
|
99
|
+
mockState.checklistProps = props;
|
|
100
|
+
return <div data-testid="checklist">{props.children}</div>;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
MockCheckList.Item = ({ value, children }: any) => <div data-testid={`item-${value}`}>{children}</div>;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
Button: (props: any) => {
|
|
107
|
+
mockState.confirmButtonProps = props;
|
|
108
|
+
return (
|
|
109
|
+
<button type="button" data-testid="confirm" onClick={props.onClick}>
|
|
110
|
+
{props.children}
|
|
111
|
+
</button>
|
|
112
|
+
);
|
|
113
|
+
},
|
|
114
|
+
Popup: (props: any) => {
|
|
115
|
+
mockState.popupProps = props;
|
|
116
|
+
return props.visible ? <div data-testid="popup">{props.children}</div> : null;
|
|
117
|
+
},
|
|
118
|
+
SearchBar: ({ value, onChange }: any) => (
|
|
119
|
+
<input data-testid="search" value={value ?? ''} onChange={(e) => onChange?.(e.target.value)} />
|
|
120
|
+
),
|
|
121
|
+
CheckList: MockCheckList,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe('MobileSelect', () => {
|
|
126
|
+
beforeEach(() => {
|
|
127
|
+
resetMockState();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('commits the selected value immediately in single mode', () => {
|
|
131
|
+
const { onChange, onChangeComplete } = renderMobileSelect();
|
|
132
|
+
|
|
133
|
+
openPopup();
|
|
134
|
+
selectValues(['a']);
|
|
135
|
+
|
|
136
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
137
|
+
expect(onChange).toHaveBeenCalledWith('a');
|
|
138
|
+
expect(onChangeComplete).toHaveBeenCalledTimes(1);
|
|
139
|
+
expect(screen.queryByTestId('popup')).not.toBeInTheDocument();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('renders filtered options based on search text', () => {
|
|
143
|
+
const { onChange, onChangeComplete } = renderMobileSelect();
|
|
144
|
+
openPopup();
|
|
145
|
+
act(() => {
|
|
146
|
+
fireEvent.change(screen.getByTestId('search'), { target: { value: 'Option A' } });
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
expect(screen.getByTestId('item-a')).toBeInTheDocument();
|
|
150
|
+
expect(screen.queryByTestId('item-b')).not.toBeInTheDocument();
|
|
151
|
+
|
|
152
|
+
selectValues(['a']);
|
|
153
|
+
|
|
154
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
155
|
+
expect(onChange).toHaveBeenCalledWith('a');
|
|
156
|
+
expect(onChangeComplete).toHaveBeenCalledTimes(1);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('defers commit until confirm in multiple mode', () => {
|
|
160
|
+
const { onChange, onChangeComplete } = renderMobileSelect({ value: [], mode: 'multiple' });
|
|
161
|
+
openPopup();
|
|
162
|
+
|
|
163
|
+
selectValues(['a', 'b']);
|
|
164
|
+
expect(onChange).not.toHaveBeenCalled();
|
|
165
|
+
expect(onChangeComplete).not.toHaveBeenCalled();
|
|
166
|
+
|
|
167
|
+
confirmSelection();
|
|
168
|
+
|
|
169
|
+
expect(onChange).toHaveBeenCalledTimes(1);
|
|
170
|
+
expect(onChange).toHaveBeenCalledWith(['a', 'b']);
|
|
171
|
+
expect(onChangeComplete).toHaveBeenCalledTimes(1);
|
|
172
|
+
expect(screen.queryByTestId('popup')).not.toBeInTheDocument();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('does not open popup when disabled', () => {
|
|
176
|
+
renderMobileSelect({ disabled: true });
|
|
177
|
+
|
|
178
|
+
clickTrigger();
|
|
179
|
+
expect(screen.queryByTestId('popup')).not.toBeInTheDocument();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
function SubTableCellHarness({ value, onCommit, mode }: { value: any; onCommit: (value: any) => void; mode?: string }) {
|
|
184
|
+
const pendingValueRef = React.useRef<any>(value);
|
|
185
|
+
return (
|
|
186
|
+
<div>
|
|
187
|
+
<MobileSelect
|
|
188
|
+
value={value}
|
|
189
|
+
mode={mode}
|
|
190
|
+
options={DEFAULT_OPTIONS}
|
|
191
|
+
onChange={(next) => {
|
|
192
|
+
pendingValueRef.current = next;
|
|
193
|
+
if (Array.isArray(next)) {
|
|
194
|
+
onCommit(next);
|
|
195
|
+
}
|
|
196
|
+
}}
|
|
197
|
+
onChangeComplete={() => {
|
|
198
|
+
onCommit(pendingValueRef.current);
|
|
199
|
+
}}
|
|
200
|
+
/>
|
|
201
|
+
</div>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
describe('MobileSelect in SubForm/SubTable containers', () => {
|
|
206
|
+
beforeEach(() => {
|
|
207
|
+
resetMockState();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('SubTable: single selection commits final value via onChangeComplete', () => {
|
|
211
|
+
const onCommit = vi.fn();
|
|
212
|
+
|
|
213
|
+
render(<SubTableCellHarness value={undefined} onCommit={onCommit} />);
|
|
214
|
+
|
|
215
|
+
openPopup();
|
|
216
|
+
selectValues(['b']);
|
|
217
|
+
|
|
218
|
+
expect(onCommit).toHaveBeenCalledTimes(1);
|
|
219
|
+
expect(onCommit).toHaveBeenCalledWith('b');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('SubTable: multiple mode only commits after confirm, and commit receives the full array', () => {
|
|
223
|
+
const onCommit = vi.fn();
|
|
224
|
+
|
|
225
|
+
render(<SubTableCellHarness value={[]} onCommit={onCommit} mode="multiple" />);
|
|
226
|
+
|
|
227
|
+
openPopup();
|
|
228
|
+
selectValues(['a', 'b']);
|
|
229
|
+
confirmSelection();
|
|
230
|
+
|
|
231
|
+
expect(onCommit).toHaveBeenCalledTimes(2);
|
|
232
|
+
expect(onCommit).toHaveBeenNthCalledWith(1, ['a', 'b']);
|
|
233
|
+
expect(onCommit).toHaveBeenNthCalledWith(2, ['a', 'b']);
|
|
234
|
+
});
|
|
235
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -32,4 +32,5 @@ export * from './collection-field-interface/CollectionFieldInterface';
|
|
|
32
32
|
export * from './collection-field-interface/CollectionFieldInterfaceManager';
|
|
33
33
|
export * from './collection-manager/interfaces';
|
|
34
34
|
export * from './flow';
|
|
35
|
+
export { DEFAULT_DATA_SOURCE_KEY, isTitleField } from './flow-compat';
|
|
35
36
|
export { default as AntdAppProvider } from './theme/AntdAppProvider';
|
package/src/utils/globalDeps.ts
CHANGED
|
@@ -9,15 +9,18 @@
|
|
|
9
9
|
|
|
10
10
|
import * as antdCssinjs from '@ant-design/cssinjs';
|
|
11
11
|
import * as antdIcons from '@ant-design/icons';
|
|
12
|
+
import * as emotionCss from '@emotion/css';
|
|
12
13
|
import * as formilyCore from '@formily/core';
|
|
13
14
|
import * as formilyReact from '@formily/react';
|
|
14
15
|
import * as formilyReactive from '@formily/reactive';
|
|
15
16
|
import * as formilyShared from '@formily/shared';
|
|
16
17
|
import * as nocobaseClientUtils from '@nocobase/utils/client';
|
|
18
|
+
import { dayjs } from '@nocobase/utils/client';
|
|
17
19
|
import * as nocobaseFlowEngine from '@nocobase/flow-engine';
|
|
18
20
|
import * as ahooks from 'ahooks';
|
|
19
21
|
import * as antd from 'antd';
|
|
20
22
|
import * as i18next from 'i18next';
|
|
23
|
+
import lodash from 'lodash';
|
|
21
24
|
import React from 'react';
|
|
22
25
|
import ReactDOM from 'react-dom';
|
|
23
26
|
import * as reactI18next from 'react-i18next';
|
|
@@ -65,4 +68,7 @@ export function defineGlobalDeps(requirejs: RequireJS) {
|
|
|
65
68
|
|
|
66
69
|
// utils
|
|
67
70
|
requirejs.define('ahooks', () => ahooks);
|
|
71
|
+
requirejs.define('dayjs', () => dayjs);
|
|
72
|
+
requirejs.define('lodash', () => lodash);
|
|
73
|
+
requirejs.define('@emotion/css', () => emotionCss);
|
|
68
74
|
}
|