@nocobase/app-template-default 1.0.0-beta.75 → 1.0.0-beta.77
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/.agents/rules/build-and-deploy.md +4 -0
- package/.agents/rules/components.md +3 -1
- package/README.MD +2 -2
- package/client/components/data-table/data-table.tsx +2 -0
- package/client/components/data-table/pagination.tsx +6 -6
- package/client/components/default-access-editor.tsx +79 -52
- package/client/components/flow-diagram.tsx +645 -83
- package/client/components/flow-layout.ts +117 -7
- package/client/components/lifecycle-diagram.tsx +175 -37
- package/client/components/long-text.tsx +200 -0
- package/client/components/permission-editor.tsx +534 -341
- package/client/components/record-link.tsx +20 -0
- package/client/components/required-role-setup.tsx +93 -0
- package/client/components/role-list.tsx +21 -5
- package/client/components/setup-readiness-state.ts +135 -0
- package/client/components/setup-readiness.tsx +91 -0
- package/client/components/ui/combobox.tsx +300 -0
- package/client/extensions/nocobase-workflow-management/README.md +3 -1
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +19 -13
- package/client/extensions/nocobase-workflow-management/layout.tsx +1 -1
- package/client/hooks/use-table-url-state.ts +93 -0
- package/client/lib/required-role-setup.ts +98 -0
- package/client/lib/return-to.ts +19 -0
- package/client/locales/system/en-US.ts +10 -0
- package/client/locales/system/settings-en-US.ts +51 -4
- package/client/locales/system/settings-zh-CN.ts +46 -5
- package/client/locales/system/zh-CN.ts +10 -0
- package/client/pages/settings/default-access-form.tsx +14 -2
- package/client/pages/settings/default-access-model.ts +6 -2
- package/client/pages/settings/default-access.tsx +40 -8
- package/client/pages/settings/page-permissions.ts +166 -1
- package/client/pages/settings/permission-filters.tsx +9 -2
- package/client/pages/settings/permission-workspace.ts +330 -0
- package/client/pages/settings/permissions-model.ts +62 -53
- package/client/pages/settings/role-form.tsx +31 -95
- package/client/pages/settings/roles.tsx +44 -6
- package/client/pages/settings/shared.tsx +15 -13
- package/client/pages/settings/user-detail.tsx +15 -259
- package/client/pages/settings/user-form.tsx +299 -147
- package/client/pages/settings/users.tsx +211 -123
- package/config.example.yml +6 -5
- package/package.json +17 -17
- package/server/config/app.ts +1 -1
- package/server/config/session.ts +7 -5
- package/tests/components/default-access-editor.test.tsx +130 -0
- package/tests/components/default-access.test.tsx +141 -2
- package/tests/components/permission-editor.test.tsx +613 -572
- package/tests/components/permission-filter-binding.test.tsx +20 -0
- package/tests/components/settings-continuity.test.tsx +426 -0
- package/tests/components/settings-feedback.test.tsx +304 -0
- package/tests/components/settings.test.tsx +34 -9
- package/tests/components/setup-readiness.test.tsx +370 -0
- package/tests/components/table-url-state.test.tsx +95 -0
- package/tests/components/user-drawer.test.tsx +526 -0
- package/tests/components/user-permissions.test.tsx +47 -13
- package/tests/logic/config.test.ts +3 -1
- package/tests/logic/page-permission-navigation.test.ts +394 -0
- package/tests/logic/permission-workspace.test.ts +570 -0
- package/tests/logic/return-to.test.ts +19 -0
- package/tests/logic/session-isolation.test.ts +250 -0
- package/tsconfig.json +1 -0
|
@@ -18,6 +18,25 @@ const catalog = {
|
|
|
18
18
|
{
|
|
19
19
|
name: 'business',
|
|
20
20
|
title: 'Business',
|
|
21
|
+
subsections: [
|
|
22
|
+
{
|
|
23
|
+
name: 'other',
|
|
24
|
+
title: 'Other',
|
|
25
|
+
resources: [
|
|
26
|
+
{
|
|
27
|
+
type: 'composite',
|
|
28
|
+
id: 'expenses.records',
|
|
29
|
+
title: 'Expenses',
|
|
30
|
+
actions: [{ name: 'other', title: 'Other' }],
|
|
31
|
+
dataScopes: {},
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: 'configuration',
|
|
39
|
+
title: 'Configuration',
|
|
21
40
|
subsections: [
|
|
22
41
|
{
|
|
23
42
|
name: 'expenses',
|
|
@@ -108,6 +127,7 @@ it('collects native custom filter parameters before changing the draft, then sav
|
|
|
108
127
|
);
|
|
109
128
|
}
|
|
110
129
|
render(<Role />);
|
|
130
|
+
await user.click(screen.getByRole('button', { name: 'Configuration' }));
|
|
111
131
|
const scope = screen.getByRole('combobox', {
|
|
112
132
|
name: 'How far View · Records reaches',
|
|
113
133
|
});
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import { answerApi, renderWithApp } from '@nocobase/app-testing/client';
|
|
2
|
+
import { apiClientToken, type ClientApplication } from '@nocobase/app-client';
|
|
3
|
+
import {
|
|
4
|
+
AuthorizationClient,
|
|
5
|
+
authorizationClientToken,
|
|
6
|
+
} from '@nocobase/app-plugin-authorization/client';
|
|
7
|
+
import { screen, waitFor, within } from '@testing-library/react';
|
|
8
|
+
import userEvent from '@testing-library/user-event';
|
|
9
|
+
import { Routes, Route, useLocation } from 'react-router';
|
|
10
|
+
import { expect, it, vi } from 'vitest';
|
|
11
|
+
import UsersPage from '../../client/pages/settings/users.js';
|
|
12
|
+
import UserDetail from '../../client/pages/settings/user-detail.js';
|
|
13
|
+
import UserForm from '../../client/pages/settings/user-form.js';
|
|
14
|
+
import RolesPage from '../../client/pages/settings/roles.js';
|
|
15
|
+
import { BackButton } from '../../client/components/back-button.js';
|
|
16
|
+
import enUS from '../../client/locales/en-US.js';
|
|
17
|
+
import zhCN from '../../client/locales/zh-CN.js';
|
|
18
|
+
const namespace = '@nocobase/continuity-test';
|
|
19
|
+
const namespaces = {
|
|
20
|
+
[namespace]: {
|
|
21
|
+
'en-US': async () => ({ default: enUS }),
|
|
22
|
+
'zh-CN': async () => ({ default: zhCN }),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
const services = (app: ClientApplication) =>
|
|
26
|
+
app.container.instance(
|
|
27
|
+
authorizationClientToken,
|
|
28
|
+
new AuthorizationClient(app.container.resolve(apiClientToken)),
|
|
29
|
+
);
|
|
30
|
+
const options = {
|
|
31
|
+
data: {
|
|
32
|
+
roleScopes: [
|
|
33
|
+
{
|
|
34
|
+
key: 'app',
|
|
35
|
+
label: 'Application',
|
|
36
|
+
selection: 'multiple',
|
|
37
|
+
requiredOnCreate: false,
|
|
38
|
+
options: [{ value: 'sales', label: 'Sales team' }],
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const row = {
|
|
44
|
+
id: 'alice',
|
|
45
|
+
name: 'Alice',
|
|
46
|
+
username: 'alice',
|
|
47
|
+
email: 'alice@example.test',
|
|
48
|
+
disabledAt: null,
|
|
49
|
+
roleScopes: { app: ['sales', 'retired-role'] },
|
|
50
|
+
};
|
|
51
|
+
function Location() {
|
|
52
|
+
const location = useLocation();
|
|
53
|
+
return (
|
|
54
|
+
<output data-testid='location'>
|
|
55
|
+
{location.pathname}
|
|
56
|
+
{location.search}
|
|
57
|
+
</output>
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
it('uses role labels and paired server filters, retaining the source and ordering when clearing filters', async () => {
|
|
62
|
+
const calls = vi.fn(({ path }: { path: string }) =>
|
|
63
|
+
path === 'authorization/permissions'
|
|
64
|
+
? { data: { unrestricted: false, permissions: [] } }
|
|
65
|
+
: path === 'users/options'
|
|
66
|
+
? options
|
|
67
|
+
: { data: [row], meta: { total: 41 } },
|
|
68
|
+
);
|
|
69
|
+
const source = '/settings/processes/loan?tab=definition#review';
|
|
70
|
+
await renderWithApp(
|
|
71
|
+
<>
|
|
72
|
+
<UsersPage />
|
|
73
|
+
<Location />
|
|
74
|
+
</>,
|
|
75
|
+
{
|
|
76
|
+
route: `/settings/users?roleScope=app&role=sales&page=2&pageSize=20&order=desc&returnTo=${encodeURIComponent(source)}`,
|
|
77
|
+
namespace,
|
|
78
|
+
namespaces,
|
|
79
|
+
services,
|
|
80
|
+
fetch: answerApi(calls),
|
|
81
|
+
},
|
|
82
|
+
);
|
|
83
|
+
expect(await screen.findByText('Sales team, retired-role')).toBeVisible();
|
|
84
|
+
expect(
|
|
85
|
+
screen.getByRole('link', { name: 'Return to previous page' }),
|
|
86
|
+
).toHaveAttribute('href', source);
|
|
87
|
+
expect(screen.getByText('Alice', { exact: true })).toBeVisible();
|
|
88
|
+
expect(screen.queryByRole('link', { name: 'Alice' })).not.toBeInTheDocument();
|
|
89
|
+
expect(calls).toHaveBeenCalledWith(
|
|
90
|
+
expect.objectContaining({
|
|
91
|
+
path: 'users',
|
|
92
|
+
query: expect.objectContaining({
|
|
93
|
+
roleScope: 'app',
|
|
94
|
+
role: 'sales',
|
|
95
|
+
page: '2',
|
|
96
|
+
order: 'desc',
|
|
97
|
+
}),
|
|
98
|
+
}),
|
|
99
|
+
);
|
|
100
|
+
await userEvent.click(screen.getByRole('button', { name: 'Clear filters' }));
|
|
101
|
+
await waitFor(() =>
|
|
102
|
+
expect(screen.getByTestId('location').textContent).not.toContain(
|
|
103
|
+
'roleScope=',
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
const after = screen.getByTestId('location').textContent!;
|
|
107
|
+
expect(after).toContain('pageSize=20');
|
|
108
|
+
expect(after).toContain('order=desc');
|
|
109
|
+
expect(after).toContain('returnTo=');
|
|
110
|
+
await waitFor(() =>
|
|
111
|
+
expect(calls).toHaveBeenCalledWith(
|
|
112
|
+
expect.objectContaining({
|
|
113
|
+
path: 'users',
|
|
114
|
+
query: { page: '1', pageSize: '20', sort: 'name', order: 'desc' },
|
|
115
|
+
}),
|
|
116
|
+
),
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('does not silently discard an incomplete role filter or expose an external return link', async () => {
|
|
121
|
+
const calls = vi.fn(
|
|
122
|
+
({ path, query }: { path: string; query?: Record<string, string> }) => {
|
|
123
|
+
if (path === 'authorization/permissions')
|
|
124
|
+
return { data: { unrestricted: false, permissions: [] } };
|
|
125
|
+
if (path === 'users/options') return options;
|
|
126
|
+
if (query?.role)
|
|
127
|
+
return Response.json(
|
|
128
|
+
{ error: { code: 'INVALID_ARGUMENT', message: 'Invalid filter' } },
|
|
129
|
+
{ status: 400 },
|
|
130
|
+
);
|
|
131
|
+
return { data: [row], meta: { total: 1 } };
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
await renderWithApp(<UsersPage />, {
|
|
135
|
+
route: '/settings/users?role=sales&returnTo=https%3A%2F%2Foutside.test',
|
|
136
|
+
namespace,
|
|
137
|
+
namespaces,
|
|
138
|
+
services,
|
|
139
|
+
fetch: answerApi(calls),
|
|
140
|
+
});
|
|
141
|
+
expect(
|
|
142
|
+
await screen.findByText(enUS.settings.roleFilterIncomplete),
|
|
143
|
+
).toBeVisible();
|
|
144
|
+
await waitFor(() =>
|
|
145
|
+
expect(calls).toHaveBeenCalledWith(
|
|
146
|
+
expect.objectContaining({
|
|
147
|
+
path: 'users',
|
|
148
|
+
query: expect.objectContaining({ role: 'sales' }),
|
|
149
|
+
}),
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
expect(screen.queryByRole('link', { name: 'Alice' })).not.toBeInTheDocument();
|
|
153
|
+
expect(
|
|
154
|
+
screen.queryByRole('link', { name: 'Return to previous page' }),
|
|
155
|
+
).not.toBeInTheDocument();
|
|
156
|
+
expect(
|
|
157
|
+
calls.mock.calls
|
|
158
|
+
.filter(([call]) => call.path === 'users')
|
|
159
|
+
.every(([call]) => call.query?.role === 'sales'),
|
|
160
|
+
).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('resolves localized role descriptors through their namespace without rewriting unknown assignments', async () => {
|
|
164
|
+
const localized = {
|
|
165
|
+
data: {
|
|
166
|
+
roleScopes: [
|
|
167
|
+
{
|
|
168
|
+
...options.data.roleScopes[0],
|
|
169
|
+
options: [
|
|
170
|
+
{
|
|
171
|
+
value: 'sales',
|
|
172
|
+
label: 'Fallback',
|
|
173
|
+
labelI18nKey: 'settings.roles',
|
|
174
|
+
labelI18nNs: namespace,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
await renderWithApp(<UsersPage />, {
|
|
182
|
+
locale: 'zh-CN',
|
|
183
|
+
namespace,
|
|
184
|
+
namespaces,
|
|
185
|
+
services,
|
|
186
|
+
fetch: answerApi(({ path }) =>
|
|
187
|
+
path === 'authorization/permissions'
|
|
188
|
+
? { data: { unrestricted: false, permissions: [] } }
|
|
189
|
+
: path === 'users/options'
|
|
190
|
+
? localized
|
|
191
|
+
: { data: [row], meta: { total: 1 } },
|
|
192
|
+
),
|
|
193
|
+
});
|
|
194
|
+
expect(
|
|
195
|
+
await screen.findByText(`${zhCN.settings.roles}, retired-role`),
|
|
196
|
+
).toBeVisible();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it.each(['/settings/users', '/users'])(
|
|
200
|
+
'keeps a failed edit draft, updates the actual user on retry, and closes to the same filtered list at %s',
|
|
201
|
+
async (base) => {
|
|
202
|
+
const user = userEvent.setup();
|
|
203
|
+
let current = row;
|
|
204
|
+
let fail = true;
|
|
205
|
+
const calls = vi.fn(
|
|
206
|
+
({
|
|
207
|
+
path,
|
|
208
|
+
method,
|
|
209
|
+
json,
|
|
210
|
+
}: {
|
|
211
|
+
path: string;
|
|
212
|
+
method: string;
|
|
213
|
+
json?: { name: string };
|
|
214
|
+
}) => {
|
|
215
|
+
if (path === 'authorization/permissions')
|
|
216
|
+
return { data: { unrestricted: true, permissions: [] } };
|
|
217
|
+
if (path === 'users/options') return options;
|
|
218
|
+
if (path === 'users/alice' && method === 'PATCH') {
|
|
219
|
+
if (fail) {
|
|
220
|
+
fail = false;
|
|
221
|
+
return Response.json(
|
|
222
|
+
{
|
|
223
|
+
error: {
|
|
224
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
225
|
+
message: 'Unavailable',
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{ status: 500 },
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
current = { ...current, name: json!.name };
|
|
232
|
+
return { data: current };
|
|
233
|
+
}
|
|
234
|
+
if (path === 'users/alice') return { data: current };
|
|
235
|
+
return { data: [current], meta: { total: 41 } };
|
|
236
|
+
},
|
|
237
|
+
);
|
|
238
|
+
const search = '?page=2&pageSize=20&roleScope=app&role=sales';
|
|
239
|
+
await renderWithApp(
|
|
240
|
+
<>
|
|
241
|
+
<Routes>
|
|
242
|
+
<Route path={base} element={<UsersPage />}>
|
|
243
|
+
<Route path=':userId' element={<UserDetail />}>
|
|
244
|
+
<Route path='edit' element={<UserForm />} />
|
|
245
|
+
</Route>
|
|
246
|
+
</Route>
|
|
247
|
+
</Routes>
|
|
248
|
+
<Location />
|
|
249
|
+
</>,
|
|
250
|
+
{
|
|
251
|
+
route: `${base}/alice/edit${search}`,
|
|
252
|
+
namespace,
|
|
253
|
+
namespaces,
|
|
254
|
+
services,
|
|
255
|
+
fetch: answerApi(calls),
|
|
256
|
+
},
|
|
257
|
+
);
|
|
258
|
+
const name = await screen.findByRole('textbox', { name: 'Name *' });
|
|
259
|
+
await waitFor(() => expect(name).toHaveValue('Alice'));
|
|
260
|
+
await user.clear(name);
|
|
261
|
+
await user.type(name, 'Alice updated');
|
|
262
|
+
await user.click(
|
|
263
|
+
screen.getByRole('button', {
|
|
264
|
+
name: 'Save',
|
|
265
|
+
exact: true,
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
expect(await screen.findByText(enUS.settings.failed)).toBeVisible();
|
|
269
|
+
expect(name).toHaveValue('Alice updated');
|
|
270
|
+
expect(screen.getByTestId('location')).toHaveTextContent('/edit');
|
|
271
|
+
await user.click(
|
|
272
|
+
screen.getByRole('button', {
|
|
273
|
+
name: 'Save',
|
|
274
|
+
exact: true,
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
await waitFor(() =>
|
|
278
|
+
expect(screen.queryByRole('dialog')).not.toBeInTheDocument(),
|
|
279
|
+
);
|
|
280
|
+
await waitFor(() =>
|
|
281
|
+
expect(screen.getByTestId('location')).toHaveTextContent(
|
|
282
|
+
`${base}${search}`,
|
|
283
|
+
),
|
|
284
|
+
);
|
|
285
|
+
expect(
|
|
286
|
+
await screen.findByText('Alice updated', { exact: true }),
|
|
287
|
+
).toBeVisible();
|
|
288
|
+
},
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
it('restores a controlled role table from URL after unmounting a detail view', async () => {
|
|
292
|
+
const roles = Array.from({ length: 25 }, (_, i) => ({
|
|
293
|
+
key: `role-${i}`,
|
|
294
|
+
title: `Role ${String(i).padStart(2, '0')}`,
|
|
295
|
+
grants: [],
|
|
296
|
+
}));
|
|
297
|
+
const user = userEvent.setup();
|
|
298
|
+
await renderWithApp(
|
|
299
|
+
<>
|
|
300
|
+
<Routes>
|
|
301
|
+
<Route path='/settings/roles' element={<RolesPage />} />
|
|
302
|
+
<Route
|
|
303
|
+
path='/settings/roles/:id/edit'
|
|
304
|
+
element={
|
|
305
|
+
<BackButton
|
|
306
|
+
to={{
|
|
307
|
+
pathname: '/settings/roles',
|
|
308
|
+
search: '?page=2&pageSize=10&sort=title&order=desc',
|
|
309
|
+
}}
|
|
310
|
+
/>
|
|
311
|
+
}
|
|
312
|
+
/>
|
|
313
|
+
</Routes>
|
|
314
|
+
<Location />
|
|
315
|
+
</>,
|
|
316
|
+
{
|
|
317
|
+
route: '/settings/roles?page=2&pageSize=10&sort=title&order=desc',
|
|
318
|
+
namespace,
|
|
319
|
+
namespaces,
|
|
320
|
+
services,
|
|
321
|
+
fetch: answerApi(({ path }) =>
|
|
322
|
+
path === 'authorization/permissions'
|
|
323
|
+
? { data: { unrestricted: false, permissions: [] } }
|
|
324
|
+
: { data: roles },
|
|
325
|
+
),
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
const link = await screen.findByRole('link', {
|
|
329
|
+
name: 'Role 14',
|
|
330
|
+
exact: true,
|
|
331
|
+
});
|
|
332
|
+
expect(screen.getByText('Page 2 of 3')).toBeVisible();
|
|
333
|
+
expect(
|
|
334
|
+
within(screen.getAllByRole('row')[1]!).getByText('Role 14'),
|
|
335
|
+
).toBeVisible();
|
|
336
|
+
await user.click(link);
|
|
337
|
+
await user.click(screen.getByRole('link', { name: 'Back' }));
|
|
338
|
+
expect(
|
|
339
|
+
await screen.findByRole('link', { name: 'Role 14', exact: true }),
|
|
340
|
+
).toBeVisible();
|
|
341
|
+
expect(screen.getByText('Page 2 of 3')).toBeVisible();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('keeps loaded users visible when role labels fail and provides retry without an empty actions menu', async () => {
|
|
345
|
+
await renderWithApp(<UsersPage />, {
|
|
346
|
+
namespace,
|
|
347
|
+
namespaces,
|
|
348
|
+
services,
|
|
349
|
+
fetch: answerApi(({ path }) =>
|
|
350
|
+
path === 'authorization/permissions'
|
|
351
|
+
? { data: { unrestricted: false, permissions: [] } }
|
|
352
|
+
: path === 'users/options'
|
|
353
|
+
? Response.json(
|
|
354
|
+
{
|
|
355
|
+
error: {
|
|
356
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
357
|
+
message: 'Unavailable',
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
{ status: 500 },
|
|
361
|
+
)
|
|
362
|
+
: { data: [row], meta: { total: 1 } },
|
|
363
|
+
),
|
|
364
|
+
});
|
|
365
|
+
expect(await screen.findByText('Alice', { exact: true })).toBeVisible();
|
|
366
|
+
expect(await screen.findByText('sales, retired-role')).toBeVisible();
|
|
367
|
+
expect(screen.getByRole('button', { name: 'Retry' })).toBeVisible();
|
|
368
|
+
expect(
|
|
369
|
+
screen.queryByRole('button', { name: 'Actions for Alice' }),
|
|
370
|
+
).not.toBeInTheDocument();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('refreshes a filtered list after disabling a user, corrects an empty last page and restores search focus', async () => {
|
|
374
|
+
let disabled = false;
|
|
375
|
+
const user = userEvent.setup();
|
|
376
|
+
await renderWithApp(
|
|
377
|
+
<>
|
|
378
|
+
<Routes>
|
|
379
|
+
<Route path='/settings/users' element={<UsersPage />} />
|
|
380
|
+
</Routes>
|
|
381
|
+
<Location />
|
|
382
|
+
</>,
|
|
383
|
+
{
|
|
384
|
+
route: '/settings/users?status=enabled&page=2&pageSize=20',
|
|
385
|
+
namespace,
|
|
386
|
+
namespaces,
|
|
387
|
+
services,
|
|
388
|
+
fetch: answerApi(({ path }) => {
|
|
389
|
+
if (path === 'authorization/permissions')
|
|
390
|
+
return { data: { unrestricted: true, permissions: [] } };
|
|
391
|
+
if (path === 'users/options') return options;
|
|
392
|
+
if (path === 'users/alice/disable') {
|
|
393
|
+
disabled = true;
|
|
394
|
+
return { data: { ...row, disabledAt: '2026-10-21T00:00:00Z' } };
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
data: disabled ? [] : [row],
|
|
398
|
+
meta: { total: disabled ? 20 : 21 },
|
|
399
|
+
};
|
|
400
|
+
}),
|
|
401
|
+
},
|
|
402
|
+
);
|
|
403
|
+
await user.click(
|
|
404
|
+
await screen.findByRole('button', { name: 'Disable', exact: true }),
|
|
405
|
+
);
|
|
406
|
+
await user.click(
|
|
407
|
+
within(screen.getByRole('alertdialog')).getByRole('button', {
|
|
408
|
+
name: 'Disable',
|
|
409
|
+
exact: true,
|
|
410
|
+
}),
|
|
411
|
+
);
|
|
412
|
+
await waitFor(() =>
|
|
413
|
+
expect(
|
|
414
|
+
screen.queryByText('Alice', { exact: true }),
|
|
415
|
+
).not.toBeInTheDocument(),
|
|
416
|
+
);
|
|
417
|
+
await waitFor(() =>
|
|
418
|
+
expect(screen.getByTestId('location')).toHaveTextContent('page=1'),
|
|
419
|
+
);
|
|
420
|
+
expect(screen.getByTestId('location')).toHaveTextContent('status=enabled');
|
|
421
|
+
await waitFor(() =>
|
|
422
|
+
expect(
|
|
423
|
+
screen.getByRole('textbox', { name: enUS.settings.userSearch }),
|
|
424
|
+
).toHaveFocus(),
|
|
425
|
+
);
|
|
426
|
+
});
|