@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.
Files changed (104) hide show
  1. package/.agents/rules/build-and-deploy.md +1 -1
  2. package/.agents/rules/shell.md +4 -0
  3. package/AGENTS.md +8 -2
  4. package/README.MD +10 -2
  5. package/client/components/back-button.tsx +3 -5
  6. package/client/components/data-table/data-table.tsx +86 -32
  7. package/client/components/default-access-editor.tsx +13 -2
  8. package/client/components/feedback-form.tsx +474 -0
  9. package/client/components/lifecycle-diagram.tsx +10 -10
  10. package/client/components/list-feedback.tsx +174 -0
  11. package/client/components/permission-editor.tsx +419 -226
  12. package/client/extensions/nocobase-auth-forms/password-login-form.tsx +17 -6
  13. package/client/extensions/nocobase-auth-forms/password-registration-form.tsx +24 -7
  14. package/client/extensions/nocobase-auth-forms/password-reset-form.tsx +19 -7
  15. package/client/extensions/nocobase-auth-forms/password-reset-request-form.tsx +15 -7
  16. package/client/extensions/nocobase-device-approval/device-approval.tsx +7 -2
  17. package/client/extensions/nocobase-device-approval/locales/en-US.ts +16 -0
  18. package/client/extensions/nocobase-device-approval/locales/zh-CN.ts +7 -0
  19. package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +20 -17
  20. package/client/extensions/nocobase-workflow-management/README.md +3 -1
  21. package/client/extensions/nocobase-workflow-management/definition-list.tsx +19 -7
  22. package/client/extensions/nocobase-workflow-management/execution-list.tsx +61 -48
  23. package/client/hooks/use-list-data.ts +76 -0
  24. package/client/hooks/use-list-location.ts +23 -0
  25. package/client/hooks/use-list-tabs.ts +42 -0
  26. package/client/hooks/use-table-url-state.ts +143 -30
  27. package/client/locales/system/en-US.ts +24 -1
  28. package/client/locales/system/settings-en-US.ts +8 -10
  29. package/client/locales/system/settings-zh-CN.ts +8 -8
  30. package/client/locales/system/zh-CN.ts +22 -1
  31. package/client/pages/settings/default-access-form.tsx +44 -26
  32. package/client/pages/settings/default-access.tsx +100 -93
  33. package/client/pages/settings/hooks.tsx +4 -59
  34. package/client/pages/settings/page-permissions.ts +17 -0
  35. package/client/pages/settings/permissions-model.ts +54 -59
  36. package/client/pages/settings/role-form.tsx +98 -56
  37. package/client/pages/settings/roles.tsx +69 -95
  38. package/client/pages/settings/shared.tsx +43 -33
  39. package/client/pages/settings/user-form.tsx +51 -30
  40. package/client/pages/settings/users.tsx +105 -91
  41. package/client/routing/session-preparation.tsx +10 -3
  42. package/client/runtime.ts +0 -4
  43. package/config.example.yml +24 -0
  44. package/database/main/migrations/202611020002_create_mutation_receipts.ts +19 -0
  45. package/package.json +24 -21
  46. package/server/app.ts +0 -2
  47. package/server/config/auth.ts +2 -0
  48. package/server/config/concurrency.ts +3 -0
  49. package/server/config/index.ts +3 -0
  50. package/server/config/jobs.ts +1 -13
  51. package/server/config/notification.ts +1 -1
  52. package/server/config/queue.ts +1 -7
  53. package/server/plugins.ts +2 -0
  54. package/server/providers/index.ts +4 -1
  55. package/server/providers/submission.ts +61 -0
  56. package/tests/client/auth-i18n.test.tsx +9 -0
  57. package/tests/client/back-button.test.tsx +3 -3
  58. package/tests/client/data-table.test.tsx +9 -4
  59. package/tests/client/default-access.test.tsx +99 -5
  60. package/tests/client/form-feedback.test.tsx +589 -0
  61. package/tests/client/list-feedback.test.tsx +301 -0
  62. package/tests/client/list-recovery.test.tsx +373 -0
  63. package/tests/client/list-tabs.test.tsx +121 -0
  64. package/tests/client/manual-list-retry.test.tsx +190 -0
  65. package/tests/client/page-permission-navigation.test.ts +8 -0
  66. package/tests/client/permission-editor.test.tsx +191 -21
  67. package/tests/client/permission-filter-binding.test.tsx +1 -1
  68. package/tests/client/permission-workspace.test.ts +13 -0
  69. package/tests/client/role-return.test.tsx +2 -1
  70. package/tests/client/session-preparation.test.tsx +24 -1
  71. package/tests/client/settings-continuity.test.tsx +85 -5
  72. package/tests/client/settings-feedback.test.tsx +32 -15
  73. package/tests/client/settings-permissions.test.ts +35 -0
  74. package/tests/client/settings.test.tsx +2 -1
  75. package/tests/client/table-url-state.test.tsx +117 -3
  76. package/tests/client/user-drawer.test.tsx +2 -1
  77. package/tests/client/user-permissions.test.tsx +30 -5
  78. package/tests/database/mutation-receipts.test.ts +45 -0
  79. package/tests/fixtures/form-feedback/README.md +33 -0
  80. package/tests/fixtures/form-feedback/equipment-detail.tsx +11 -0
  81. package/tests/fixtures/form-feedback/equipment-layout.tsx +15 -0
  82. package/tests/fixtures/form-feedback/equipment-migration.ts +27 -0
  83. package/tests/fixtures/form-feedback/equipment-server.ts +197 -0
  84. package/tests/fixtures/form-feedback/equipment-validation.tsx +94 -0
  85. package/tests/fixtures/form-feedback/equipment.tsx +369 -0
  86. package/tests/fixtures/form-feedback/verify-inline.mjs +174 -0
  87. package/tests/fixtures/form-feedback/verify.mjs +373 -0
  88. package/tests/fixtures/permission-alignment.html +16 -0
  89. package/tests/fixtures/permission-alignment.tsx +124 -0
  90. package/tests/fixtures/submission/README.md +35 -0
  91. package/tests/fixtures/submission/browser.test.ts +478 -0
  92. package/tests/fixtures/submission/migration.ts +61 -0
  93. package/tests/fixtures/submission/page.tsx +138 -0
  94. package/tests/fixtures/submission/playwright.config.ts +11 -0
  95. package/tests/fixtures/submission/routes.ts +208 -0
  96. package/tests/helpers/settings-client.tsx +31 -0
  97. package/tests/playwright/permission-alignment.test.ts +109 -0
  98. package/tests/server/auth-email-config.test.ts +79 -0
  99. package/tests/server/concurrency.test.ts +61 -0
  100. package/tests/server/config.test.ts +34 -14
  101. package/tests/server/member-role.test.ts +7 -1
  102. package/tests/server/submission-settings.test.ts +241 -0
  103. package/client/route-overrides.ts +0 -9
  104. package/client/source-extensions.ts +0 -19
