@nocobase/app-template-default 1.0.0-beta.84 → 1.0.0-beta.86
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.md +1 -1
- package/client/components/data-table/data-table.tsx +86 -32
- package/client/components/list-feedback.tsx +174 -0
- package/client/components/permission-editor.tsx +40 -33
- 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 +1 -2
- package/client/locales/system/en-US.ts +14 -1
- package/client/locales/system/settings-en-US.ts +0 -10
- package/client/locales/system/settings-zh-CN.ts +0 -8
- package/client/locales/system/zh-CN.ts +14 -1
- package/client/pages/settings/default-access.tsx +50 -72
- package/client/pages/settings/permissions-model.ts +0 -1
- package/client/pages/settings/roles.tsx +63 -97
- package/client/pages/settings/users.tsx +72 -70
- package/client/runtime.ts +0 -4
- package/config.example.yml +24 -0
- package/package.json +33 -31
- 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/tests/client/data-table.test.tsx +9 -4
- package/tests/client/default-access.test.tsx +43 -5
- package/tests/client/list-feedback.test.tsx +301 -0
- package/tests/client/manual-list-retry.test.tsx +190 -0
- package/tests/client/permission-workspace.test.ts +13 -0
- package/tests/client/settings-continuity.test.tsx +80 -1
- package/tests/client/settings-feedback.test.tsx +7 -15
- package/tests/fixtures/permission-alignment.html +16 -0
- package/tests/fixtures/permission-alignment.tsx +124 -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 +125 -0
- package/tests/server/config.test.ts +34 -14
- package/client/route-overrides.ts +0 -9
- package/client/source-extensions.ts +0 -19
|
@@ -9,7 +9,12 @@ import {
|
|
|
9
9
|
import { DataTable, DataTableColumnHeader } from '#components/data-table';
|
|
10
10
|
import type { ColumnDef } from '@tanstack/react-table';
|
|
11
11
|
import { useSettingsData, useUserPermission } from './hooks.js';
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
resolveListState,
|
|
14
|
+
useApiClient,
|
|
15
|
+
useSubmission,
|
|
16
|
+
useToaster,
|
|
17
|
+
} from '@nocobase/app-client';
|
|
13
18
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
14
19
|
import {
|
|
15
20
|
UsersClient,
|
|
@@ -25,12 +30,13 @@ import {
|
|
|
25
30
|
type ReactElement,
|
|
26
31
|
} from 'react';
|
|
27
32
|
import { Link, Outlet, useLocation, useResolvedPath } from 'react-router';
|
|
28
|
-
import { PlusIcon
|
|
33
|
+
import { PlusIcon } from 'lucide-react';
|
|
29
34
|
import { PageContainer } from '#components/page-container';
|
|
30
35
|
import { PageHeader } from '#components/page-header';
|
|
31
36
|
import { Button } from '#components/ui/button';
|
|
32
37
|
import { Input } from '#components/ui/input';
|
|
33
38
|
import { StatusBadge } from '#components/status-badge';
|
|
39
|
+
import { Spinner } from '#components/ui/spinner';
|
|
34
40
|
import {
|
|
35
41
|
Select,
|
|
36
42
|
SelectTrigger,
|
|
@@ -38,14 +44,26 @@ import {
|
|
|
38
44
|
SelectContent,
|
|
39
45
|
SelectItem,
|
|
40
46
|
} from '#components/ui/select';
|
|
41
|
-
import {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
}
|
|
49
67
|
|
|
50
68
|
function UserName({ user }: { readonly user: ManagedUser }): ReactElement {
|
|
51
69
|
const canUpdate = useUserPermission('update', user.id);
|
|
@@ -273,10 +291,12 @@ export default function UsersPage(): ReactElement {
|
|
|
273
291
|
}
|
|
274
292
|
: {}),
|
|
275
293
|
});
|
|
294
|
+
const requestPath = `users?${remote}`;
|
|
276
295
|
const request = useSettingsData<{
|
|
277
296
|
data: readonly ManagedUser[];
|
|
278
297
|
meta: { total: number };
|
|
279
|
-
}>(
|
|
298
|
+
}>(requestPath, { keepPreviousData: true, active });
|
|
299
|
+
const hasCurrentResponse = request.path === requestPath;
|
|
280
300
|
const tableState = useTableUrlState({
|
|
281
301
|
active,
|
|
282
302
|
revision: request.revision,
|
|
@@ -284,7 +304,9 @@ export default function UsersPage(): ReactElement {
|
|
|
284
304
|
pageSizeOptions: [20, 50, 100],
|
|
285
305
|
sortColumns: ['name'],
|
|
286
306
|
rowCount:
|
|
287
|
-
!request.loading && !request.error
|
|
307
|
+
hasCurrentResponse && !request.loading && !request.error
|
|
308
|
+
? request.data?.meta.total
|
|
309
|
+
: undefined,
|
|
288
310
|
});
|
|
289
311
|
const clearFilters = () => {
|
|
290
312
|
setQuery('');
|
|
@@ -317,6 +339,24 @@ export default function UsersPage(): ReactElement {
|
|
|
317
339
|
const canCreate = useUserPermission('create');
|
|
318
340
|
const canAssign = useUserPermission('assign-role');
|
|
319
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');
|
|
320
360
|
const columns = useMemo<ColumnDef<ManagedUser>[]>(
|
|
321
361
|
() => [
|
|
322
362
|
{
|
|
@@ -389,8 +429,6 @@ export default function UsersPage(): ReactElement {
|
|
|
389
429
|
{t('settings.newUser')}
|
|
390
430
|
</Button>
|
|
391
431
|
) : null;
|
|
392
|
-
const filtered =
|
|
393
|
-
Boolean(params.get('q') || roleScope || role) || status !== 'all';
|
|
394
432
|
return (
|
|
395
433
|
<>
|
|
396
434
|
<PageContainer>
|
|
@@ -400,7 +438,7 @@ export default function UsersPage(): ReactElement {
|
|
|
400
438
|
<PageHeader
|
|
401
439
|
title={t('settings.users')}
|
|
402
440
|
description={t('settings.usersDescription')}
|
|
403
|
-
actions={
|
|
441
|
+
actions={canShowCreate ? create : undefined}
|
|
404
442
|
/>
|
|
405
443
|
<div className='flex flex-wrap items-center gap-3'>
|
|
406
444
|
<Input
|
|
@@ -472,73 +510,37 @@ export default function UsersPage(): ReactElement {
|
|
|
472
510
|
))}
|
|
473
511
|
</SelectContent>
|
|
474
512
|
</Select>
|
|
475
|
-
{filtered && (
|
|
513
|
+
{filtered && listState !== 'no-results' && (
|
|
476
514
|
<Button variant='ghost' onClick={clearFilters}>
|
|
477
515
|
{t('settings.clear')}
|
|
478
516
|
</Button>
|
|
479
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>
|
|
480
523
|
</div>
|
|
481
|
-
{request.data && request.loading && (
|
|
482
|
-
<span role='status' className='text-sm text-muted-foreground'>
|
|
483
|
-
{t('settings.loading')}
|
|
484
|
-
</span>
|
|
485
|
-
)}
|
|
486
524
|
{Boolean(roleScope) !== Boolean(role) && (
|
|
487
525
|
<p role='alert' className='text-sm text-destructive'>
|
|
488
526
|
{t('settings.roleFilterIncomplete')}
|
|
489
527
|
</p>
|
|
490
528
|
)}
|
|
491
529
|
<SettingsError error={options.error} retry={options.reload} />
|
|
492
|
-
<
|
|
493
|
-
|
|
494
|
-
|
|
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}
|
|
495
543
|
/>
|
|
496
|
-
{request.loading && !request.data ? (
|
|
497
|
-
<SettingsLoading />
|
|
498
|
-
) : (
|
|
499
|
-
!tableState.error &&
|
|
500
|
-
!request.error &&
|
|
501
|
-
(rows.length ? (
|
|
502
|
-
<DataTable<ManagedUser>
|
|
503
|
-
columns={columns}
|
|
504
|
-
data={rows}
|
|
505
|
-
getRowId={(row) => row.id}
|
|
506
|
-
manualPagination
|
|
507
|
-
rowCount={request.data?.meta.total ?? 0}
|
|
508
|
-
{...tableState}
|
|
509
|
-
manualSorting
|
|
510
|
-
showSelectedCount={false}
|
|
511
|
-
loading={request.loading}
|
|
512
|
-
/>
|
|
513
|
-
) : (
|
|
514
|
-
<Empty className='min-h-48 border border-dashed'>
|
|
515
|
-
<EmptyMedia variant='icon'>
|
|
516
|
-
<UsersIcon />
|
|
517
|
-
</EmptyMedia>
|
|
518
|
-
<EmptyTitle>
|
|
519
|
-
{t(filtered ? 'settings.noResults' : 'settings.noUsers')}
|
|
520
|
-
</EmptyTitle>
|
|
521
|
-
<EmptyDescription>
|
|
522
|
-
{t(
|
|
523
|
-
filtered
|
|
524
|
-
? 'settings.clearHint'
|
|
525
|
-
: canCreate && canAssign
|
|
526
|
-
? 'settings.usersDescription'
|
|
527
|
-
: 'settings.usersReadOnlyEmpty',
|
|
528
|
-
)}
|
|
529
|
-
</EmptyDescription>
|
|
530
|
-
<EmptyContent>
|
|
531
|
-
{filtered ? (
|
|
532
|
-
<Button variant='outline' onClick={clearFilters}>
|
|
533
|
-
{t('settings.clear')}
|
|
534
|
-
</Button>
|
|
535
|
-
) : (
|
|
536
|
-
create
|
|
537
|
-
)}
|
|
538
|
-
</EmptyContent>
|
|
539
|
-
</Empty>
|
|
540
|
-
))
|
|
541
|
-
)}
|
|
542
544
|
{request.data && (
|
|
543
545
|
<p className='text-sm text-muted-foreground'>
|
|
544
546
|
{t('settings.total', { count: request.data.meta.total })}
|
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
|
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.86",
|
|
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.86"
|
|
13
13
|
},
|
|
14
14
|
"devDependencies": {
|
|
15
15
|
"@ai-sdk/react": "4.0.131",
|
|
@@ -74,10 +74,10 @@
|
|
|
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.
|
|
77
|
+
"@nocobase/app-client": "^4.0.0-beta.6",
|
|
78
78
|
"@nocobase/app-plugin-template-print": "0.0.2-beta.2",
|
|
79
|
-
"@nocobase/app-skills": "0.1.0-beta.
|
|
80
|
-
"@nocobase/app-testing": "0.1.0-beta.
|
|
79
|
+
"@nocobase/app-skills": "0.1.0-beta.56",
|
|
80
|
+
"@nocobase/app-testing": "0.1.0-beta.7",
|
|
81
81
|
"@nocobase/app-websocket": "^0.1.0-beta.1",
|
|
82
82
|
"@nocobase/dev-config": "0.1.0-beta.25",
|
|
83
83
|
"@nocobase/drive": "^0.1.0-beta.7",
|
|
@@ -142,39 +142,41 @@
|
|
|
142
142
|
"@oclif/core": "^4.14.0",
|
|
143
143
|
"better-auth": "1.7.5",
|
|
144
144
|
"hono": "^4.13.2",
|
|
145
|
-
"@nocobase/
|
|
146
|
-
"@nocobase/app-plugin-
|
|
147
|
-
"@nocobase/app-plugin-
|
|
148
|
-
"@nocobase/app-plugin-
|
|
149
|
-
"@nocobase/app-plugin-
|
|
150
|
-
"@nocobase/app-plugin-
|
|
151
|
-
"@nocobase/app-plugin-
|
|
152
|
-
"@nocobase/app-plugin-
|
|
153
|
-
"@nocobase/app-plugin-
|
|
154
|
-
"@nocobase/app-plugin-
|
|
155
|
-
"@nocobase/app-plugin-
|
|
156
|
-
"@nocobase/app-plugin-
|
|
157
|
-
"@nocobase/app-plugin-notification
|
|
158
|
-
"@nocobase/app-plugin-
|
|
159
|
-
"@nocobase/app-plugin-
|
|
160
|
-
"@nocobase/app-
|
|
145
|
+
"@nocobase/concurrency": "^0.1.0-beta.1",
|
|
146
|
+
"@nocobase/app-plugin-concurrency": "^0.1.0-beta.1",
|
|
147
|
+
"@nocobase/app-plugin-agents": "^2.0.0-beta.1",
|
|
148
|
+
"@nocobase/app-plugin-api-keys": "^1.0.0-beta.20",
|
|
149
|
+
"@nocobase/app-plugin-authentication": "^2.0.0-beta.17",
|
|
150
|
+
"@nocobase/app-plugin-authorization": "^1.0.0-beta.34",
|
|
151
|
+
"@nocobase/app-plugin-database-explorer": "^1.0.0-beta.18",
|
|
152
|
+
"@nocobase/app-plugin-file": "^1.0.0-beta.28",
|
|
153
|
+
"@nocobase/app-plugin-foundation": "^0.1.0-beta.11",
|
|
154
|
+
"@nocobase/app-plugin-i18n": "^1.0.0-beta.19",
|
|
155
|
+
"@nocobase/app-plugin-knowledge": "^0.1.0-beta.16",
|
|
156
|
+
"@nocobase/app-plugin-mail": "^1.0.0-beta.19",
|
|
157
|
+
"@nocobase/app-plugin-notification": "^2.0.0-beta.3",
|
|
158
|
+
"@nocobase/app-plugin-notification-in-app": "^2.0.0-beta.1",
|
|
159
|
+
"@nocobase/app-plugin-notification-providers": "^1.0.0-beta.17",
|
|
160
|
+
"@nocobase/app-plugin-scheduler": "^1.0.0-beta.22",
|
|
161
|
+
"@nocobase/app-plugin-users": "^2.0.0-beta.12",
|
|
162
|
+
"@nocobase/app-server": "^4.0.0-beta.0",
|
|
161
163
|
"@nocobase/config": "^0.1.0-beta.3",
|
|
162
|
-
"@nocobase/db": "^1.0.0-beta.
|
|
164
|
+
"@nocobase/db": "^1.0.0-beta.24",
|
|
163
165
|
"@nocobase/db-sqlite": "^0.1.0-beta.6",
|
|
164
166
|
"@nocobase/service-provider": "^0.0.2-beta.2",
|
|
165
167
|
"@nocobase/snowflake": "^1.0.0-beta.4",
|
|
166
|
-
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.
|
|
167
|
-
"@nocobase/app-plugin-authz-sharing-rules": "^1.0.0-beta.
|
|
168
|
-
"@nocobase/app-plugin-authz-restriction-rules": "^1.0.0-beta.
|
|
169
|
-
"@nocobase/authorization": "^1.0.0-beta.
|
|
170
|
-
"@nocobase/queue": "^
|
|
171
|
-
"@nocobase/jobs": "^
|
|
172
|
-
"@nocobase/lifecycle": "^0.1.0-beta.
|
|
168
|
+
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.18",
|
|
169
|
+
"@nocobase/app-plugin-authz-sharing-rules": "^1.0.0-beta.17",
|
|
170
|
+
"@nocobase/app-plugin-authz-restriction-rules": "^1.0.0-beta.16",
|
|
171
|
+
"@nocobase/authorization": "^1.0.0-beta.16",
|
|
172
|
+
"@nocobase/queue": "^1.0.0-beta.10",
|
|
173
|
+
"@nocobase/jobs": "^1.0.0-beta.5",
|
|
174
|
+
"@nocobase/lifecycle": "^0.1.0-beta.4",
|
|
173
175
|
"@nocobase/i18n": "^2.0.0-beta.1",
|
|
174
176
|
"@nocobase/caching": "^0.1.0-beta.3",
|
|
175
177
|
"@nocobase/repository-input": "^0.1.0-beta.3",
|
|
176
|
-
"@nocobase/app-cli": "^1.0.0-beta.
|
|
177
|
-
"@nocobase/realtime": "^
|
|
178
|
+
"@nocobase/app-cli": "^1.0.0-beta.31",
|
|
179
|
+
"@nocobase/realtime": "^1.0.0-beta.0"
|
|
178
180
|
},
|
|
179
181
|
"description": "NocoBase 3 application template with the standard product capabilities.",
|
|
180
182
|
"imports": {
|
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
|
});
|
package/server/config/auth.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { bearer, deviceAuthorization, username } from 'better-auth/plugins';
|
|
|
12
12
|
*/
|
|
13
13
|
const CLI_CLIENT_IDS: readonly string[] = ['nocobase-cli'];
|
|
14
14
|
|
|
15
|
+
// defineAuthConfig supplies AUTH_EMAIL_* mappings and validates the private auth.email sender.
|
|
16
|
+
// Automatic recovery requires passwordReset.enabled: true; leave the switch absent for custom callback compatibility.
|
|
15
17
|
const auth: AppConfigFactory<AuthConfig> = defineAuthConfig({
|
|
16
18
|
defaults: {
|
|
17
19
|
plugins: [
|
package/server/config/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import server from './server.js';
|
|
|
12
12
|
import spa from './spa.js';
|
|
13
13
|
import logging from './logging.js';
|
|
14
14
|
import drive from './drive.js';
|
|
15
|
+
import concurrency from './concurrency.js';
|
|
15
16
|
import queue from './queue.js';
|
|
16
17
|
import jobs from './jobs.js';
|
|
17
18
|
import scheduler from './scheduler.js';
|
|
@@ -35,6 +36,7 @@ const defaultConfigs: AppConfigFactory<{
|
|
|
35
36
|
spa: ReturnType<typeof spa>;
|
|
36
37
|
logging: ReturnType<typeof logging>;
|
|
37
38
|
drive: ReturnType<typeof drive>;
|
|
39
|
+
concurrency: ReturnType<typeof concurrency>;
|
|
38
40
|
queue: ReturnType<typeof queue>;
|
|
39
41
|
jobs: ReturnType<typeof jobs>;
|
|
40
42
|
scheduler: ReturnType<typeof scheduler>;
|
|
@@ -57,6 +59,7 @@ const defaultConfigs: AppConfigFactory<{
|
|
|
57
59
|
spa,
|
|
58
60
|
logging,
|
|
59
61
|
drive,
|
|
62
|
+
concurrency,
|
|
60
63
|
queue,
|
|
61
64
|
jobs,
|
|
62
65
|
scheduler,
|
package/server/config/jobs.ts
CHANGED
|
@@ -4,24 +4,12 @@ import {
|
|
|
4
4
|
} from '@nocobase/app-server/config';
|
|
5
5
|
import type { AppJobsConfig } from '@nocobase/app-server/jobs';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Configurations the jobs service can run on. None is the default: until `jobs.default` names one, executors
|
|
9
|
-
* run on the built-in memory adapter, which serves one process and writes its state under storage/jobs when the
|
|
10
|
-
* application stops, and which is reported at startup outside development. Name `memory` to keep that choice without
|
|
11
|
-
* the report, or `redis` to run any number of instances with each firing executed once.
|
|
12
|
-
*/
|
|
7
|
+
/** Single-process persistence. Distributed jobs require an explicitly registered backend plugin. */
|
|
13
8
|
const jobs: AppConfigFactory<AppJobsConfig> = defineAppConfig(({ paths }) => ({
|
|
14
9
|
memory: {
|
|
15
10
|
adapter: 'memory',
|
|
16
11
|
persistence: { path: paths.storage('jobs') },
|
|
17
12
|
},
|
|
18
|
-
redis: {
|
|
19
|
-
adapter: 'redis',
|
|
20
|
-
connection: { host: '127.0.0.1', port: 6379, db: 0 },
|
|
21
|
-
// Every firing leaves a finished job behind; keep the history bounded.
|
|
22
|
-
removeOnComplete: { count: 1000 },
|
|
23
|
-
removeOnFail: { age: 604_800 },
|
|
24
|
-
},
|
|
25
13
|
}));
|
|
26
14
|
|
|
27
15
|
export default jobs;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineMailChannelsConfig } from '@nocobase/app-
|
|
1
|
+
import { defineMailChannelsConfig } from '@nocobase/app-plugin-notification-providers/config';
|
|
2
2
|
import { type AppConfigFactory } from '@nocobase/app-server/config';
|
|
3
3
|
import type { NotificationConfig } from '@nocobase/app-plugin-notification/server';
|
|
4
4
|
|
package/server/config/queue.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { AppQueueConfig } from '@nocobase/app-server/queue';
|
|
|
8
8
|
* Configurations queues can run on. None is the default: until `queue.default` names one, queues run on the
|
|
9
9
|
* built-in memory configuration, which serves one process and writes its pending jobs under storage/queue when the
|
|
10
10
|
* application stops, and which is reported at startup outside development. Name `memory` to keep that choice without
|
|
11
|
-
* the report
|
|
11
|
+
* the report. Shared Redis queues require explicit registration of @nocobase/app-plugin-queue-redis/server.
|
|
12
12
|
*/
|
|
13
13
|
const queue: AppConfigFactory<AppQueueConfig> = defineAppConfig(
|
|
14
14
|
({ paths }) => ({
|
|
@@ -16,12 +16,6 @@ const queue: AppConfigFactory<AppQueueConfig> = defineAppConfig(
|
|
|
16
16
|
adapter: 'inMemory',
|
|
17
17
|
persistence: { path: paths.storage('queue') },
|
|
18
18
|
},
|
|
19
|
-
redis: {
|
|
20
|
-
adapter: 'redis',
|
|
21
|
-
connection: { host: '127.0.0.1', port: 6379, db: 0 },
|
|
22
|
-
removeOnComplete: { count: 1000 },
|
|
23
|
-
removeOnFail: { age: 604_800 },
|
|
24
|
-
},
|
|
25
19
|
}),
|
|
26
20
|
);
|
|
27
21
|
|
package/server/plugins.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import concurrency from '@nocobase/app-plugin-concurrency/server';
|
|
1
2
|
import foundation from '@nocobase/app-plugin-foundation/server';
|
|
2
3
|
import defaultAccess from '@nocobase/app-plugin-authz-default-access/server';
|
|
3
4
|
import sharingRules from '@nocobase/app-plugin-authz-sharing-rules/server';
|
|
@@ -20,6 +21,7 @@ import file from '@nocobase/app-plugin-file/server';
|
|
|
20
21
|
import mail from '@nocobase/app-plugin-mail/server';
|
|
21
22
|
|
|
22
23
|
const serverPlugins: AppServerPlugins = defineServerPlugins([
|
|
24
|
+
concurrency,
|
|
23
25
|
authentication,
|
|
24
26
|
authorization,
|
|
25
27
|
defaultAccess,
|
|
@@ -114,14 +114,19 @@ describe('standard DataTable', () => {
|
|
|
114
114
|
expect(screen.getByText('Page 4 of 6')).toBeVisible();
|
|
115
115
|
expect(screen.getByText('row-0')).toBeVisible();
|
|
116
116
|
});
|
|
117
|
-
it('
|
|
117
|
+
it('localizes successful emptiness and keeps the table headers without paging controls', async () => {
|
|
118
118
|
await renderWithApp(
|
|
119
119
|
<DataTable columns={columns} data={[]} showSelectedCount={false} />,
|
|
120
120
|
{ namespace, namespaces, locale: 'zh-CN' },
|
|
121
121
|
);
|
|
122
|
-
expect(screen.getByText(zhCN.
|
|
122
|
+
expect(screen.getByText(zhCN.listFeedback.empty)).toBeVisible();
|
|
123
|
+
expect(screen.getByRole('table')).toBeVisible();
|
|
124
|
+
expect(screen.getByRole('columnheader', { name: 'ID' })).toBeVisible();
|
|
123
125
|
expect(
|
|
124
|
-
screen.
|
|
125
|
-
).
|
|
126
|
+
screen.queryByText(zhCN.dataTable.noResults),
|
|
127
|
+
).not.toBeInTheDocument();
|
|
128
|
+
expect(
|
|
129
|
+
screen.queryByRole('button', { name: zhCN.dataTable.nextPage }),
|
|
130
|
+
).not.toBeInTheDocument();
|
|
126
131
|
});
|
|
127
132
|
});
|
|
@@ -343,7 +343,7 @@ it('keeps failed deletion in its confirmation and deletes only after explicit co
|
|
|
343
343
|
expect(screen.getByRole('alertdialog')).toBeVisible();
|
|
344
344
|
expect(screen.queryByText('Private server detail')).not.toBeInTheDocument();
|
|
345
345
|
await user.click(screen.getByRole('button', { name: 'Delete', exact: true }));
|
|
346
|
-
await screen.findByText(enUS.
|
|
346
|
+
await screen.findByText(enUS.listFeedback.empty);
|
|
347
347
|
expect(deletes).toBe(2);
|
|
348
348
|
});
|
|
349
349
|
it('retains the create draft when the server rejects a duplicate and shows a contextual error', async () => {
|
|
@@ -485,6 +485,46 @@ it('refuses deleting a changed rule and refreshes the list for explicit re-confi
|
|
|
485
485
|
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
|
|
486
486
|
});
|
|
487
487
|
|
|
488
|
+
it('keeps one authorized create entry in the header for an empty list', async () => {
|
|
489
|
+
await renderWithApp(tree(), {
|
|
490
|
+
route: '/default-access',
|
|
491
|
+
namespace,
|
|
492
|
+
namespaces,
|
|
493
|
+
services,
|
|
494
|
+
fetch: answerApi(({ path }) =>
|
|
495
|
+
path === 'authorization/permissions'
|
|
496
|
+
? { data: { unrestricted: true, permissions: [] } }
|
|
497
|
+
: path.endsWith('/options')
|
|
498
|
+
? { data: catalog }
|
|
499
|
+
: { data: [] },
|
|
500
|
+
),
|
|
501
|
+
});
|
|
502
|
+
const label = enUS.settings.defaultAccess.new;
|
|
503
|
+
const create = await screen.findByRole('button', { name: label });
|
|
504
|
+
expect(create).toHaveAttribute('href', '/default-access/create');
|
|
505
|
+
expect(screen.getAllByRole('button', { name: label })).toHaveLength(1);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('hides the create entry for a read-only empty list', async () => {
|
|
509
|
+
await renderWithApp(tree(), {
|
|
510
|
+
route: '/default-access',
|
|
511
|
+
namespace,
|
|
512
|
+
namespaces,
|
|
513
|
+
services,
|
|
514
|
+
fetch: answerApi(({ path }) =>
|
|
515
|
+
path === 'authorization/permissions'
|
|
516
|
+
? { data: { unrestricted: false, permissions: [] } }
|
|
517
|
+
: path.endsWith('/options')
|
|
518
|
+
? { data: catalog }
|
|
519
|
+
: { data: [] },
|
|
520
|
+
),
|
|
521
|
+
});
|
|
522
|
+
const label = enUS.settings.defaultAccess.new;
|
|
523
|
+
expect(await screen.findByText(enUS.listFeedback.empty)).toBeVisible();
|
|
524
|
+
expect(screen.queryByRole('link', { name: label })).not.toBeInTheDocument();
|
|
525
|
+
expect(screen.queryByRole('button', { name: label })).not.toBeInTheDocument();
|
|
526
|
+
});
|
|
527
|
+
|
|
488
528
|
it('disables New rule with a reason when every available resource is already configured', async () => {
|
|
489
529
|
await renderWithApp(tree(), {
|
|
490
530
|
route: '/default-access',
|
|
@@ -733,16 +773,14 @@ it.each(['transport', 'server'])(
|
|
|
733
773
|
await userEvent.click(
|
|
734
774
|
screen.getByRole('button', { name: 'Delete', exact: true }),
|
|
735
775
|
);
|
|
736
|
-
expect(
|
|
737
|
-
await screen.findByText(enUS.settings.defaultAccess.empty),
|
|
738
|
-
).toBeVisible();
|
|
776
|
+
expect(await screen.findByText(enUS.listFeedback.empty)).toBeVisible();
|
|
739
777
|
expect(screen.getByRole('alertdialog')).toBeVisible();
|
|
740
778
|
expect(screen.getByText(enUS.settings.failed)).toBeVisible();
|
|
741
779
|
await userEvent.click(
|
|
742
780
|
screen.getByRole('button', { name: 'Cancel', exact: true }),
|
|
743
781
|
);
|
|
744
782
|
await waitFor(() =>
|
|
745
|
-
expect(screen.getByText(enUS.
|
|
783
|
+
expect(screen.getByText(enUS.listFeedback.empty)).toBeVisible(),
|
|
746
784
|
);
|
|
747
785
|
expect(writes).toBe(1);
|
|
748
786
|
},
|