@nocobase/app-template-default 1.0.0-beta.83 → 1.0.0-beta.85
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 +1 -1
- package/.agents/rules/shell.md +4 -0
- package/AGENTS.md +8 -2
- package/README.MD +10 -2
- package/client/components/back-button.tsx +3 -5
- package/client/components/data-table/data-table.tsx +86 -32
- package/client/components/default-access-editor.tsx +13 -2
- package/client/components/feedback-form.tsx +474 -0
- package/client/components/lifecycle-diagram.tsx +10 -10
- package/client/components/list-feedback.tsx +174 -0
- package/client/components/permission-editor.tsx +419 -226
- package/client/extensions/nocobase-auth-forms/password-login-form.tsx +17 -6
- package/client/extensions/nocobase-auth-forms/password-registration-form.tsx +24 -7
- package/client/extensions/nocobase-auth-forms/password-reset-form.tsx +19 -7
- package/client/extensions/nocobase-auth-forms/password-reset-request-form.tsx +15 -7
- package/client/extensions/nocobase-device-approval/device-approval.tsx +7 -2
- package/client/extensions/nocobase-device-approval/locales/en-US.ts +16 -0
- package/client/extensions/nocobase-device-approval/locales/zh-CN.ts +7 -0
- package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +20 -17
- package/client/extensions/nocobase-workflow-management/README.md +3 -1
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +19 -7
- package/client/extensions/nocobase-workflow-management/execution-list.tsx +61 -48
- package/client/hooks/use-list-data.ts +76 -0
- package/client/hooks/use-list-location.ts +23 -0
- package/client/hooks/use-list-tabs.ts +42 -0
- package/client/hooks/use-table-url-state.ts +143 -30
- package/client/locales/system/en-US.ts +24 -1
- package/client/locales/system/settings-en-US.ts +8 -10
- package/client/locales/system/settings-zh-CN.ts +8 -8
- package/client/locales/system/zh-CN.ts +22 -1
- package/client/pages/settings/default-access-form.tsx +44 -26
- package/client/pages/settings/default-access.tsx +100 -93
- package/client/pages/settings/hooks.tsx +4 -59
- package/client/pages/settings/page-permissions.ts +17 -0
- package/client/pages/settings/permissions-model.ts +54 -59
- package/client/pages/settings/role-form.tsx +98 -56
- package/client/pages/settings/roles.tsx +69 -95
- package/client/pages/settings/shared.tsx +43 -33
- package/client/pages/settings/user-form.tsx +51 -30
- package/client/pages/settings/users.tsx +105 -91
- package/client/routing/session-preparation.tsx +10 -3
- package/client/runtime.ts +0 -4
- package/config.example.yml +24 -0
- package/database/main/migrations/202611020002_create_mutation_receipts.ts +19 -0
- package/package.json +24 -21
- package/server/app.ts +0 -2
- package/server/config/auth.ts +2 -0
- package/server/config/concurrency.ts +3 -0
- package/server/config/index.ts +3 -0
- package/server/config/jobs.ts +1 -13
- package/server/config/notification.ts +1 -1
- package/server/config/queue.ts +1 -7
- package/server/plugins.ts +2 -0
- package/server/providers/index.ts +4 -1
- package/server/providers/submission.ts +61 -0
- package/tests/client/auth-i18n.test.tsx +9 -0
- package/tests/client/back-button.test.tsx +3 -3
- package/tests/client/data-table.test.tsx +9 -4
- package/tests/client/default-access.test.tsx +99 -5
- package/tests/client/form-feedback.test.tsx +589 -0
- package/tests/client/list-feedback.test.tsx +301 -0
- package/tests/client/list-recovery.test.tsx +373 -0
- package/tests/client/list-tabs.test.tsx +121 -0
- package/tests/client/manual-list-retry.test.tsx +190 -0
- package/tests/client/page-permission-navigation.test.ts +8 -0
- package/tests/client/permission-editor.test.tsx +191 -21
- package/tests/client/permission-filter-binding.test.tsx +1 -1
- package/tests/client/permission-workspace.test.ts +13 -0
- package/tests/client/role-return.test.tsx +2 -1
- package/tests/client/session-preparation.test.tsx +24 -1
- package/tests/client/settings-continuity.test.tsx +85 -5
- package/tests/client/settings-feedback.test.tsx +32 -15
- package/tests/client/settings-permissions.test.ts +35 -0
- package/tests/client/settings.test.tsx +2 -1
- package/tests/client/table-url-state.test.tsx +117 -3
- package/tests/client/user-drawer.test.tsx +2 -1
- package/tests/client/user-permissions.test.tsx +30 -5
- package/tests/database/mutation-receipts.test.ts +45 -0
- package/tests/fixtures/form-feedback/README.md +33 -0
- package/tests/fixtures/form-feedback/equipment-detail.tsx +11 -0
- package/tests/fixtures/form-feedback/equipment-layout.tsx +15 -0
- package/tests/fixtures/form-feedback/equipment-migration.ts +27 -0
- package/tests/fixtures/form-feedback/equipment-server.ts +197 -0
- package/tests/fixtures/form-feedback/equipment-validation.tsx +94 -0
- package/tests/fixtures/form-feedback/equipment.tsx +369 -0
- package/tests/fixtures/form-feedback/verify-inline.mjs +174 -0
- package/tests/fixtures/form-feedback/verify.mjs +373 -0
- package/tests/fixtures/permission-alignment.html +16 -0
- package/tests/fixtures/permission-alignment.tsx +124 -0
- package/tests/fixtures/submission/README.md +35 -0
- package/tests/fixtures/submission/browser.test.ts +478 -0
- package/tests/fixtures/submission/migration.ts +61 -0
- package/tests/fixtures/submission/page.tsx +138 -0
- package/tests/fixtures/submission/playwright.config.ts +11 -0
- package/tests/fixtures/submission/routes.ts +208 -0
- package/tests/helpers/settings-client.tsx +31 -0
- package/tests/playwright/permission-alignment.test.ts +109 -0
- package/tests/server/auth-email-config.test.ts +79 -0
- package/tests/server/concurrency.test.ts +61 -0
- package/tests/server/config.test.ts +34 -14
- package/tests/server/member-role.test.ts +7 -1
- package/tests/server/submission-settings.test.ts +241 -0
- package/client/route-overrides.ts +0 -9
- package/client/source-extensions.ts +0 -19
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { FeedbackForm } from '#components/feedback-form';
|
|
2
|
+
import { useAuthentication } from '@nocobase/app-plugin-authentication/client';
|
|
3
|
+
import { useSubmission } from '@nocobase/app-client';
|
|
1
4
|
import {
|
|
2
5
|
useSettingsLeave,
|
|
3
6
|
useSettingsData,
|
|
@@ -267,7 +270,12 @@ function UserEditor(): ReactElement {
|
|
|
267
270
|
const [confirmRoles, setConfirmRoles] = useState(false);
|
|
268
271
|
const formRef = useRef<HTMLFormElement>(null);
|
|
269
272
|
const [dirty, setDirty] = useState(false);
|
|
270
|
-
const
|
|
273
|
+
const { session } = useAuthentication();
|
|
274
|
+
const submission = useSubmission({
|
|
275
|
+
scope: `${session?.user.id}:settings:user:${userId}`,
|
|
276
|
+
});
|
|
277
|
+
const [requestBusy, setRequestBusy] = useState(false);
|
|
278
|
+
const busy = requestBusy || submission.submitting;
|
|
271
279
|
const leave = useSettingsLeave(dirty, busy);
|
|
272
280
|
const [error, setError] = useState<unknown>();
|
|
273
281
|
const [serverErrors, setServerErrors] = useState<Record<string, string>>({});
|
|
@@ -332,34 +340,43 @@ function UserEditor(): ReactElement {
|
|
|
332
340
|
setConfirmRoles(true);
|
|
333
341
|
return;
|
|
334
342
|
}
|
|
335
|
-
|
|
343
|
+
setRequestBusy(true);
|
|
336
344
|
setError(undefined);
|
|
337
345
|
setServerErrors({});
|
|
338
346
|
try {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
:
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
}
|
|
347
|
+
const input = creating
|
|
348
|
+
? {
|
|
349
|
+
...fields,
|
|
350
|
+
name: fields.name.trim(),
|
|
351
|
+
email: fields.email.trim(),
|
|
352
|
+
...(fields.username.trim()
|
|
353
|
+
? { username: fields.username.trim() }
|
|
354
|
+
: { username: undefined }),
|
|
355
|
+
roleScopes: roles,
|
|
356
|
+
}
|
|
357
|
+
: {
|
|
358
|
+
...(canUpdate
|
|
359
|
+
? {
|
|
360
|
+
name: fields.name.trim(),
|
|
361
|
+
email: fields.email.trim(),
|
|
362
|
+
username: fields.username.trim() || null,
|
|
363
|
+
}
|
|
364
|
+
: {}),
|
|
365
|
+
...(canAssign && Object.keys(changedRoles).length
|
|
366
|
+
? { roleScopes: changedRoles }
|
|
367
|
+
: {}),
|
|
368
|
+
};
|
|
369
|
+
const updated = await submission.run(
|
|
370
|
+
{ userId, creating, input },
|
|
371
|
+
(idempotencyKey) =>
|
|
372
|
+
creating
|
|
373
|
+
? client.create(
|
|
374
|
+
input as Parameters<typeof client.create>[0],
|
|
375
|
+
idempotencyKey,
|
|
376
|
+
)
|
|
377
|
+
: client.update(userId, input, idempotencyKey),
|
|
378
|
+
);
|
|
379
|
+
if (!updated) return;
|
|
363
380
|
setDirty(false);
|
|
364
381
|
parent.reload(updated);
|
|
365
382
|
toaster.show({
|
|
@@ -395,7 +412,7 @@ function UserEditor(): ReactElement {
|
|
|
395
412
|
setError(next);
|
|
396
413
|
}
|
|
397
414
|
} finally {
|
|
398
|
-
|
|
415
|
+
setRequestBusy(false);
|
|
399
416
|
}
|
|
400
417
|
};
|
|
401
418
|
const allowed = creating ? canCreate && canAssign : canUpdate || canAssign;
|
|
@@ -453,12 +470,14 @@ function UserEditor(): ReactElement {
|
|
|
453
470
|
<>
|
|
454
471
|
<SettingsError error={data.error} retry={data.reload} />
|
|
455
472
|
<SettingsError error={options.error} retry={options.reload} />
|
|
456
|
-
<SettingsError error={error} />
|
|
457
473
|
{!creating && data.loading ? (
|
|
458
474
|
<SettingsLoading />
|
|
459
475
|
) : (
|
|
460
476
|
!data.error && (
|
|
461
|
-
<
|
|
477
|
+
<FeedbackForm
|
|
478
|
+
submitting={busy}
|
|
479
|
+
error={error}
|
|
480
|
+
errorContent={<SettingsError error={error} inline />}
|
|
462
481
|
id={formId}
|
|
463
482
|
ref={formRef}
|
|
464
483
|
onSubmit={(event) => {
|
|
@@ -510,6 +529,7 @@ function UserEditor(): ReactElement {
|
|
|
510
529
|
/>
|
|
511
530
|
{fieldError(name) && (
|
|
512
531
|
<p
|
|
532
|
+
data-feedback-field-error
|
|
513
533
|
id={`${formId}-${name}-error`}
|
|
514
534
|
className='text-xs text-destructive'
|
|
515
535
|
>
|
|
@@ -536,6 +556,7 @@ function UserEditor(): ReactElement {
|
|
|
536
556
|
/>
|
|
537
557
|
{serverErrors[scope.key] && (
|
|
538
558
|
<p
|
|
559
|
+
data-feedback-field-error
|
|
539
560
|
id={`${formId}-roles-${encodeURIComponent(scope.key)}-error`}
|
|
540
561
|
className='text-xs text-destructive'
|
|
541
562
|
>
|
|
@@ -544,7 +565,7 @@ function UserEditor(): ReactElement {
|
|
|
544
565
|
)}
|
|
545
566
|
</div>
|
|
546
567
|
))}
|
|
547
|
-
</
|
|
568
|
+
</FeedbackForm>
|
|
548
569
|
)
|
|
549
570
|
)}
|
|
550
571
|
{leave.dialog}
|
|
@@ -1,11 +1,20 @@
|
|
|
1
|
+
import { useListLocation } from '#hooks/use-list-location';
|
|
1
2
|
import { RecordLink } from '#components/record-link';
|
|
2
3
|
import { BackButton } from '#components/back-button';
|
|
3
4
|
import { safeReturnTo } from '../../lib/return-to.js';
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
readTableUrlState,
|
|
7
|
+
useTableUrlState,
|
|
8
|
+
} from '#hooks/use-table-url-state';
|
|
5
9
|
import { DataTable, DataTableColumnHeader } from '#components/data-table';
|
|
6
10
|
import type { ColumnDef } from '@tanstack/react-table';
|
|
7
11
|
import { useSettingsData, useUserPermission } from './hooks.js';
|
|
8
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
resolveListState,
|
|
14
|
+
useApiClient,
|
|
15
|
+
useSubmission,
|
|
16
|
+
useToaster,
|
|
17
|
+
} from '@nocobase/app-client';
|
|
9
18
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
10
19
|
import {
|
|
11
20
|
UsersClient,
|
|
@@ -20,19 +29,14 @@ import {
|
|
|
20
29
|
useEffect,
|
|
21
30
|
type ReactElement,
|
|
22
31
|
} from 'react';
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
Outlet,
|
|
26
|
-
useLocation,
|
|
27
|
-
useSearchParams,
|
|
28
|
-
useResolvedPath,
|
|
29
|
-
} from 'react-router';
|
|
30
|
-
import { PlusIcon, UsersIcon } from 'lucide-react';
|
|
32
|
+
import { Link, Outlet, useLocation, useResolvedPath } from 'react-router';
|
|
33
|
+
import { PlusIcon } from 'lucide-react';
|
|
31
34
|
import { PageContainer } from '#components/page-container';
|
|
32
35
|
import { PageHeader } from '#components/page-header';
|
|
33
36
|
import { Button } from '#components/ui/button';
|
|
34
37
|
import { Input } from '#components/ui/input';
|
|
35
38
|
import { StatusBadge } from '#components/status-badge';
|
|
39
|
+
import { Spinner } from '#components/ui/spinner';
|
|
36
40
|
import {
|
|
37
41
|
Select,
|
|
38
42
|
SelectTrigger,
|
|
@@ -40,14 +44,26 @@ import {
|
|
|
40
44
|
SelectContent,
|
|
41
45
|
SelectItem,
|
|
42
46
|
} from '#components/ui/select';
|
|
43
|
-
import {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
import { ConfirmAction, SettingsError } from './shared.js';
|
|
48
|
+
|
|
49
|
+
function UsersRefreshing(): ReactElement | null {
|
|
50
|
+
const { t } = useTranslation();
|
|
51
|
+
const [visible, setVisible] = useState(false);
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
// Fast background reads should not flash feedback on overlay close.
|
|
54
|
+
const timer = setTimeout(() => setVisible(true), 300);
|
|
55
|
+
return () => clearTimeout(timer);
|
|
56
|
+
}, []);
|
|
57
|
+
return visible ? (
|
|
58
|
+
<span
|
|
59
|
+
role='status'
|
|
60
|
+
className='flex items-center gap-2 text-sm text-muted-foreground'
|
|
61
|
+
>
|
|
62
|
+
<Spinner aria-hidden='true' className='size-4' />
|
|
63
|
+
{t('settings.loading')}
|
|
64
|
+
</span>
|
|
65
|
+
) : null;
|
|
66
|
+
}
|
|
51
67
|
|
|
52
68
|
function UserName({ user }: { readonly user: ManagedUser }): ReactElement {
|
|
53
69
|
const canUpdate = useUserPermission('update', user.id);
|
|
@@ -79,14 +95,21 @@ function UserActions({
|
|
|
79
95
|
const [action, setAction] = useState<
|
|
80
96
|
'disable' | 'enable' | 'revokeSessions'
|
|
81
97
|
>();
|
|
82
|
-
const [
|
|
98
|
+
const [requestBusy, setRequestBusy] = useState(false);
|
|
83
99
|
const [error, setError] = useState<unknown>();
|
|
100
|
+
const submission = useSubmission();
|
|
101
|
+
const busy = requestBusy || submission.submitting;
|
|
84
102
|
const run = async () => {
|
|
85
103
|
if (!action) return;
|
|
86
|
-
|
|
104
|
+
setRequestBusy(true);
|
|
87
105
|
setError(undefined);
|
|
88
106
|
try {
|
|
89
|
-
const
|
|
107
|
+
const outcome = await submission.run(
|
|
108
|
+
{ action, id: user.id },
|
|
109
|
+
async (key) => ({ updated: await client[action](user.id, key) }),
|
|
110
|
+
);
|
|
111
|
+
if (!outcome) return;
|
|
112
|
+
const updated = outcome.updated;
|
|
90
113
|
toaster.show({
|
|
91
114
|
type: 'success',
|
|
92
115
|
title: t(`settings.userActionSaved.${action}`, { name: user.name }),
|
|
@@ -96,7 +119,7 @@ function UserActions({
|
|
|
96
119
|
} catch (next) {
|
|
97
120
|
setError(next);
|
|
98
121
|
} finally {
|
|
99
|
-
|
|
122
|
+
setRequestBusy(false);
|
|
100
123
|
}
|
|
101
124
|
};
|
|
102
125
|
if (!canRevoke && !(user.disabledAt ? canEnable : canDisable)) return <></>;
|
|
@@ -156,7 +179,7 @@ function UserActions({
|
|
|
156
179
|
|
|
157
180
|
export default function UsersPage(): ReactElement {
|
|
158
181
|
const { t } = useTranslation();
|
|
159
|
-
const
|
|
182
|
+
const { params, setParams, active } = useListLocation();
|
|
160
183
|
const location = useLocation();
|
|
161
184
|
const listPath = useResolvedPath('.').pathname;
|
|
162
185
|
const searchRef = useRef<HTMLInputElement>(null);
|
|
@@ -218,16 +241,14 @@ export default function UsersPage(): ReactElement {
|
|
|
218
241
|
!roleChoices.some((option) => option.value === selectedRole)
|
|
219
242
|
)
|
|
220
243
|
roleChoices.push({ value: selectedRole, label: `${roleScope} · ${role}` });
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
244
|
+
const { paginationState, sortingState } = readTableUrlState(params, {
|
|
245
|
+
defaultPageSize: 20,
|
|
246
|
+
pageSizeOptions: [20, 50, 100],
|
|
247
|
+
sortColumns: ['name'],
|
|
248
|
+
});
|
|
249
|
+
const page = paginationState.pageIndex + 1;
|
|
227
250
|
const status = params.get('status') ?? 'all';
|
|
228
|
-
const pageSize =
|
|
229
|
-
? Number(params.get('pageSize'))
|
|
230
|
-
: 20;
|
|
251
|
+
const pageSize = paginationState.pageSize;
|
|
231
252
|
const update = (key: string, value: string) => {
|
|
232
253
|
setParams(
|
|
233
254
|
(old) => {
|
|
@@ -263,20 +284,29 @@ export default function UsersPage(): ReactElement {
|
|
|
263
284
|
...(status !== 'all' ? { status } : {}),
|
|
264
285
|
...(roleScope ? { roleScope } : {}),
|
|
265
286
|
...(role ? { role } : {}),
|
|
266
|
-
...(
|
|
267
|
-
? {
|
|
287
|
+
...(sortingState[0]
|
|
288
|
+
? {
|
|
289
|
+
sort: sortingState[0].id,
|
|
290
|
+
order: sortingState[0].desc ? 'desc' : 'asc',
|
|
291
|
+
}
|
|
268
292
|
: {}),
|
|
269
293
|
});
|
|
294
|
+
const requestPath = `users?${remote}`;
|
|
270
295
|
const request = useSettingsData<{
|
|
271
296
|
data: readonly ManagedUser[];
|
|
272
297
|
meta: { total: number };
|
|
273
|
-
}>(
|
|
298
|
+
}>(requestPath, { keepPreviousData: true, active });
|
|
299
|
+
const hasCurrentResponse = request.path === requestPath;
|
|
274
300
|
const tableState = useTableUrlState({
|
|
301
|
+
active,
|
|
302
|
+
revision: request.revision,
|
|
275
303
|
defaultPageSize: 20,
|
|
276
304
|
pageSizeOptions: [20, 50, 100],
|
|
277
305
|
sortColumns: ['name'],
|
|
278
306
|
rowCount:
|
|
279
|
-
!request.loading && !request.error
|
|
307
|
+
hasCurrentResponse && !request.loading && !request.error
|
|
308
|
+
? request.data?.meta.total
|
|
309
|
+
: undefined,
|
|
280
310
|
});
|
|
281
311
|
const clearFilters = () => {
|
|
282
312
|
setQuery('');
|
|
@@ -309,6 +339,24 @@ export default function UsersPage(): ReactElement {
|
|
|
309
339
|
const canCreate = useUserPermission('create');
|
|
310
340
|
const canAssign = useUserPermission('assign-role');
|
|
311
341
|
const rows = request.data?.data ?? [];
|
|
342
|
+
const filtered =
|
|
343
|
+
Boolean(params.get('q') || roleScope || role) || status !== 'all';
|
|
344
|
+
const listState = resolveListState(
|
|
345
|
+
{
|
|
346
|
+
data: hasCurrentResponse ? request.data : undefined,
|
|
347
|
+
error: tableState.error ?? request.error,
|
|
348
|
+
isPending: request.loading && !hasCurrentResponse,
|
|
349
|
+
},
|
|
350
|
+
hasCurrentResponse ? (request.data?.meta.total ?? rows.length) : 0,
|
|
351
|
+
filtered,
|
|
352
|
+
);
|
|
353
|
+
const canShowCreate =
|
|
354
|
+
canCreate &&
|
|
355
|
+
canAssign &&
|
|
356
|
+
(listState === 'empty' ||
|
|
357
|
+
listState === 'ready' ||
|
|
358
|
+
listState === 'no-results' ||
|
|
359
|
+
listState === 'refresh-failed');
|
|
312
360
|
const columns = useMemo<ColumnDef<ManagedUser>[]>(
|
|
313
361
|
() => [
|
|
314
362
|
{
|
|
@@ -381,8 +429,6 @@ export default function UsersPage(): ReactElement {
|
|
|
381
429
|
{t('settings.newUser')}
|
|
382
430
|
</Button>
|
|
383
431
|
) : null;
|
|
384
|
-
const filtered =
|
|
385
|
-
Boolean(params.get('q') || roleScope || role) || status !== 'all';
|
|
386
432
|
return (
|
|
387
433
|
<>
|
|
388
434
|
<PageContainer>
|
|
@@ -392,7 +438,7 @@ export default function UsersPage(): ReactElement {
|
|
|
392
438
|
<PageHeader
|
|
393
439
|
title={t('settings.users')}
|
|
394
440
|
description={t('settings.usersDescription')}
|
|
395
|
-
actions={
|
|
441
|
+
actions={canShowCreate ? create : undefined}
|
|
396
442
|
/>
|
|
397
443
|
<div className='flex flex-wrap items-center gap-3'>
|
|
398
444
|
<Input
|
|
@@ -464,69 +510,37 @@ export default function UsersPage(): ReactElement {
|
|
|
464
510
|
))}
|
|
465
511
|
</SelectContent>
|
|
466
512
|
</Select>
|
|
467
|
-
{filtered && (
|
|
513
|
+
{filtered && listState !== 'no-results' && (
|
|
468
514
|
<Button variant='ghost' onClick={clearFilters}>
|
|
469
515
|
{t('settings.clear')}
|
|
470
516
|
</Button>
|
|
471
517
|
)}
|
|
518
|
+
<div className='ml-auto flex h-8 w-28 items-center justify-end'>
|
|
519
|
+
{active && hasCurrentResponse && request.loading && request.data ? (
|
|
520
|
+
<UsersRefreshing />
|
|
521
|
+
) : null}
|
|
522
|
+
</div>
|
|
472
523
|
</div>
|
|
473
|
-
{request.data && request.loading && (
|
|
474
|
-
<span role='status' className='text-sm text-muted-foreground'>
|
|
475
|
-
{t('settings.loading')}
|
|
476
|
-
</span>
|
|
477
|
-
)}
|
|
478
524
|
{Boolean(roleScope) !== Boolean(role) && (
|
|
479
525
|
<p role='alert' className='text-sm text-destructive'>
|
|
480
526
|
{t('settings.roleFilterIncomplete')}
|
|
481
527
|
</p>
|
|
482
528
|
)}
|
|
483
529
|
<SettingsError error={options.error} retry={options.reload} />
|
|
484
|
-
<
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
showSelectedCount={false}
|
|
499
|
-
loading={request.loading}
|
|
500
|
-
/>
|
|
501
|
-
) : (
|
|
502
|
-
<Empty className='min-h-48 border border-dashed'>
|
|
503
|
-
<EmptyMedia variant='icon'>
|
|
504
|
-
<UsersIcon />
|
|
505
|
-
</EmptyMedia>
|
|
506
|
-
<EmptyTitle>
|
|
507
|
-
{t(filtered ? 'settings.noResults' : 'settings.noUsers')}
|
|
508
|
-
</EmptyTitle>
|
|
509
|
-
<EmptyDescription>
|
|
510
|
-
{t(
|
|
511
|
-
filtered
|
|
512
|
-
? 'settings.clearHint'
|
|
513
|
-
: canCreate && canAssign
|
|
514
|
-
? 'settings.usersDescription'
|
|
515
|
-
: 'settings.usersReadOnlyEmpty',
|
|
516
|
-
)}
|
|
517
|
-
</EmptyDescription>
|
|
518
|
-
<EmptyContent>
|
|
519
|
-
{filtered ? (
|
|
520
|
-
<Button variant='outline' onClick={clearFilters}>
|
|
521
|
-
{t('settings.clear')}
|
|
522
|
-
</Button>
|
|
523
|
-
) : (
|
|
524
|
-
create
|
|
525
|
-
)}
|
|
526
|
-
</EmptyContent>
|
|
527
|
-
</Empty>
|
|
528
|
-
))
|
|
529
|
-
)}
|
|
530
|
+
<DataTable<ManagedUser>
|
|
531
|
+
columns={columns}
|
|
532
|
+
data={rows}
|
|
533
|
+
getRowId={(row) => row.id}
|
|
534
|
+
manualPagination
|
|
535
|
+
rowCount={request.data?.meta.total ?? 0}
|
|
536
|
+
{...tableState}
|
|
537
|
+
manualSorting
|
|
538
|
+
showSelectedCount={false}
|
|
539
|
+
loading={request.loading}
|
|
540
|
+
listState={listState}
|
|
541
|
+
onRetry={request.reload}
|
|
542
|
+
onClearFilters={clearFilters}
|
|
543
|
+
/>
|
|
530
544
|
{request.data && (
|
|
531
545
|
<p className='text-sm text-muted-foreground'>
|
|
532
546
|
{t('settings.total', { count: request.data.meta.total })}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { useAuthentication } from '@nocobase/app-plugin-authentication/client';
|
|
2
2
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Fragment,
|
|
5
|
+
useState,
|
|
6
|
+
type PropsWithChildren,
|
|
7
|
+
type ReactElement,
|
|
8
|
+
} from 'react';
|
|
4
9
|
|
|
5
10
|
import { Loading } from '#components/loading';
|
|
6
11
|
import { Button } from '#components/ui/button';
|
|
@@ -9,13 +14,15 @@ import { Button } from '#components/ui/button';
|
|
|
9
14
|
export function SessionPreparation({
|
|
10
15
|
children,
|
|
11
16
|
}: PropsWithChildren): ReactElement {
|
|
12
|
-
const { preparation, retryPreparation, client, refresh } =
|
|
17
|
+
const { preparation, retryPreparation, client, refresh, session } =
|
|
13
18
|
useAuthentication();
|
|
14
19
|
const { t } = useTranslation();
|
|
15
20
|
const [signingOut, setSigningOut] = useState(false);
|
|
16
21
|
const [signOutFailed, setSignOutFailed] = useState(false);
|
|
17
22
|
|
|
18
|
-
|
|
23
|
+
// List memory and ancestor queries belong to one signed-in identity only.
|
|
24
|
+
if (preparation === 'ready')
|
|
25
|
+
return <Fragment key={session?.user.id}>{children}</Fragment>;
|
|
19
26
|
if (preparation === 'preparing') {
|
|
20
27
|
return <Loading className='min-h-svh' label={t('auth.preparingSession')} />;
|
|
21
28
|
}
|
package/client/runtime.ts
CHANGED
|
@@ -5,10 +5,8 @@ import { defineAppRuntime } from '@nocobase/app-client/runtime';
|
|
|
5
5
|
import locales from './locales/index.js';
|
|
6
6
|
import plugins from './plugins.js';
|
|
7
7
|
import reactProviders from './react-providers.js';
|
|
8
|
-
import routeComponentOverrides from './route-overrides.js';
|
|
9
8
|
import routes from './routes.js';
|
|
10
9
|
import serviceProviders from './service-provider.js';
|
|
11
|
-
import sourceExtensions from './source-extensions.js';
|
|
12
10
|
|
|
13
11
|
const appRuntime = defineAppRuntime({
|
|
14
12
|
packageName: '@nocobase/app-template-default',
|
|
@@ -19,8 +17,6 @@ const appRuntime = defineAppRuntime({
|
|
|
19
17
|
reactProviders,
|
|
20
18
|
routes,
|
|
21
19
|
plugins,
|
|
22
|
-
routeComponentOverrides,
|
|
23
|
-
sourceExtensions,
|
|
24
20
|
});
|
|
25
21
|
|
|
26
22
|
export default appRuntime;
|
package/config.example.yml
CHANGED
|
@@ -70,6 +70,26 @@ secrets:
|
|
|
70
70
|
key: replace-with-a-unique-secret
|
|
71
71
|
|
|
72
72
|
auth:
|
|
73
|
+
# Authentication owns this sender; notification and mailbox configuration are independent.
|
|
74
|
+
# Set app.publicOrigin (or APP_PUBLIC_ORIGIN) before enabling automatic recovery.
|
|
75
|
+
# passwordReset:
|
|
76
|
+
# enabled: true
|
|
77
|
+
# email:
|
|
78
|
+
# provider: smtp
|
|
79
|
+
# host: smtp.example.com
|
|
80
|
+
# port: 587
|
|
81
|
+
# secure: false
|
|
82
|
+
# auth:
|
|
83
|
+
# user: noreply@example.com
|
|
84
|
+
# pass: replace-with-the-smtp-password
|
|
85
|
+
# from: App <noreply@example.com>
|
|
86
|
+
# replyTo: support@example.com
|
|
87
|
+
# For Resend, replace the entire email block with these fields; do not mix SMTP fields:
|
|
88
|
+
# email:
|
|
89
|
+
# provider: resend
|
|
90
|
+
# apiKey: replace-with-the-resend-api-key
|
|
91
|
+
# from: App <noreply@example.com>
|
|
92
|
+
# AUTH_EMAIL_PROVIDER/HOST/PORT/SECURE/USER/PASSWORD/FROM/REPLY_TO/API_KEY override auth.email.
|
|
73
93
|
emailAndPassword:
|
|
74
94
|
enabled: true
|
|
75
95
|
autoSignIn: false
|
|
@@ -146,9 +166,13 @@ snowflake:
|
|
|
146
166
|
# process that is killed loses what changed since it started. It serves one process: every other process or instance
|
|
147
167
|
# would run its own copy. Set default to memory to keep that without the startup warning, or to redis to run several.
|
|
148
168
|
# Redis must persist its data (AOF or RDB) and use maxmemory-policy noeviction, or schedules can be lost.
|
|
169
|
+
# Install the commercial @nocobase/app-plugin-jobs-redis package and register its Server plugin
|
|
170
|
+
# from @nocobase/app-plugin-jobs-redis/server in server/plugins.ts on every producer and consumer.
|
|
171
|
+
# Upgrading deployments must set adapter: redis explicitly; templates no longer supply Redis defaults.
|
|
149
172
|
# jobs:
|
|
150
173
|
# default: redis
|
|
151
174
|
# redis:
|
|
175
|
+
# adapter: redis
|
|
152
176
|
# connection:
|
|
153
177
|
# host: 127.0.0.1
|
|
154
178
|
# port: 6379
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineMigration, type MigrationDefinition } from '@nocobase/db';
|
|
2
|
+
|
|
3
|
+
const migration: MigrationDefinition = defineMigration({
|
|
4
|
+
name: '202611020002_create_mutation_receipts',
|
|
5
|
+
async up({ builder }) {
|
|
6
|
+
await builder.createCollection('mutationReceipts', (collection) => {
|
|
7
|
+
collection.string('id', { length: 64 }).notNull();
|
|
8
|
+
collection.string('digest', { length: 64 }).notNull();
|
|
9
|
+
collection.json('result').nullable();
|
|
10
|
+
collection.datetime('expiresAt').notNull();
|
|
11
|
+
collection.primary('id', { name: 'pk_mutation_receipts' });
|
|
12
|
+
collection.index('expiresAt', { name: 'ix_mutation_receipts_expiry' });
|
|
13
|
+
});
|
|
14
|
+
},
|
|
15
|
+
async down({ builder }) {
|
|
16
|
+
await builder.dropCollection('mutationReceipts');
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
export default migration;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nocobase/app-template-default",
|
|
3
3
|
"displayName": "Default Template",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.85",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"prettier": "@nocobase/dev-config/prettier",
|
|
7
7
|
"engines": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"nocobase": {
|
|
11
11
|
"templateKind": "app",
|
|
12
|
-
"defaultTemplateVersion": "1.0.0-beta.
|
|
12
|
+
"defaultTemplateVersion": "1.0.0-beta.85"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@ai-sdk/react": "4.0.131",
|
|
@@ -73,9 +73,10 @@
|
|
|
73
73
|
"@xyflow/react": "^12.11.5",
|
|
74
74
|
"elkjs": "^0.10.2",
|
|
75
75
|
"@types/jsdom": "^30.0.0",
|
|
76
|
-
"@
|
|
76
|
+
"@noble/hashes": "2.3.0",
|
|
77
|
+
"@nocobase/app-client": "^4.0.0-beta.5",
|
|
77
78
|
"@nocobase/app-plugin-template-print": "0.0.2-beta.2",
|
|
78
|
-
"@nocobase/app-skills": "0.1.0-beta.
|
|
79
|
+
"@nocobase/app-skills": "0.1.0-beta.55",
|
|
79
80
|
"@nocobase/app-testing": "0.1.0-beta.6",
|
|
80
81
|
"@nocobase/app-websocket": "^0.1.0-beta.1",
|
|
81
82
|
"@nocobase/dev-config": "0.1.0-beta.25",
|
|
@@ -141,38 +142,40 @@
|
|
|
141
142
|
"@oclif/core": "^4.14.0",
|
|
142
143
|
"better-auth": "1.7.5",
|
|
143
144
|
"hono": "^4.13.2",
|
|
144
|
-
"@nocobase/
|
|
145
|
+
"@nocobase/concurrency": "^0.0.2-beta.0",
|
|
146
|
+
"@nocobase/app-plugin-concurrency": "^0.1.0-beta.0",
|
|
147
|
+
"@nocobase/app-plugin-agents": "^2.0.0-beta.0",
|
|
145
148
|
"@nocobase/app-plugin-api-keys": "^1.0.0-beta.19",
|
|
146
|
-
"@nocobase/app-plugin-authentication": "^2.0.0-beta.
|
|
147
|
-
"@nocobase/app-plugin-authorization": "^1.0.0-beta.
|
|
149
|
+
"@nocobase/app-plugin-authentication": "^2.0.0-beta.16",
|
|
150
|
+
"@nocobase/app-plugin-authorization": "^1.0.0-beta.33",
|
|
148
151
|
"@nocobase/app-plugin-database-explorer": "^1.0.0-beta.17",
|
|
149
152
|
"@nocobase/app-plugin-file": "^1.0.0-beta.27",
|
|
150
|
-
"@nocobase/app-plugin-foundation": "^0.1.0-beta.
|
|
153
|
+
"@nocobase/app-plugin-foundation": "^0.1.0-beta.10",
|
|
151
154
|
"@nocobase/app-plugin-i18n": "^1.0.0-beta.18",
|
|
152
|
-
"@nocobase/app-plugin-knowledge": "^0.1.0-beta.
|
|
153
|
-
"@nocobase/app-plugin-mail": "^1.0.0-beta.
|
|
154
|
-
"@nocobase/app-plugin-notification": "^2.0.0-beta.
|
|
155
|
+
"@nocobase/app-plugin-knowledge": "^0.1.0-beta.15",
|
|
156
|
+
"@nocobase/app-plugin-mail": "^1.0.0-beta.18",
|
|
157
|
+
"@nocobase/app-plugin-notification": "^2.0.0-beta.2",
|
|
155
158
|
"@nocobase/app-plugin-notification-in-app": "^2.0.0-beta.0",
|
|
156
|
-
"@nocobase/app-plugin-notification-providers": "^1.0.0-beta.
|
|
157
|
-
"@nocobase/app-plugin-scheduler": "^1.0.0-beta.
|
|
158
|
-
"@nocobase/app-plugin-users": "^2.0.0-beta.
|
|
159
|
-
"@nocobase/app-server": "^3.0.0-beta.
|
|
159
|
+
"@nocobase/app-plugin-notification-providers": "^1.0.0-beta.16",
|
|
160
|
+
"@nocobase/app-plugin-scheduler": "^1.0.0-beta.21",
|
|
161
|
+
"@nocobase/app-plugin-users": "^2.0.0-beta.11",
|
|
162
|
+
"@nocobase/app-server": "^3.0.0-beta.5",
|
|
160
163
|
"@nocobase/config": "^0.1.0-beta.3",
|
|
161
|
-
"@nocobase/db": "^1.0.0-beta.
|
|
164
|
+
"@nocobase/db": "^1.0.0-beta.24",
|
|
162
165
|
"@nocobase/db-sqlite": "^0.1.0-beta.6",
|
|
163
166
|
"@nocobase/service-provider": "^0.0.2-beta.2",
|
|
164
167
|
"@nocobase/snowflake": "^1.0.0-beta.4",
|
|
165
|
-
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.
|
|
168
|
+
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.17",
|
|
166
169
|
"@nocobase/app-plugin-authz-sharing-rules": "^1.0.0-beta.16",
|
|
167
170
|
"@nocobase/app-plugin-authz-restriction-rules": "^1.0.0-beta.15",
|
|
168
171
|
"@nocobase/authorization": "^1.0.0-beta.15",
|
|
169
|
-
"@nocobase/queue": "^
|
|
170
|
-
"@nocobase/jobs": "^
|
|
171
|
-
"@nocobase/lifecycle": "^0.1.0-beta.
|
|
172
|
+
"@nocobase/queue": "^1.0.0-beta.10",
|
|
173
|
+
"@nocobase/jobs": "^1.0.0-beta.5",
|
|
174
|
+
"@nocobase/lifecycle": "^0.1.0-beta.4",
|
|
172
175
|
"@nocobase/i18n": "^2.0.0-beta.1",
|
|
173
176
|
"@nocobase/caching": "^0.1.0-beta.3",
|
|
174
177
|
"@nocobase/repository-input": "^0.1.0-beta.3",
|
|
175
|
-
"@nocobase/app-cli": "^1.0.0-beta.
|
|
178
|
+
"@nocobase/app-cli": "^1.0.0-beta.30",
|
|
176
179
|
"@nocobase/realtime": "^0.1.0-beta.3"
|
|
177
180
|
},
|
|
178
181
|
"description": "NocoBase 3 application template with the standard product capabilities.",
|
package/server/app.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { MailerProvider } from '@nocobase/app-server/mailer';
|
|
2
1
|
import type { Application } from '@nocobase/app-server/application';
|
|
3
2
|
import { DatabaseProvider } from '@nocobase/app-server/database';
|
|
4
3
|
import { I18nProvider, i18nHttpMiddleware } from '@nocobase/app-server/i18n';
|
|
@@ -34,7 +33,6 @@ export function createApp(runtime: AppRuntimeContext): Application {
|
|
|
34
33
|
app.addServiceProvider(SecretsProvider);
|
|
35
34
|
app.addServiceProvider(SessionProvider);
|
|
36
35
|
app.addServiceProvider(DriveProvider);
|
|
37
|
-
app.addServiceProvider(MailerProvider);
|
|
38
36
|
app.addServiceProvider(QueueServiceProvider, {
|
|
39
37
|
nodeEnv: runtime.env.NODE_ENV,
|
|
40
38
|
});
|