@@ -1,6 +1,13 @@
1
+ import { useListLocation } from '#hooks/use-list-location';
1
2
  import { RecordLink } from '#components/record-link';
2
3
  import { useTableUrlState } from '#hooks/use-table-url-state';
3
- import { useApiClient, useToaster } from '@nocobase/app-client';
4
+ import {
5
+ ApiClientError,
6
+ resolveListState,
7
+ useApiClient,
8
+ useSubmission,
9
+ useToaster,
10
+ } from '@nocobase/app-client';
4
11
  import {
5
12
  useCan,
6
13
  type AuthorizationOptionsResponse,
@@ -8,13 +15,14 @@ import {
8
15
  } from '@nocobase/app-plugin-authorization/client';
9
16
  import { useTranslation } from '@nocobase/i18n/client';
10
17
  import { useRef, useState, type ReactElement } from 'react';
11
- import { Link, Outlet, useLocation, useSearchParams } from 'react-router';
12
- import { ListFilterIcon, MoreHorizontalIcon, PlusIcon } from 'lucide-react';
18
+ import { Link, Outlet, useLocation } from 'react-router';
19
+ import { MoreHorizontalIcon, PlusIcon } from 'lucide-react';
13
20
  import type { ColumnDef } from '@tanstack/react-table';
14
21
  import { Alert, AlertDescription } from '#components/ui/alert';
15
22
  import { PermissionManagementPage } from './permission-management-page.js';
16
23
  import { DataTable, DataTableColumnHeader } from '#components/data-table';
17
24
  import { Button } from '#components/ui/button';
25
+ import { Spinner } from '#components/ui/spinner';
18
26
  import { Input } from '#components/ui/input';
19
27
  import {
20
28
  DropdownMenu,
@@ -23,14 +31,7 @@ import {
23
31
  DropdownMenuSeparator,
24
32
  DropdownMenuTrigger,
25
33
  } from '#components/ui/dropdown-menu';
26
- import {
27
- Empty,
28
- EmptyMedia,
29
- EmptyTitle,
30
- EmptyDescription,
31
- EmptyContent,
32
- } from '#components/ui/empty';
33
- import { ConfirmAction, SettingsError, SettingsLoading } from './shared.js';
34
+ import { ConfirmAction, SettingsError } from './shared.js';
34
35
  import { useSettingsData } from './hooks.js';
35
36
  import {
36
37
  actionKey,
@@ -45,19 +46,21 @@ export default function DefaultAccessPage(): ReactElement {
45
46
  const api = useApiClient();
46
47
  const toaster = useToaster();
47
48
  const location = useLocation();
48
- const [params, setParams] = useSearchParams();
49
+ const { params, setParams, active } = useListLocation();
49
50
  const search = params.get('q') ?? '';
50
51
  const searchRef = useRef<HTMLInputElement>(null);
51
52
  const rules = useSettingsData<{ data: readonly DefaultAccessRule[] }>(
52
53
  'authorization/defaultAccess',
54
+ { active },
53
55
  );
54
56
  const catalog = useSettingsData<{ data: AuthorizationOptionsResponse }>(
55
57
  'authorization/defaultAccess/options',
56
58
  );
57
- const canCreate = useCan({
59
+ const createPermission = useCan({
58
60
  resource: { type: 'settings', id: 'authorization.default-access' },
59
61
  action: 'create',
60
- }).can;
62
+ });
63
+ const canCreate = createPermission.can && !createPermission.isPending;
61
64
  const canDelete = useCan({
62
65
  resource: { type: 'settings', id: 'authorization.default-access' },
63
66
  action: 'delete',
@@ -212,6 +215,8 @@ export default function DefaultAccessPage(): ReactElement {
212
215
  )
213
216
  .sort((a, b) => name(a).localeCompare(name(b), i18n.language));
214
217
  const tableState = useTableUrlState({
218
+ active,
219
+ revision: rules.revision,
215
220
  defaultPageSize: 20,
216
221
  sortColumns: ['resource'],
217
222
  rowCount:
@@ -224,6 +229,30 @@ export default function DefaultAccessPage(): ReactElement {
224
229
  ? rows.length
225
230
  : undefined,
226
231
  });
232
+ const listError =
233
+ [rules.error, catalog.error].find(
234
+ (error) =>
235
+ error instanceof ApiClientError && [401, 403].includes(error.status),
236
+ ) ??
237
+ tableState.error ??
238
+ rules.error ??
239
+ catalog.error;
240
+ const listData = rules.data && catalog.data ? rows : undefined;
241
+ const listState = resolveListState(
242
+ {
243
+ data: listData,
244
+ error: listError,
245
+ isPending: !listData && (rules.loading || catalog.loading),
246
+ },
247
+ rows.length,
248
+ Boolean(search.trim()),
249
+ );
250
+ const canShowCreate =
251
+ canCreate &&
252
+ (listState === 'empty' ||
253
+ listState === 'ready' ||
254
+ listState === 'no-results' ||
255
+ listState === 'refresh-failed');
227
256
  const clearSearch = () =>
228
257
  setParams(
229
258
  (old) => {
@@ -262,43 +291,58 @@ export default function DefaultAccessPage(): ReactElement {
262
291
  </div>
263
292
  )
264
293
  ) : null;
294
+ const submission = useSubmission();
265
295
  const remove = async () => {
266
296
  if (!removing || busy || !canDelete) return;
267
297
  setBusy(true);
268
298
  setError(undefined);
299
+ let submitted = false;
269
300
  try {
270
- const latest = await api.request<{ data: readonly DefaultAccessRule[] }>({
271
- path: 'authorization/defaultAccess',
272
- });
273
- if (
274
- JSON.stringify(
275
- latest.data.find((rule) => rule.key === removing.key),
276
- ) !== JSON.stringify(removing)
277
- ) {
278
- rules.reload();
279
- setRemoving(undefined);
280
- setConflict(true);
281
- requestAnimationFrame(() => searchRef.current?.focus());
282
- return;
283
- }
284
- await api.request({
285
- path: `authorization/defaultAccess/${encodeURIComponent(removing.key)}`,
286
- method: 'DELETE',
287
- });
301
+ const outcome = await submission.run(
302
+ { key: removing.key },
303
+ async (idempotencyKey, _signal, _snapshot, recovering) => {
304
+ if (!recovering) {
305
+ const latest = await api.request<{
306
+ data: readonly DefaultAccessRule[];
307
+ }>({ path: 'authorization/defaultAccess' });
308
+ if (
309
+ JSON.stringify(
310
+ latest.data.find((rule) => rule.key === removing.key),
311
+ ) !== JSON.stringify(removing)
312
+ ) {
313
+ rules.reload();
314
+ setRemoving(undefined);
315
+ setConflict(true);
316
+ requestAnimationFrame(() => searchRef.current?.focus());
317
+ return undefined;
318
+ }
319
+ }
320
+ submitted = true;
321
+ await api.request({
322
+ path: `authorization/defaultAccess/${encodeURIComponent(removing.key)}`,
323
+ method: 'DELETE',
324
+ headers: { 'Idempotency-Key': idempotencyKey },
325
+ });
326
+ return true;
327
+ },
328
+ );
329
+ if (!outcome) return;
288
330
  toaster.show({
289
331
  type: 'success',
290
332
  title: t('settings.defaultAccess.deleted', { name: name(removing) }),
291
333
  });
292
- rules.replaceData({
293
- data: (rules.data?.data ?? []).filter(
294
- (rule) => rule.key !== removing.key,
295
- ),
296
- });
297
334
  setRemoving(undefined);
298
335
  rules.reload();
299
336
  requestAnimationFrame(() => searchRef.current?.focus());
300
337
  } catch (next) {
301
338
  setError(next);
339
+ if (
340
+ submitted &&
341
+ (!(next instanceof ApiClientError) || next.status >= 500)
342
+ ) {
343
+ // The write may have committed before the response was lost. Verify without replaying it.
344
+ rules.reload();
345
+ }
302
346
  } finally {
303
347
  setBusy(false);
304
348
  }
@@ -307,9 +351,7 @@ export default function DefaultAccessPage(): ReactElement {
307
351
  <>
308
352
  <PermissionManagementPage
309
353
  tab='default-access'
310
- actions={
311
- rules.data && (rules.data.data.length || search) ? create : undefined
312
- }
354
+ actions={canShowCreate ? create : undefined}
313
355
  >
314
356
  <div className='flex flex-wrap items-center gap-2'>
315
357
  <Input
@@ -331,11 +373,16 @@ export default function DefaultAccessPage(): ReactElement {
331
373
  )
332
374
  }
333
375
  />
334
- {search && (
376
+ {search && listState !== 'no-results' && (
335
377
  <Button variant='ghost' onClick={clearSearch}>
336
378
  {t('settings.clear')}
337
379
  </Button>
338
380
  )}
381
+ {active &&
382
+ (rules.loading || catalog.loading) &&
383
+ (rules.data || catalog.data) ? (
384
+ <Spinner className='text-muted-foreground' />
385
+ ) : null}
339
386
  </div>
340
387
  {conflict && (
341
388
  <Alert variant='destructive'>
@@ -344,62 +391,22 @@ export default function DefaultAccessPage(): ReactElement {
344
391
  </AlertDescription>
345
392
  </Alert>
346
393
  )}
347
- <SettingsError
348
- error={rules.error || catalog.error || (!removing && error)}
349
- retry={() => {
394
+ <SettingsError error={!removing && error} />
395
+ <DataTable
396
+ {...tableState}
397
+ columns={columns}
398
+ data={rows}
399
+ getRowId={(rule) => rule.key}
400
+ showSelectedCount={false}
401
+ pageSize={20}
402
+ loading={rules.loading || catalog.loading}
403
+ listState={listState}
404
+ onRetry={() => {
350
405
  rules.reload();
351
406
  catalog.reload();
352
- setError(undefined);
353
407
  }}
408
+ onClearFilters={clearSearch}
354
409
  />
355
- {(rules.loading && !rules.data) ||
356
- (catalog.loading && !catalog.data) ? (
357
- <SettingsLoading />
358
- ) : (
359
- !rules.error &&
360
- !catalog.error &&
361
- (rows.length ? (
362
- <DataTable
363
- {...tableState}
364
- columns={columns}
365
- data={rows}
366
- getRowId={(rule) => rule.key}
367
- showSelectedCount={false}
368
- pageSize={20}
369
- />
370
- ) : (
371
- <Empty className='min-h-48 border border-dashed'>
372
- <EmptyMedia variant='icon'>
373
- <ListFilterIcon />
374
- </EmptyMedia>
375
- <EmptyTitle>
376
- {t(
377
- search
378
- ? 'settings.noResults'
379
- : 'settings.defaultAccess.empty',
380
- )}
381
- </EmptyTitle>
382
- <EmptyDescription>
383
- {t(
384
- search
385
- ? 'settings.clearHint'
386
- : canCreate
387
- ? 'settings.defaultAccess.emptyHint'
388
- : 'settings.defaultAccess.readOnlyEmpty',
389
- )}
390
- </EmptyDescription>
391
- <EmptyContent>
392
- {search ? (
393
- <Button variant='outline' onClick={clearSearch}>
394
- {t('settings.clear')}
395
- </Button>
396
- ) : (
397
- create
398
- )}
399
- </EmptyContent>
400
- </Empty>
401
- ))
402
- )}
403
410
  <ConfirmAction
404
411
  error={<SettingsError error={error} />}
405
412
  open={Boolean(removing)}
@@ -1,67 +1,12 @@
1
- import {
2
- ApiClientError,
3
- useApiClient,
4
- useNavigationGuard,
5
- } from '@nocobase/app-client';
1
+ import { useNavigationGuard } from '@nocobase/app-client';
6
2
  import { useCan } from '@nocobase/app-plugin-authorization/client';
7
3
  import { useTranslation } from '@nocobase/i18n/client';
8
- import { useCallback, useEffect, useRef, useState } from 'react';
4
+ import { useEffect, useRef, useState } from 'react';
9
5
  import { useLocation } from 'react-router';
10
6
  import { ConfirmAction } from './shared.js';
11
7
 
12
- export function useSettingsData<T>(path: string, keepPreviousData = false) {
13
- const api = useApiClient();
14
- const [revision, setRevision] = useState(0);
15
- const [result, setResult] = useState<{
16
- path: string;
17
- revision: number;
18
- data?: T;
19
- error?: unknown;
20
- loading: boolean;
21
- }>({ path, revision: -1, loading: true });
22
- useEffect(() => {
23
- let active = true;
24
- void api.request<T>({ path }).then(
25
- (data) => {
26
- if (active) setResult({ path, revision, data, loading: false });
27
- },
28
- (error: unknown) => {
29
- if (active)
30
- setResult((old) => ({
31
- ...(old.path === path &&
32
- !(
33
- error instanceof ApiClientError &&
34
- [401, 403, 404].includes(error.status)
35
- )
36
- ? old
37
- : {}),
38
- path,
39
- revision,
40
- error,
41
- loading: false,
42
- }));
43
- },
44
- );
45
- return () => {
46
- active = false;
47
- };
48
- }, [api, path, revision]);
49
- return {
50
- ...(result.path === path || keepPreviousData
51
- ? result
52
- : { path, loading: true, data: undefined, error: undefined }),
53
- loading: result.path !== path || result.revision !== revision,
54
- error:
55
- result.path === path && result.revision === revision
56
- ? result.error
57
- : undefined,
58
- reload: useCallback(() => setRevision((value) => value + 1), []),
59
- replaceData: useCallback(
60
- (data: T) => setResult({ path, revision, data, loading: false }),
61
- [path, revision],
62
- ),
63
- };
64
- }
8
+ /** Compatibility entry for prepared settings pages and application-owned consumers. */
9
+ export { useListData as useSettingsData } from '#hooks/use-list-data';
65
10
 
66
11
  export function useUserPermission(action: string, id = '*') {
67
12
  return useCan({ resource: { type: 'user', id }, action }).can;
@@ -101,6 +101,21 @@ export function pagePermissionNavigation(
101
101
  ({ type, id }) => !configuration.has(JSON.stringify([type, id])),
102
102
  );
103
103
  const resources = new Map(pages.map((page) => [page.id, page]));
104
+ const paths = new Map<string, string>();
105
+ const collectPaths = (items: readonly AppClientRegisteredRoute[]): void => {
106
+ for (const route of items) {
107
+ if (
108
+ route.componentLoader &&
109
+ typeof route.authz === 'object' &&
110
+ route.authz.resource.type === 'page' &&
111
+ !paths.has(route.authz.resource.id)
112
+ ) {
113
+ paths.set(route.authz.resource.id, route.path);
114
+ }
115
+ collectPaths(route.children ?? []);
116
+ }
117
+ };
118
+ collectPaths(routes);
104
119
  const seen = new Set<string>();
105
120
  const visit = (
106
121
  items: readonly RouteNavigationItem[],
@@ -130,6 +145,7 @@ export function pagePermissionNavigation(
130
145
  : unnamed(route.path),
131
146
  ),
132
147
  rowId,
148
+ path: route.path,
133
149
  children: descendants,
134
150
  },
135
151
  ];
@@ -142,6 +158,7 @@ export function pagePermissionNavigation(
142
158
  id: `page:${page.id}`,
143
159
  label: text(page.title),
144
160
  rowId: permissionRowId(page, 'access'),
161
+ path: paths.get(page.id),
145
162
  }));
146
163
  return [
147
164
  ...navigation,
@@ -61,7 +61,6 @@ export function setPermission(
61
61
  }
62
62
 
63
63
  const sectionDescriptions: Readonly<Record<string, string>> = {
64
- pages: 'pageAccessHint',
65
64
  business: 'businessOperationsHint',
66
65
  users: 'userAccountsHint',
67
66
  configuration: 'configurationPermissionsHint',
@@ -126,68 +125,64 @@ export function permissionSections(
126
125
  actionKey: action.name,
127
126
  enabled: Boolean(granted),
128
127
  };
129
- // Scope selectors belong to the action and do not inflate permission counts.
128
+ // Keep scope metadata stable for layout and search; only enabled actions render controls.
130
129
  return {
131
130
  ...row,
132
- scopes: granted
133
- ? (resource.dataScopes?.[action.name] ?? [])
134
- .filter((scope) =>
135
- showScope(resource, action.name, scope.key),
136
- )
137
- .map((scope) => {
138
- const value = scopeSelectionKey(
139
- granted.policy?.scopes &&
140
- typeof granted.policy.scopes === 'object'
141
- ? Reflect.get(granted.policy.scopes, scope.key)
142
- : undefined,
143
- );
144
- const preserved =
145
- value !== '' && !scope.recordAccess.includes(value);
146
- return {
147
- id: JSON.stringify([
148
- resource.type,
149
- resource.id,
150
- action.name,
151
- scope.key,
152
- ]),
131
+ scopes: (resource.dataScopes?.[action.name] ?? [])
132
+ .filter((scope) => showScope(resource, action.name, scope.key))
133
+ .map((scope) => {
134
+ const value = scopeSelectionKey(
135
+ granted?.policy?.scopes &&
136
+ typeof granted.policy.scopes === 'object'
137
+ ? Reflect.get(granted.policy.scopes, scope.key)
138
+ : undefined,
139
+ );
140
+ const preserved =
141
+ value !== '' && !scope.recordAccess.includes(value);
142
+ return {
143
+ id: JSON.stringify([
144
+ resource.type,
145
+ resource.id,
146
+ action.name,
147
+ scope.key,
148
+ ]),
149
+ label: text(
150
+ typeof scope.title === 'string' &&
151
+ scope.title === scope.collection
152
+ ? { key: 'settings.recordScope' }
153
+ : scope.title,
154
+ ),
155
+ levels: [
156
+ ...(preserved
157
+ ? [
158
+ {
159
+ value,
160
+ label: text({
161
+ key: 'settings.preservedRecordScope',
162
+ }),
163
+ },
164
+ ]
165
+ : []),
166
+ {
167
+ value: '',
168
+ label: text({ key: 'settings.noRecordScope' }),
169
+ },
170
+ ...scope.recordAccess.map((key) => ({
171
+ value: key,
153
172
  label: text(
154
- typeof scope.title === 'string' &&
155
- scope.title === scope.collection
156
- ? { key: 'settings.recordScope' }
157
- : scope.title,
173
+ catalog.recordAccess.find(
174
+ (entry) => entry.key === key,
175
+ )?.title ?? key,
158
176
  ),
159
- levels: [
160
- ...(preserved
161
- ? [
162
- {
163
- value,
164
- label: text({
165
- key: 'settings.preservedRecordScope',
166
- }),
167
- },
168
- ]
169
- : []),
170
- {
171
- value: '',
172
- label: text({ key: 'settings.noRecordScope' }),
173
- },
174
- ...scope.recordAccess.map((key) => ({
175
- value: key,
176
- label: text(
177
- catalog.recordAccess.find(
178
- (entry) => entry.key === key,
179
- )?.title ?? key,
180
- ),
181
- })),
182
- ],
183
- level: value,
184
- hint:
185
- value === ''
186
- ? text({ key: 'settings.noAdditionalScopeHint' })
187
- : undefined,
188
- };
189
- })
190
- : [],
177
+ })),
178
+ ],
179
+ level: value,
180
+ hint:
181
+ value === ''
182
+ ? text({ key: 'settings.noAdditionalScopeHint' })
183
+ : undefined,
184
+ };
185
+ }),
191
186
  };
192
187
  }),
193
188
  })),