@nocobase/client-v2 2.3.0-beta.20 → 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/flow/admin-shell/admin-layout/MobileTopbarActionsContext.d.ts +10 -0
- package/es/index.mjs +88 -88
- package/lib/index.js +88 -88
- package/package.json +7 -7
- 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/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +4 -1
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +4 -1
- package/src/flow/models/fields/AssociationFieldModel/__tests__/associationSelect.search.test.ts +113 -0
- package/src/flow/models/topbar/UserCenterTopbarActionModel.tsx +8 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/client-v2",
|
|
3
|
-
"version": "2.3.0-beta.
|
|
3
|
+
"version": "2.3.0-beta.21",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"module": "es/index.mjs",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
"@formily/antd-v5": "1.2.3",
|
|
28
28
|
"@formily/react": "^2.2.27",
|
|
29
29
|
"@formily/shared": "^2.2.27",
|
|
30
|
-
"@nocobase/evaluators": "2.3.0-beta.
|
|
31
|
-
"@nocobase/flow-engine": "2.3.0-beta.
|
|
32
|
-
"@nocobase/sdk": "2.3.0-beta.
|
|
33
|
-
"@nocobase/shared": "2.3.0-beta.
|
|
34
|
-
"@nocobase/utils": "2.3.0-beta.
|
|
30
|
+
"@nocobase/evaluators": "2.3.0-beta.21",
|
|
31
|
+
"@nocobase/flow-engine": "2.3.0-beta.21",
|
|
32
|
+
"@nocobase/sdk": "2.3.0-beta.21",
|
|
33
|
+
"@nocobase/shared": "2.3.0-beta.21",
|
|
34
|
+
"@nocobase/utils": "2.3.0-beta.21",
|
|
35
35
|
"ahooks": "^3.7.2",
|
|
36
36
|
"antd": "5.24.2",
|
|
37
37
|
"antd-style": "3.7.1",
|
|
@@ -49,5 +49,5 @@
|
|
|
49
49
|
"react-router-dom": "^6.30.1",
|
|
50
50
|
"zxing-wasm": "^3.1.3"
|
|
51
51
|
},
|
|
52
|
-
"gitHead": "
|
|
52
|
+
"gitHead": "a2bdd27cc7bc8ced9ad395b3a7468fc1850eef4a"
|
|
53
53
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
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 { createContext } from 'react';
|
|
11
|
+
|
|
12
|
+
// Internal callback for dismissing the mobile topbar after a nested menu action completes.
|
|
13
|
+
export const MobileTopbarActionsContext = createContext<(() => void) | undefined>(undefined);
|
|
@@ -11,13 +11,14 @@ import { EllipsisOutlined } from '@ant-design/icons';
|
|
|
11
11
|
import { css } from '@emotion/css';
|
|
12
12
|
import { FlowModelRenderer } from '@nocobase/flow-engine';
|
|
13
13
|
import { ConfigProvider, Divider, Popover, theme } from 'antd';
|
|
14
|
-
import React, { useCallback, useMemo, useState } from 'react';
|
|
14
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
15
15
|
import { ErrorBoundary } from 'react-error-boundary';
|
|
16
16
|
import { useAclSnippets } from '../../../acl/useAclSnippets';
|
|
17
17
|
import type { CustomToken } from '../../../theme';
|
|
18
18
|
import { TopbarActionModel } from '../../models/topbar/TopbarActionModel';
|
|
19
19
|
import { USER_CENTER_ACTION_ID } from '../../models/topbar/UserCenterTopbarActionModel';
|
|
20
20
|
import { HelpLite } from './HelpLite';
|
|
21
|
+
import { MobileTopbarActionsContext } from './MobileTopbarActionsContext';
|
|
21
22
|
|
|
22
23
|
const topbarActionsBarClassName = css`
|
|
23
24
|
display: inline-flex;
|
|
@@ -183,11 +184,21 @@ export const TopbarActionsBar = React.memo((props: { actions: TopbarActionModel[
|
|
|
183
184
|
setOpen(false);
|
|
184
185
|
}, []);
|
|
185
186
|
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
if (!props.isMobile) {
|
|
189
|
+
setOpen(false);
|
|
190
|
+
}
|
|
191
|
+
}, [props.isMobile]);
|
|
192
|
+
|
|
186
193
|
if (props.isMobile) {
|
|
187
194
|
return (
|
|
188
195
|
<Popover
|
|
189
196
|
rootClassName={popoverStyle}
|
|
190
|
-
content={
|
|
197
|
+
content={
|
|
198
|
+
<MobileTopbarActionsContext.Provider value={handleContentClick}>
|
|
199
|
+
<TopbarActionsContent actions={props.actions} onActionClick={handleContentClick} />
|
|
200
|
+
</MobileTopbarActionsContext.Provider>
|
|
201
|
+
}
|
|
191
202
|
color={customToken.colorBgHeader}
|
|
192
203
|
trigger="click"
|
|
193
204
|
open={open}
|
|
@@ -0,0 +1,205 @@
|
|
|
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 { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
11
|
+
import { Drawer, Input } from 'antd';
|
|
12
|
+
import React, { useState } from 'react';
|
|
13
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
14
|
+
|
|
15
|
+
vi.mock('@nocobase/flow-engine', () => ({
|
|
16
|
+
FlowModel: class {
|
|
17
|
+
uid: string;
|
|
18
|
+
context = { t: (value: string) => value };
|
|
19
|
+
|
|
20
|
+
constructor(options: { uid: string }) {
|
|
21
|
+
this.uid = options.uid;
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
FlowModelRenderer: ({ model }: { model: { render: () => React.ReactNode } }) => model.render(),
|
|
25
|
+
observer: (component: React.ComponentType) => component,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock('../../../models/topbar/TopbarActionModel', async () => {
|
|
29
|
+
const { FlowModel } = await import('@nocobase/flow-engine');
|
|
30
|
+
return {
|
|
31
|
+
TopbarActionModel: class extends FlowModel {
|
|
32
|
+
isHidden() {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getActionId() {
|
|
37
|
+
return 'user-center';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
renderWrapper(node: React.ReactNode) {
|
|
41
|
+
return node;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
renderButton() {
|
|
45
|
+
return <button data-testid="user-center-button">User center</button>;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
vi.mock('../../../../acl/useAclSnippets', () => ({
|
|
52
|
+
useAclSnippets: () => ({ allow: () => true }),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
vi.mock('../HelpLite', () => ({ HelpLite: () => null }));
|
|
56
|
+
|
|
57
|
+
import {
|
|
58
|
+
UserCenterActionItemModel,
|
|
59
|
+
UserCenterSelectItemModel,
|
|
60
|
+
UserCenterTopbarActionModel,
|
|
61
|
+
} from '../../../models/topbar/UserCenterTopbarActionModel';
|
|
62
|
+
import { TopbarActionsBar } from '../TopbarActionsBar';
|
|
63
|
+
|
|
64
|
+
afterEach(cleanup);
|
|
65
|
+
|
|
66
|
+
function createUserCenter(onClick: () => Promise<void>, closeOnClick = true) {
|
|
67
|
+
const item = new UserCenterActionItemModel({ uid: 'test-edit-profile', use: 'UserCenterActionItemModel' });
|
|
68
|
+
item.label = 'Edit profile';
|
|
69
|
+
item.onClick = onClick;
|
|
70
|
+
item.closeOnClick = closeOnClick;
|
|
71
|
+
const model = new UserCenterTopbarActionModel({ uid: 'test-user-center', use: 'UserCenterTopbarActionModel' });
|
|
72
|
+
vi.spyOn(model, 'getPreparedItems').mockImplementation(async (closeDropdown) => {
|
|
73
|
+
item.setCloseDropdown(closeDropdown);
|
|
74
|
+
return [item];
|
|
75
|
+
});
|
|
76
|
+
return model;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function openUserMenu(isMobile: boolean) {
|
|
80
|
+
if (isMobile) {
|
|
81
|
+
fireEvent.click(screen.getByRole('img', { name: 'ellipsis' }));
|
|
82
|
+
}
|
|
83
|
+
fireEvent.mouseEnter(screen.getByTestId('user-center-button'));
|
|
84
|
+
const action = await screen.findByRole('button', { name: 'Edit profile' });
|
|
85
|
+
await waitFor(() => expect(action).toBeVisible());
|
|
86
|
+
return action;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('mobile user center popover', () => {
|
|
90
|
+
it('closes both menus after selecting an action and keeps the opened drawer mounted', async () => {
|
|
91
|
+
const model = createUserCenter(vi.fn());
|
|
92
|
+
const Test = () => {
|
|
93
|
+
const [open, setOpen] = useState(false);
|
|
94
|
+
model.getPreparedItems = async (closeDropdown) => {
|
|
95
|
+
const item = new UserCenterActionItemModel({ uid: 'profile', use: 'UserCenterActionItemModel' });
|
|
96
|
+
item.label = 'Edit profile';
|
|
97
|
+
item.onClick = async () => setOpen(true);
|
|
98
|
+
item.setCloseDropdown(closeDropdown);
|
|
99
|
+
return [item];
|
|
100
|
+
};
|
|
101
|
+
return (
|
|
102
|
+
<>
|
|
103
|
+
<TopbarActionsBar actions={[model]} isMobile />
|
|
104
|
+
<Drawer open={open} title="Edit profile">
|
|
105
|
+
<Input aria-label="Nickname" />
|
|
106
|
+
</Drawer>
|
|
107
|
+
</>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
render(<Test />);
|
|
111
|
+
|
|
112
|
+
fireEvent.click(await openUserMenu(true));
|
|
113
|
+
|
|
114
|
+
await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument());
|
|
115
|
+
expect(screen.getByRole('textbox', { name: 'Nickname' })).toBeVisible();
|
|
116
|
+
expect(screen.queryByRole('button', { name: 'Edit profile' })).not.toBeInTheDocument();
|
|
117
|
+
|
|
118
|
+
fireEvent.click(screen.getByRole('img', { name: 'ellipsis' }));
|
|
119
|
+
fireEvent.mouseEnter(screen.getByTestId('user-center-button'));
|
|
120
|
+
await waitFor(() => expect(screen.getByRole('button', { name: 'Edit profile' })).toBeVisible());
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('respects actions that keep the menu open', async () => {
|
|
124
|
+
const onClick = vi.fn(async () => {});
|
|
125
|
+
render(<TopbarActionsBar actions={[createUserCenter(onClick, false)]} isMobile />);
|
|
126
|
+
|
|
127
|
+
fireEvent.click(await openUserMenu(true));
|
|
128
|
+
|
|
129
|
+
await waitFor(() => expect(onClick).toHaveBeenCalledOnce());
|
|
130
|
+
expect(screen.getByRole('tooltip')).toBeVisible();
|
|
131
|
+
expect(screen.getByRole('button', { name: 'Edit profile' })).toBeVisible();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('keeps the topbar open when the user only leaves the nested menu', async () => {
|
|
135
|
+
render(<TopbarActionsBar actions={[createUserCenter(vi.fn())]} isMobile />);
|
|
136
|
+
const action = await openUserMenu(true);
|
|
137
|
+
|
|
138
|
+
const dropdown = action.closest('.ant-dropdown');
|
|
139
|
+
expect(dropdown).not.toBeNull();
|
|
140
|
+
if (dropdown) {
|
|
141
|
+
fireEvent.mouseLeave(dropdown);
|
|
142
|
+
}
|
|
143
|
+
fireEvent.mouseLeave(screen.getByTestId('user-center-button'));
|
|
144
|
+
|
|
145
|
+
await waitFor(() => expect(screen.queryByRole('button', { name: 'Edit profile' })).not.toBeInTheDocument());
|
|
146
|
+
expect(screen.getByRole('tooltip')).toBeVisible();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('keeps desktop actions working without a mobile popover', async () => {
|
|
150
|
+
const onClick = vi.fn(async () => {});
|
|
151
|
+
render(<TopbarActionsBar actions={[createUserCenter(onClick)]} />);
|
|
152
|
+
|
|
153
|
+
fireEvent.click(await openUserMenu(false));
|
|
154
|
+
|
|
155
|
+
await waitFor(() => expect(screen.queryByRole('button', { name: 'Edit profile' })).not.toBeInTheDocument());
|
|
156
|
+
expect(onClick).toHaveBeenCalledOnce();
|
|
157
|
+
expect(screen.queryByRole('img', { name: 'ellipsis' })).not.toBeInTheDocument();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it.each([true, false])('respects closeOnChange=%s for preference selectors', async (closeOnChange) => {
|
|
161
|
+
const onChange = vi.fn(async () => {});
|
|
162
|
+
const item = new UserCenterSelectItemModel({ uid: 'language', use: 'UserCenterSelectItemModel' });
|
|
163
|
+
item.label = 'Language';
|
|
164
|
+
item.value = 'en';
|
|
165
|
+
item.options = [
|
|
166
|
+
{ label: 'English', value: 'en' },
|
|
167
|
+
{ label: 'Chinese', value: 'zh' },
|
|
168
|
+
];
|
|
169
|
+
item.onChange = onChange;
|
|
170
|
+
item.closeOnChange = closeOnChange;
|
|
171
|
+
const model = createUserCenter(vi.fn());
|
|
172
|
+
model.getPreparedItems = async (closeDropdown) => {
|
|
173
|
+
item.setCloseDropdown(closeDropdown);
|
|
174
|
+
return [item];
|
|
175
|
+
};
|
|
176
|
+
render(<TopbarActionsBar actions={[model]} isMobile />);
|
|
177
|
+
fireEvent.click(screen.getByRole('img', { name: 'ellipsis' }));
|
|
178
|
+
fireEvent.mouseEnter(screen.getByTestId('user-center-button'));
|
|
179
|
+
const select = await screen.findByRole('combobox');
|
|
180
|
+
await waitFor(() => expect(select.closest('.ant-select')).toBeVisible());
|
|
181
|
+
|
|
182
|
+
fireEvent.mouseDown(select);
|
|
183
|
+
fireEvent.click(await screen.findByText('Chinese'));
|
|
184
|
+
|
|
185
|
+
await waitFor(() => expect(onChange).toHaveBeenCalledWith('zh'));
|
|
186
|
+
if (closeOnChange) {
|
|
187
|
+
await waitFor(() => expect(screen.queryByRole('tooltip')).not.toBeInTheDocument());
|
|
188
|
+
} else {
|
|
189
|
+
expect(screen.getByRole('tooltip')).toBeVisible();
|
|
190
|
+
expect(select.closest('.ant-select')).toBeVisible();
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('does not reopen the previous popover after switching to desktop and back', async () => {
|
|
195
|
+
const actions = [createUserCenter(vi.fn())];
|
|
196
|
+
const { rerender } = render(<TopbarActionsBar actions={actions} isMobile />);
|
|
197
|
+
fireEvent.click(screen.getByRole('img', { name: 'ellipsis' }));
|
|
198
|
+
await waitFor(() => expect(screen.getByRole('tooltip')).toBeVisible());
|
|
199
|
+
|
|
200
|
+
rerender(<TopbarActionsBar actions={actions} isMobile={false} />);
|
|
201
|
+
rerender(<TopbarActionsBar actions={actions} isMobile />);
|
|
202
|
+
|
|
203
|
+
expect(screen.queryByRole('tooltip')).not.toBeInTheDocument();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
@@ -751,7 +751,10 @@ async function originalHandler(ctx, params) {
|
|
|
751
751
|
|
|
752
752
|
const targetInterface = targetLabelField.getInterfaceOptions();
|
|
753
753
|
|
|
754
|
-
const operator =
|
|
754
|
+
const operator =
|
|
755
|
+
targetLabelField.type === 'string' || targetLabelField.dataType === 'string'
|
|
756
|
+
? '$includes'
|
|
757
|
+
: targetInterface?.filterable?.operators?.[0]?.value || '$includes';
|
|
755
758
|
|
|
756
759
|
const searchText = ctx.inputArgs.searchText?.trim();
|
|
757
760
|
const resource = ctx.model.resource;
|
|
@@ -590,7 +590,10 @@ async function originalHandler(ctx, params) {
|
|
|
590
590
|
|
|
591
591
|
const targetInterface = targetLabelField.getInterfaceOptions();
|
|
592
592
|
|
|
593
|
-
const operator =
|
|
593
|
+
const operator =
|
|
594
|
+
targetLabelField.type === 'string' || targetLabelField.dataType === 'string'
|
|
595
|
+
? '$includes'
|
|
596
|
+
: targetInterface?.filterable?.operators?.[0]?.value || '$includes';
|
|
594
597
|
|
|
595
598
|
const searchText = ctx.inputArgs.searchText?.trim();
|
|
596
599
|
|
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
|
+
});
|
|
@@ -11,9 +11,10 @@ import { UserOutlined } from '@ant-design/icons';
|
|
|
11
11
|
import { css, cx } from '@emotion/css';
|
|
12
12
|
import { FlowModel, FlowModelRenderer, observer } from '@nocobase/flow-engine';
|
|
13
13
|
import { Divider, Dropdown, Empty, Select, Spin, theme } from 'antd';
|
|
14
|
-
import React, { useEffect, useState } from 'react';
|
|
14
|
+
import React, { useCallback, useContext, useEffect, useState } from 'react';
|
|
15
15
|
import { ErrorBoundary } from 'react-error-boundary';
|
|
16
16
|
import { createAclSnippetAllow } from '../../../acl/createAclSnippetAllow';
|
|
17
|
+
import { MobileTopbarActionsContext } from '../../admin-shell/admin-layout/MobileTopbarActionsContext';
|
|
17
18
|
import { TopbarActionModel } from './TopbarActionModel';
|
|
18
19
|
|
|
19
20
|
export const USER_CENTER_ACTION_ID = 'user-center';
|
|
@@ -452,6 +453,11 @@ const UserCenterTopbarActionView = observer(
|
|
|
452
453
|
(props: { model: UserCenterTopbarActionModel }) => {
|
|
453
454
|
const { model } = props;
|
|
454
455
|
const [open, setOpen] = useState(false);
|
|
456
|
+
const closeMobileTopbar = useContext(MobileTopbarActionsContext);
|
|
457
|
+
const closeDropdown = useCallback(() => {
|
|
458
|
+
setOpen(false);
|
|
459
|
+
closeMobileTopbar?.();
|
|
460
|
+
}, [closeMobileTopbar]);
|
|
455
461
|
|
|
456
462
|
return model.renderWrapper(
|
|
457
463
|
<Dropdown
|
|
@@ -459,9 +465,7 @@ const UserCenterTopbarActionView = observer(
|
|
|
459
465
|
placement="bottomRight"
|
|
460
466
|
open={open}
|
|
461
467
|
onOpenChange={setOpen}
|
|
462
|
-
dropdownRender={() =>
|
|
463
|
-
<UserCenterDropdownPanel model={model} open={open} closeDropdown={() => setOpen(false)} />
|
|
464
|
-
)}
|
|
468
|
+
dropdownRender={() => <UserCenterDropdownPanel model={model} open={open} closeDropdown={closeDropdown} />}
|
|
465
469
|
>
|
|
466
470
|
<span className={userCenterTriggerClassName}>{model.renderButton()}</span>
|
|
467
471
|
</Dropdown>,
|