@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
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,
|
|
@@ -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
|
});
|
|
@@ -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
|
});
|
|
@@ -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),
|
|
@@ -342,7 +343,7 @@ it('keeps failed deletion in its confirmation and deletes only after explicit co
|
|
|
342
343
|
expect(screen.getByRole('alertdialog')).toBeVisible();
|
|
343
344
|
expect(screen.queryByText('Private server detail')).not.toBeInTheDocument();
|
|
344
345
|
await user.click(screen.getByRole('button', { name: 'Delete', exact: true }));
|
|
345
|
-
await screen.findByText(enUS.
|
|
346
|
+
await screen.findByText(enUS.listFeedback.empty);
|
|
346
347
|
expect(deletes).toBe(2);
|
|
347
348
|
});
|
|
348
349
|
it('retains the create draft when the server rejects a duplicate and shows a contextual error', async () => {
|
|
@@ -484,6 +485,46 @@ it('refuses deleting a changed rule and refreshes the list for explicit re-confi
|
|
|
484
485
|
expect(screen.queryByRole('alertdialog')).not.toBeInTheDocument();
|
|
485
486
|
});
|
|
486
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
|
+
|
|
487
528
|
it('disables New rule with a reason when every available resource is already configured', async () => {
|
|
488
529
|
await renderWithApp(tree(), {
|
|
489
530
|
route: '/default-access',
|
|
@@ -691,3 +732,56 @@ it('replaces the saved editor history entry and keeps the list query when going
|
|
|
691
732
|
).not.toBeInTheDocument();
|
|
692
733
|
expect(patches).toBe(1);
|
|
693
734
|
});
|
|
735
|
+
|
|
736
|
+
it.each(['transport', 'server'])(
|
|
737
|
+
'verifies an uncertain %s delete without replaying the mutation',
|
|
738
|
+
async (failure) => {
|
|
739
|
+
let deleted = false;
|
|
740
|
+
let writes = 0;
|
|
741
|
+
await renderWithApp(tree(), {
|
|
742
|
+
route: '/default-access',
|
|
743
|
+
namespace,
|
|
744
|
+
namespaces,
|
|
745
|
+
services,
|
|
746
|
+
fetch: answerApi(({ path, method }) => {
|
|
747
|
+
if (path === 'authorization/permissions')
|
|
748
|
+
return { data: { unrestricted: true, permissions: [] } };
|
|
749
|
+
if (path.endsWith('/options')) return { data: catalog };
|
|
750
|
+
if (method === 'DELETE') {
|
|
751
|
+
deleted = true;
|
|
752
|
+
writes++;
|
|
753
|
+
if (failure === 'transport') throw new TypeError('Response lost');
|
|
754
|
+
return Response.json(
|
|
755
|
+
{
|
|
756
|
+
error: {
|
|
757
|
+
code: 'INTERNAL_SERVER_ERROR',
|
|
758
|
+
message: 'Response lost',
|
|
759
|
+
},
|
|
760
|
+
},
|
|
761
|
+
{ status: 500 },
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
return { data: deleted ? [] : [existing] };
|
|
765
|
+
}),
|
|
766
|
+
});
|
|
767
|
+
await userEvent.click(
|
|
768
|
+
await screen.findByRole('button', { name: 'Actions for Projects' }),
|
|
769
|
+
);
|
|
770
|
+
await userEvent.click(
|
|
771
|
+
await screen.findByRole('menuitem', { name: 'Delete' }),
|
|
772
|
+
);
|
|
773
|
+
await userEvent.click(
|
|
774
|
+
screen.getByRole('button', { name: 'Delete', exact: true }),
|
|
775
|
+
);
|
|
776
|
+
expect(await screen.findByText(enUS.listFeedback.empty)).toBeVisible();
|
|
777
|
+
expect(screen.getByRole('alertdialog')).toBeVisible();
|
|
778
|
+
expect(screen.getByText(enUS.settings.failed)).toBeVisible();
|
|
779
|
+
await userEvent.click(
|
|
780
|
+
screen.getByRole('button', { name: 'Cancel', exact: true }),
|
|
781
|
+
);
|
|
782
|
+
await waitFor(() =>
|
|
783
|
+
expect(screen.getByText(enUS.listFeedback.empty)).toBeVisible(),
|
|
784
|
+
);
|
|
785
|
+
expect(writes).toBe(1);
|
|
786
|
+
},
|
|
787
|
+
);
|