@nocobase/app-template-default 1.0.0-beta.82 → 1.0.0-beta.84
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 +9 -1
- package/README.MD +10 -2
- package/client/components/back-button.tsx +3 -5
- 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/permission-editor.tsx +396 -210
- 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/hooks/use-list-data.ts +77 -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 +10 -0
- package/client/locales/system/settings-en-US.ts +8 -0
- package/client/locales/system/settings-zh-CN.ts +8 -0
- package/client/locales/system/zh-CN.ts +8 -0
- package/client/pages/settings/default-access-form.tsx +44 -26
- package/client/pages/settings/default-access.tsx +56 -27
- 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 -58
- package/client/pages/settings/role-form.tsx +98 -56
- package/client/pages/settings/roles.tsx +11 -3
- package/client/pages/settings/shared.tsx +43 -33
- package/client/pages/settings/user-form.tsx +51 -30
- package/client/pages/settings/users.tsx +39 -27
- package/client/routing/session-preparation.tsx +10 -3
- package/database/main/migrations/202611020002_create_mutation_receipts.ts +19 -0
- package/package.json +13 -12
- 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/default-access.test.tsx +60 -4
- package/tests/client/form-feedback.test.tsx +589 -0
- package/tests/client/list-recovery.test.tsx +373 -0
- package/tests/client/list-tabs.test.tsx +121 -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/role-return.test.tsx +2 -1
- package/tests/client/session-preparation.test.tsx +24 -1
- package/tests/client/settings-continuity.test.tsx +5 -4
- package/tests/client/settings-feedback.test.tsx +25 -0
- 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/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/server/member-role.test.ts +7 -1
- package/tests/server/submission-settings.test.ts +241 -0
|
@@ -1,11 +1,15 @@
|
|
|
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 { useApiClient, useToaster } from '@nocobase/app-client';
|
|
12
|
+
import { useApiClient, useSubmission, useToaster } from '@nocobase/app-client';
|
|
9
13
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
10
14
|
import {
|
|
11
15
|
UsersClient,
|
|
@@ -20,13 +24,7 @@ import {
|
|
|
20
24
|
useEffect,
|
|
21
25
|
type ReactElement,
|
|
22
26
|
} from 'react';
|
|
23
|
-
import {
|
|
24
|
-
Link,
|
|
25
|
-
Outlet,
|
|
26
|
-
useLocation,
|
|
27
|
-
useSearchParams,
|
|
28
|
-
useResolvedPath,
|
|
29
|
-
} from 'react-router';
|
|
27
|
+
import { Link, Outlet, useLocation, useResolvedPath } from 'react-router';
|
|
30
28
|
import { PlusIcon, UsersIcon } from 'lucide-react';
|
|
31
29
|
import { PageContainer } from '#components/page-container';
|
|
32
30
|
import { PageHeader } from '#components/page-header';
|
|
@@ -79,14 +77,21 @@ function UserActions({
|
|
|
79
77
|
const [action, setAction] = useState<
|
|
80
78
|
'disable' | 'enable' | 'revokeSessions'
|
|
81
79
|
>();
|
|
82
|
-
const [
|
|
80
|
+
const [requestBusy, setRequestBusy] = useState(false);
|
|
83
81
|
const [error, setError] = useState<unknown>();
|
|
82
|
+
const submission = useSubmission();
|
|
83
|
+
const busy = requestBusy || submission.submitting;
|
|
84
84
|
const run = async () => {
|
|
85
85
|
if (!action) return;
|
|
86
|
-
|
|
86
|
+
setRequestBusy(true);
|
|
87
87
|
setError(undefined);
|
|
88
88
|
try {
|
|
89
|
-
const
|
|
89
|
+
const outcome = await submission.run(
|
|
90
|
+
{ action, id: user.id },
|
|
91
|
+
async (key) => ({ updated: await client[action](user.id, key) }),
|
|
92
|
+
);
|
|
93
|
+
if (!outcome) return;
|
|
94
|
+
const updated = outcome.updated;
|
|
90
95
|
toaster.show({
|
|
91
96
|
type: 'success',
|
|
92
97
|
title: t(`settings.userActionSaved.${action}`, { name: user.name }),
|
|
@@ -96,7 +101,7 @@ function UserActions({
|
|
|
96
101
|
} catch (next) {
|
|
97
102
|
setError(next);
|
|
98
103
|
} finally {
|
|
99
|
-
|
|
104
|
+
setRequestBusy(false);
|
|
100
105
|
}
|
|
101
106
|
};
|
|
102
107
|
if (!canRevoke && !(user.disabledAt ? canEnable : canDisable)) return <></>;
|
|
@@ -156,7 +161,7 @@ function UserActions({
|
|
|
156
161
|
|
|
157
162
|
export default function UsersPage(): ReactElement {
|
|
158
163
|
const { t } = useTranslation();
|
|
159
|
-
const
|
|
164
|
+
const { params, setParams, active } = useListLocation();
|
|
160
165
|
const location = useLocation();
|
|
161
166
|
const listPath = useResolvedPath('.').pathname;
|
|
162
167
|
const searchRef = useRef<HTMLInputElement>(null);
|
|
@@ -218,16 +223,14 @@ export default function UsersPage(): ReactElement {
|
|
|
218
223
|
!roleChoices.some((option) => option.value === selectedRole)
|
|
219
224
|
)
|
|
220
225
|
roleChoices.push({ value: selectedRole, label: `${roleScope} · ${role}` });
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
const { paginationState, sortingState } = readTableUrlState(params, {
|
|
227
|
+
defaultPageSize: 20,
|
|
228
|
+
pageSizeOptions: [20, 50, 100],
|
|
229
|
+
sortColumns: ['name'],
|
|
230
|
+
});
|
|
231
|
+
const page = paginationState.pageIndex + 1;
|
|
227
232
|
const status = params.get('status') ?? 'all';
|
|
228
|
-
const pageSize =
|
|
229
|
-
? Number(params.get('pageSize'))
|
|
230
|
-
: 20;
|
|
233
|
+
const pageSize = paginationState.pageSize;
|
|
231
234
|
const update = (key: string, value: string) => {
|
|
232
235
|
setParams(
|
|
233
236
|
(old) => {
|
|
@@ -263,15 +266,20 @@ export default function UsersPage(): ReactElement {
|
|
|
263
266
|
...(status !== 'all' ? { status } : {}),
|
|
264
267
|
...(roleScope ? { roleScope } : {}),
|
|
265
268
|
...(role ? { role } : {}),
|
|
266
|
-
...(
|
|
267
|
-
? {
|
|
269
|
+
...(sortingState[0]
|
|
270
|
+
? {
|
|
271
|
+
sort: sortingState[0].id,
|
|
272
|
+
order: sortingState[0].desc ? 'desc' : 'asc',
|
|
273
|
+
}
|
|
268
274
|
: {}),
|
|
269
275
|
});
|
|
270
276
|
const request = useSettingsData<{
|
|
271
277
|
data: readonly ManagedUser[];
|
|
272
278
|
meta: { total: number };
|
|
273
|
-
}>(`users?${remote}`, true);
|
|
279
|
+
}>(`users?${remote}`, { keepPreviousData: true, active });
|
|
274
280
|
const tableState = useTableUrlState({
|
|
281
|
+
active,
|
|
282
|
+
revision: request.revision,
|
|
275
283
|
defaultPageSize: 20,
|
|
276
284
|
pageSizeOptions: [20, 50, 100],
|
|
277
285
|
sortColumns: ['name'],
|
|
@@ -481,10 +489,14 @@ export default function UsersPage(): ReactElement {
|
|
|
481
489
|
</p>
|
|
482
490
|
)}
|
|
483
491
|
<SettingsError error={options.error} retry={options.reload} />
|
|
484
|
-
<SettingsError
|
|
492
|
+
<SettingsError
|
|
493
|
+
error={tableState.error ?? request.error}
|
|
494
|
+
retry={request.reload}
|
|
495
|
+
/>
|
|
485
496
|
{request.loading && !request.data ? (
|
|
486
497
|
<SettingsLoading />
|
|
487
498
|
) : (
|
|
499
|
+
!tableState.error &&
|
|
488
500
|
!request.error &&
|
|
489
501
|
(rows.length ? (
|
|
490
502
|
<DataTable<ManagedUser>
|
|
@@ -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
|
}
|
|
@@ -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.84",
|
|
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.84"
|
|
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.4",
|
|
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.54",
|
|
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,28 +142,28 @@
|
|
|
141
142
|
"@oclif/core": "^4.14.0",
|
|
142
143
|
"better-auth": "1.7.5",
|
|
143
144
|
"hono": "^4.13.2",
|
|
144
|
-
"@nocobase/app-plugin-agents": "^1.0.0-beta.
|
|
145
|
+
"@nocobase/app-plugin-agents": "^1.0.0-beta.28",
|
|
145
146
|
"@nocobase/app-plugin-api-keys": "^1.0.0-beta.19",
|
|
146
147
|
"@nocobase/app-plugin-authentication": "^2.0.0-beta.15",
|
|
147
|
-
"@nocobase/app-plugin-authorization": "^1.0.0-beta.
|
|
148
|
+
"@nocobase/app-plugin-authorization": "^1.0.0-beta.33",
|
|
148
149
|
"@nocobase/app-plugin-database-explorer": "^1.0.0-beta.17",
|
|
149
150
|
"@nocobase/app-plugin-file": "^1.0.0-beta.27",
|
|
150
151
|
"@nocobase/app-plugin-foundation": "^0.1.0-beta.9",
|
|
151
152
|
"@nocobase/app-plugin-i18n": "^1.0.0-beta.18",
|
|
152
|
-
"@nocobase/app-plugin-knowledge": "^0.1.0-beta.
|
|
153
|
+
"@nocobase/app-plugin-knowledge": "^0.1.0-beta.14",
|
|
153
154
|
"@nocobase/app-plugin-mail": "^1.0.0-beta.17",
|
|
154
155
|
"@nocobase/app-plugin-notification": "^2.0.0-beta.1",
|
|
155
156
|
"@nocobase/app-plugin-notification-in-app": "^2.0.0-beta.0",
|
|
156
157
|
"@nocobase/app-plugin-notification-providers": "^1.0.0-beta.15",
|
|
157
158
|
"@nocobase/app-plugin-scheduler": "^1.0.0-beta.20",
|
|
158
|
-
"@nocobase/app-plugin-users": "^2.0.0-beta.
|
|
159
|
-
"@nocobase/app-server": "^3.0.0-beta.
|
|
159
|
+
"@nocobase/app-plugin-users": "^2.0.0-beta.11",
|
|
160
|
+
"@nocobase/app-server": "^3.0.0-beta.4",
|
|
160
161
|
"@nocobase/config": "^0.1.0-beta.3",
|
|
161
|
-
"@nocobase/db": "^1.0.0-beta.
|
|
162
|
+
"@nocobase/db": "^1.0.0-beta.23",
|
|
162
163
|
"@nocobase/db-sqlite": "^0.1.0-beta.6",
|
|
163
164
|
"@nocobase/service-provider": "^0.0.2-beta.2",
|
|
164
165
|
"@nocobase/snowflake": "^1.0.0-beta.4",
|
|
165
|
-
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.
|
|
166
|
+
"@nocobase/app-plugin-authz-default-access": "^1.0.0-beta.17",
|
|
166
167
|
"@nocobase/app-plugin-authz-sharing-rules": "^1.0.0-beta.16",
|
|
167
168
|
"@nocobase/app-plugin-authz-restriction-rules": "^1.0.0-beta.15",
|
|
168
169
|
"@nocobase/authorization": "^1.0.0-beta.15",
|
|
@@ -172,7 +173,7 @@
|
|
|
172
173
|
"@nocobase/i18n": "^2.0.0-beta.1",
|
|
173
174
|
"@nocobase/caching": "^0.1.0-beta.3",
|
|
174
175
|
"@nocobase/repository-input": "^0.1.0-beta.3",
|
|
175
|
-
"@nocobase/app-cli": "^1.0.0-beta.
|
|
176
|
+
"@nocobase/app-cli": "^1.0.0-beta.30",
|
|
176
177
|
"@nocobase/realtime": "^0.1.0-beta.3"
|
|
177
178
|
},
|
|
178
179
|
"description": "NocoBase 3 application template with the standard product capabilities.",
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import SubmissionProvider from './submission.js';
|
|
1
2
|
import type { ApplicationServiceProviderConstructor } from '@nocobase/app-server/application';
|
|
2
3
|
|
|
3
|
-
const serviceProviders: readonly ApplicationServiceProviderConstructor[] = [
|
|
4
|
+
const serviceProviders: readonly ApplicationServiceProviderConstructor[] = [
|
|
5
|
+
SubmissionProvider,
|
|
6
|
+
];
|
|
4
7
|
|
|
5
8
|
export default serviceProviders;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Application } from '@nocobase/app-server/application';
|
|
2
|
+
import {
|
|
3
|
+
authenticationToken,
|
|
4
|
+
type AuthEnv,
|
|
5
|
+
} from '@nocobase/app-plugin-authentication';
|
|
6
|
+
import {
|
|
7
|
+
repositoryIdempotencyToken,
|
|
8
|
+
pruneMutationReceipts,
|
|
9
|
+
} from '@nocobase/app-server/router';
|
|
10
|
+
import { jobExecutorServiceToken } from '@nocobase/app-server/jobs';
|
|
11
|
+
import { databaseManagerToken } from '@nocobase/db';
|
|
12
|
+
import { ServiceProvider } from '@nocobase/service-provider';
|
|
13
|
+
import type { Context } from 'hono';
|
|
14
|
+
import type { ScheduleExecutor } from '@nocobase/jobs';
|
|
15
|
+
|
|
16
|
+
/** Standard data writes require an operation key; custom HTTP actions opt in explicitly. */
|
|
17
|
+
export default class SubmissionProvider extends ServiceProvider<Application> {
|
|
18
|
+
readonly name = 'app/submission';
|
|
19
|
+
private cleanup?: ScheduleExecutor;
|
|
20
|
+
override register(): void {
|
|
21
|
+
this.app.container.singleton(repositoryIdempotencyToken, () => ({
|
|
22
|
+
required: true,
|
|
23
|
+
principal: async (context) => {
|
|
24
|
+
const auth =
|
|
25
|
+
(context as Context<AuthEnv>).get('auth') ??
|
|
26
|
+
(await this.app.container
|
|
27
|
+
.resolve(authenticationToken)
|
|
28
|
+
.getSession(context.req.raw.headers, {
|
|
29
|
+
disableRefresh: true,
|
|
30
|
+
disableCookieCache: true,
|
|
31
|
+
}));
|
|
32
|
+
return auth && !auth.credential
|
|
33
|
+
? `${this.app.appName}:${auth.user.id}`
|
|
34
|
+
: undefined;
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
override async boot(): Promise<void> {
|
|
39
|
+
if (
|
|
40
|
+
!this.app.container.has(databaseManagerToken) ||
|
|
41
|
+
!this.app.container.has(jobExecutorServiceToken)
|
|
42
|
+
)
|
|
43
|
+
return;
|
|
44
|
+
const database = this.app.container.resolve(databaseManagerToken);
|
|
45
|
+
this.cleanup = this.app.container
|
|
46
|
+
.resolve(jobExecutorServiceToken)
|
|
47
|
+
.getScheduleExecutor('app.mutation-receipts');
|
|
48
|
+
await this.cleanup.addJob({
|
|
49
|
+
name: 'prune',
|
|
50
|
+
options: { every: 60 * 60 * 1000 },
|
|
51
|
+
payload: {},
|
|
52
|
+
execute: async () => {
|
|
53
|
+
await pruneMutationReceipts(database.connection());
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
await this.cleanup.setup();
|
|
57
|
+
}
|
|
58
|
+
override async shutdown(): Promise<void> {
|
|
59
|
+
await this.cleanup?.shutdown();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -116,6 +116,15 @@ describe('authentication translations', () => {
|
|
|
116
116
|
it('retranslates an existing password mismatch', async () => {
|
|
117
117
|
const value = await runtime();
|
|
118
118
|
const { container } = renderPage(value, <RegisterPage />);
|
|
119
|
+
for (const [label, text] of [
|
|
120
|
+
['Name', 'Test'],
|
|
121
|
+
['Username', 'tester'],
|
|
122
|
+
['Email', 'tester@example.test'],
|
|
123
|
+
]) {
|
|
124
|
+
fireEvent.change(screen.getByLabelText(label!), {
|
|
125
|
+
target: { value: text },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
119
128
|
fireEvent.change(screen.getByLabelText('Password'), {
|
|
120
129
|
target: { value: 'one' },
|
|
121
130
|
});
|
|
@@ -46,15 +46,15 @@ function renderAt(url: string, child: ReactElement) {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
describe('BackButton', () => {
|
|
49
|
-
it('
|
|
49
|
+
it('uses the default parent list when no source visit exists', async () => {
|
|
50
50
|
const router = renderAt('/projects/12?q=alpha&page=2', <BackButton />);
|
|
51
51
|
|
|
52
52
|
const link = screen.getByRole('link', { name: enUS.navigation.back });
|
|
53
|
-
expect(link).toHaveAttribute('href', '/projects
|
|
53
|
+
expect(link).toHaveAttribute('href', '/projects');
|
|
54
54
|
|
|
55
55
|
await userEvent.click(link);
|
|
56
56
|
expect(router.state.location.pathname).toBe('/projects');
|
|
57
|
-
expect(router.state.location.search).toBe('
|
|
57
|
+
expect(router.state.location.search).toBe('');
|
|
58
58
|
// Leaving the child page replaces its entry, as closing an overlay does.
|
|
59
59
|
expect(router.state.historyAction).toBe('REPLACE');
|
|
60
60
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { renderSettingsClient as renderWithApp } from '../helpers/settings-client.js';
|
|
2
|
+
import { answerApi } from '@nocobase/app-testing/client';
|
|
2
3
|
import { apiClientToken, type ClientApplication } from '@nocobase/app-client';
|
|
3
4
|
import {
|
|
4
5
|
AuthorizationClient,
|
|
@@ -207,7 +208,7 @@ it('supports read-only inspection and does not show write or delete actions with
|
|
|
207
208
|
});
|
|
208
209
|
it('shows unknown rules read-only and refuses overwriting a rule changed after loading', async () => {
|
|
209
210
|
const user = userEvent.setup();
|
|
210
|
-
let
|
|
211
|
+
let changed = false;
|
|
211
212
|
let writes = 0;
|
|
212
213
|
await renderWithApp(tree(), {
|
|
213
214
|
route: '/default-access/project-default/edit',
|
|
@@ -219,14 +220,14 @@ it('shows unknown rules read-only and refuses overwriting a rule changed after l
|
|
|
219
220
|
return { data: { unrestricted: true, permissions: [] } };
|
|
220
221
|
if (path.endsWith('/options')) return { data: catalog };
|
|
221
222
|
if (method === 'PATCH') writes++;
|
|
222
|
-
|
|
223
|
-
return { data: [count > 2 ? { ...existing, actions: [] } : existing] };
|
|
223
|
+
return { data: [changed ? { ...existing, actions: [] } : existing] };
|
|
224
224
|
}),
|
|
225
225
|
});
|
|
226
226
|
await user.click(
|
|
227
227
|
await screen.findByRole('combobox', { name: 'Edit · Projects' }),
|
|
228
228
|
);
|
|
229
229
|
await user.click(await screen.findByRole('option', { name: 'All records' }));
|
|
230
|
+
changed = true;
|
|
230
231
|
await user.click(screen.getByRole('button', { name: 'Save' }));
|
|
231
232
|
expect(
|
|
232
233
|
await screen.findByText(enUS.settings.defaultAccess.changed),
|
|
@@ -691,3 +692,58 @@ it('replaces the saved editor history entry and keeps the list query when going
|
|
|
691
692
|
).not.toBeInTheDocument();
|
|
692
693
|
expect(patches).toBe(1);
|
|
693
694
|
});
|
|
695
|
+
|
|
696
|
+
it.each(['transport', 'server'])(
|
|
697
|
+
'verifies an uncertain %s delete without replaying the mutation',
|
|
698
|
+
async (failure) => {
|
|
699
|
+
let deleted = false;
|
|
700
|
+
let writes = 0;
|
|
701
|
+
await renderWithApp(tree(), {
|
|
702
|
+
route: '/default-access',
|
|
703
|
+
namespace,
|
|
704
|
+
namespaces,
|
|
705
|
+
services,
|
|
706
|
+
fetch: answerApi(({ path, method }) => {
|
|
707
|
+
if (path === 'authorization/permissions')
|
|
708
|
+
return { data: { unrestricted: true, permissions: [] } };
|
|
709
|
+
if (path.endsWith('/options')) return { data: catalog };
|
|
710
|
+
if (method === 'DELETE') {
|
|
711
|
+
deleted = true;
|
|
712
|
+
writes++;
|
|
713
|
+
if (failure === 'transport') throw new TypeError('Response lost');
|
|
714
|
+
return Response.json(
|
|
715
|
+
{
|
|
716
|
+
error: {
|
|
717
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
718
|
+
message: 'Response lost',
|
|
719
|
+
},
|
|
720
|
+
},
|
|
721
|
+
{ status: 500 },
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
return { data: deleted ? [] : [existing] };
|
|
725
|
+
}),
|
|
726
|
+
});
|
|
727
|
+
await userEvent.click(
|
|
728
|
+
await screen.findByRole('button', { name: 'Actions for Projects' }),
|
|
729
|
+
);
|
|
730
|
+
await userEvent.click(
|
|
731
|
+
await screen.findByRole('menuitem', { name: 'Delete' }),
|
|
732
|
+
);
|
|
733
|
+
await userEvent.click(
|
|
734
|
+
screen.getByRole('button', { name: 'Delete', exact: true }),
|
|
735
|
+
);
|
|
736
|
+
expect(
|
|
737
|
+
await screen.findByText(enUS.settings.defaultAccess.empty),
|
|
738
|
+
).toBeVisible();
|
|
739
|
+
expect(screen.getByRole('alertdialog')).toBeVisible();
|
|
740
|
+
expect(screen.getByText(enUS.settings.failed)).toBeVisible();
|
|
741
|
+
await userEvent.click(
|
|
742
|
+
screen.getByRole('button', { name: 'Cancel', exact: true }),
|
|
743
|
+
);
|
|
744
|
+
await waitFor(() =>
|
|
745
|
+
expect(screen.getByText(enUS.settings.defaultAccess.empty)).toBeVisible(),
|
|
746
|
+
);
|
|
747
|
+
expect(writes).toBe(1);
|
|
748
|
+
},
|
|
749
|
+
);
|