@nocobase/app-template-default 1.0.0-beta.68 → 1.0.0-beta.70

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 (76) hide show
  1. package/.agents/rules/components.md +5 -1
  2. package/.agents/rules/shell.md +3 -1
  3. package/AGENTS.md +7 -3
  4. package/Dockerfile.dockerignore +0 -1
  5. package/README.MD +3 -1
  6. package/client/app.ts +15 -0
  7. package/client/components/data-table/column-header.tsx +91 -0
  8. package/client/components/data-table/data-table.tsx +216 -0
  9. package/client/components/data-table/index.ts +13 -0
  10. package/client/components/data-table/pagination.tsx +165 -0
  11. package/client/components/data-table/view-options.tsx +77 -0
  12. package/client/components/flow-diagram-css.d.ts +2 -0
  13. package/client/components/flow-diagram.tsx +674 -0
  14. package/client/components/flow-layout.ts +250 -0
  15. package/client/components/lifecycle-diagram-model.ts +217 -0
  16. package/client/components/lifecycle-diagram.tsx +310 -0
  17. package/client/components/permission-editor.tsx +589 -0
  18. package/client/components/permission-filter.tsx +455 -0
  19. package/client/components/route-overlay.tsx +26 -13
  20. package/client/components/ui/checkbox.tsx +25 -0
  21. package/client/components/ui/sidebar.tsx +1 -4
  22. package/client/components/ui/switch.tsx +30 -0
  23. package/client/components/ui/table.tsx +113 -0
  24. package/client/extensions/nocobase-workflow-management/README.md +70 -0
  25. package/client/extensions/nocobase-workflow-management/definition-list.tsx +148 -0
  26. package/client/extensions/nocobase-workflow-management/execution-detail.tsx +230 -0
  27. package/client/extensions/nocobase-workflow-management/execution-list.tsx +175 -0
  28. package/client/extensions/nocobase-workflow-management/layout.tsx +95 -0
  29. package/client/layouts/components/app-sidebar.tsx +1 -1
  30. package/client/layouts/use-sidebar-preference.ts +62 -42
  31. package/client/locales/system/en-US.ts +29 -0
  32. package/client/locales/system/settings-en-US.ts +142 -0
  33. package/client/locales/system/settings-zh-CN.ts +136 -0
  34. package/client/locales/system/zh-CN.ts +29 -0
  35. package/client/pages/auth/auth-page.css +15 -0
  36. package/client/pages/auth/login.tsx +1 -1
  37. package/client/pages/auth/shared.tsx +3 -1
  38. package/client/pages/settings/hooks.tsx +137 -0
  39. package/client/pages/settings/page-permission-titles.ts +7 -0
  40. package/client/pages/settings/page-permissions.ts +42 -0
  41. package/client/pages/settings/permission-filters.tsx +119 -0
  42. package/client/pages/settings/permissions-model.ts +260 -0
  43. package/client/pages/settings/role-form.tsx +405 -0
  44. package/client/pages/settings/roles.tsx +163 -0
  45. package/client/pages/settings/routes.ts +79 -0
  46. package/client/pages/settings/shared.tsx +119 -0
  47. package/client/pages/settings/user-detail.tsx +268 -0
  48. package/client/pages/settings/user-form.tsx +427 -0
  49. package/client/pages/settings/users.tsx +451 -0
  50. package/client/routing/app-router.tsx +4 -1
  51. package/client/routing/session-preparation.tsx +65 -0
  52. package/client/styles.css +4 -7
  53. package/package.json +27 -24
  54. package/tests/components/app-layout-routes.test.tsx +14 -7
  55. package/tests/components/data-table.test.tsx +127 -0
  56. package/tests/components/layout-i18n.test.tsx +8 -1
  57. package/tests/components/navigation-menu.test.tsx +24 -14
  58. package/tests/components/permission-editor.test.tsx +477 -0
  59. package/tests/components/permission-filter-binding.test.tsx +150 -0
  60. package/tests/components/permission-filter.test.tsx +202 -0
  61. package/tests/components/settings.test.tsx +294 -0
  62. package/tests/components/sidebar-permissions.test.tsx +12 -4
  63. package/tests/components/sidebar-preference.test.tsx +209 -14
  64. package/tests/components/user-permissions.test.tsx +142 -0
  65. package/tests/fixtures/theme-tokens.tsx +9 -1
  66. package/tests/logic/asset-url-build.test.ts +0 -1
  67. package/tests/logic/client-routes.test.ts +40 -0
  68. package/tests/logic/client-shell.test.tsx +10 -2
  69. package/tests/logic/config-example.test.ts +30 -0
  70. package/tests/logic/dev-client-watch.test.ts +8 -1
  71. package/tests/logic/page-permission-titles.test.ts +223 -0
  72. package/tests/logic/route-overlay.test.tsx +42 -0
  73. package/tests/logic/session-preparation.test.tsx +223 -0
  74. package/tests/logic/settings-permissions.test.ts +168 -0
  75. package/vite.config.ts +0 -22
  76. package/client/agent-annotations-host.ts +0 -25
@@ -0,0 +1,175 @@
1
+ import type { ReactElement, ReactNode } from 'react';
2
+ import type {
3
+ CellContext,
4
+ ColumnMeta,
5
+ HeaderContext,
6
+ } from '@tanstack/react-table';
7
+ import { ArrowDown, ArrowUp } from 'lucide-react';
8
+ import { Button } from '#components/ui/button';
9
+ import { Empty, EmptyDescription } from '#components/ui/empty';
10
+ import { DataTable } from '#components/data-table/data-table';
11
+
12
+ export interface WorkflowExecutionRow {
13
+ id: string;
14
+ record: ReactNode;
15
+ workflow: ReactNode;
16
+ state: ReactNode;
17
+ createdAt: ReactNode;
18
+ }
19
+ export interface WorkflowExecutionListProps {
20
+ rows: readonly WorkflowExecutionRow[];
21
+ loaded: boolean;
22
+ loading: boolean;
23
+ filters: ReactNode;
24
+ status?: ReactNode;
25
+ empty: ReactNode;
26
+ labels: {
27
+ refresh: string;
28
+ record: string;
29
+ workflow: string;
30
+ state: string;
31
+ createdAt: string;
32
+ previous: string;
33
+ next: string;
34
+ };
35
+ order: 'newest' | 'oldest';
36
+ onSort: () => void;
37
+ onRefresh: () => void;
38
+ pagination: ReactNode;
39
+ previousDisabled: boolean;
40
+ nextDisabled: boolean;
41
+ onPrevious: () => void;
42
+ onNext: () => void;
43
+ }
44
+
45
+ type ExecutionDateMeta = ColumnMeta<WorkflowExecutionRow, unknown> &
46
+ Pick<WorkflowExecutionListProps, 'labels' | 'order' | 'onSort'>;
47
+
48
+ export function WorkflowExecutionList({
49
+ rows,
50
+ loaded,
51
+ loading,
52
+ filters,
53
+ status,
54
+ empty,
55
+ labels,
56
+ order,
57
+ onSort,
58
+ onRefresh,
59
+ pagination,
60
+ previousDisabled,
61
+ nextDisabled,
62
+ onPrevious,
63
+ onNext,
64
+ }: WorkflowExecutionListProps): ReactElement {
65
+ const activityMeta: ExecutionDateMeta = { labels, order, onSort };
66
+ return (
67
+ <section className='flex flex-col gap-4'>
68
+ <div className='flex flex-wrap items-center justify-between gap-3'>
69
+ <div className='flex flex-wrap items-center gap-2'>{filters}</div>
70
+ <Button variant='outline' disabled={loading} onClick={onRefresh}>
71
+ {labels.refresh}
72
+ </Button>
73
+ </div>
74
+ {status}
75
+ <div className='bg-card'>
76
+ <DataTable
77
+ data={rows}
78
+ getRowId={(row) => row.id}
79
+ pagination={false}
80
+ manualSorting
81
+ sortingState={[{ id: 'createdAt', desc: order === 'newest' }]}
82
+ loading={loading}
83
+ emptyMessage={
84
+ loaded ? (
85
+ <Empty>
86
+ <EmptyDescription>{empty}</EmptyDescription>
87
+ </Empty>
88
+ ) : (
89
+ ''
90
+ )
91
+ }
92
+ columns={[
93
+ {
94
+ id: 'record',
95
+ header: labels.record,
96
+ enableSorting: false,
97
+ cell: ExecutionRecordCell,
98
+ },
99
+ {
100
+ id: 'workflow',
101
+ header: labels.workflow,
102
+ enableSorting: false,
103
+ cell: ExecutionWorkflowCell,
104
+ },
105
+ {
106
+ id: 'state',
107
+ header: labels.state,
108
+ enableSorting: false,
109
+ cell: ExecutionStateCell,
110
+ },
111
+ {
112
+ id: 'createdAt',
113
+ accessorFn: (row) => row.createdAt,
114
+ enableSorting: true,
115
+ meta: activityMeta,
116
+ header: ExecutionDateHeader,
117
+ cell: ExecutionCreatedAtCell,
118
+ },
119
+ ]}
120
+ />
121
+ </div>
122
+ <div className='flex flex-wrap items-center justify-between gap-3 text-sm'>
123
+ <p>{pagination}</p>
124
+ <div className='flex gap-2'>
125
+ <Button
126
+ variant='outline'
127
+ disabled={previousDisabled}
128
+ onClick={onPrevious}
129
+ >
130
+ {labels.previous}
131
+ </Button>
132
+ <Button variant='outline' disabled={nextDisabled} onClick={onNext}>
133
+ {labels.next}
134
+ </Button>
135
+ </div>
136
+ </div>
137
+ </section>
138
+ );
139
+ }
140
+
141
+ function ExecutionRecordCell({
142
+ row,
143
+ }: CellContext<WorkflowExecutionRow, unknown>) {
144
+ return row.original.record;
145
+ }
146
+
147
+ function ExecutionWorkflowCell({
148
+ row,
149
+ }: CellContext<WorkflowExecutionRow, unknown>) {
150
+ return row.original.workflow;
151
+ }
152
+
153
+ function ExecutionStateCell({
154
+ row,
155
+ }: CellContext<WorkflowExecutionRow, unknown>) {
156
+ return row.original.state;
157
+ }
158
+
159
+ function ExecutionCreatedAtCell({
160
+ row,
161
+ }: CellContext<WorkflowExecutionRow, unknown>) {
162
+ return row.original.createdAt;
163
+ }
164
+
165
+ function ExecutionDateHeader({
166
+ column,
167
+ }: HeaderContext<WorkflowExecutionRow, unknown>) {
168
+ const { labels, order, onSort } = column.columnDef.meta as ExecutionDateMeta;
169
+ return (
170
+ <Button variant='ghost' size='sm' onClick={onSort}>
171
+ {labels.createdAt}
172
+ {order === 'newest' ? <ArrowDown /> : <ArrowUp />}
173
+ </Button>
174
+ );
175
+ }
@@ -0,0 +1,95 @@
1
+ import type { ReactElement, ReactNode } from 'react';
2
+ import { PageContainer } from '#components/page-container';
3
+ import { PageHeader } from '#components/page-header';
4
+ import { Tabs, TabsList, TabsTrigger } from '#components/ui/tabs';
5
+ import { Card, CardContent, CardHeader, CardTitle } from '#components/ui/card';
6
+
7
+ export type WorkflowTab = 'definitions' | 'executions';
8
+
9
+ /** The caller owns routes, tab URLs and permissions; details replace the list chrome. */
10
+ export interface WorkflowManagementLayoutProps {
11
+ title: string;
12
+ description?: ReactNode;
13
+ labels: Record<WorkflowTab, string>;
14
+ tab: WorkflowTab;
15
+ onTabChange: (tab: WorkflowTab) => void;
16
+ detail?: boolean;
17
+ children: ReactNode;
18
+ }
19
+
20
+ export function WorkflowManagementLayout({
21
+ title,
22
+ description,
23
+ labels,
24
+ tab,
25
+ onTabChange,
26
+ detail = false,
27
+ children,
28
+ }: WorkflowManagementLayoutProps): ReactElement {
29
+ return (
30
+ <PageContainer>
31
+ {!detail && (
32
+ <>
33
+ <PageHeader title={title} description={description} />
34
+ <Tabs
35
+ value={tab}
36
+ onValueChange={(value) => onTabChange(value as WorkflowTab)}
37
+ >
38
+ <TabsList variant='line'>
39
+ <TabsTrigger value='definitions'>
40
+ {labels.definitions}
41
+ </TabsTrigger>
42
+ <TabsTrigger value='executions'>{labels.executions}</TabsTrigger>
43
+ </TabsList>
44
+ </Tabs>
45
+ </>
46
+ )}
47
+ {children}
48
+ </PageContainer>
49
+ );
50
+ }
51
+
52
+ export interface WorkflowDefinitionDetailProps {
53
+ diagram: ReactNode;
54
+ parameters?: ReactNode;
55
+ diagramTitle: string;
56
+ parametersTitle: string;
57
+ }
58
+
59
+ export function WorkflowDefinitionDetail({
60
+ diagram,
61
+ parameters,
62
+ diagramTitle,
63
+ parametersTitle,
64
+ }: WorkflowDefinitionDetailProps): ReactElement {
65
+ return (
66
+ <div className='flex flex-col gap-4'>
67
+ <div
68
+ className={
69
+ parameters == null
70
+ ? 'min-w-0'
71
+ : 'grid min-w-0 grid-cols-1 items-start gap-6 lg:grid-cols-2'
72
+ }
73
+ >
74
+ <Card className='min-w-0' role='region' aria-label={diagramTitle}>
75
+ <CardHeader>
76
+ <CardTitle>
77
+ <h2>{diagramTitle}</h2>
78
+ </CardTitle>
79
+ </CardHeader>
80
+ <CardContent className='flex flex-1 flex-col'>{diagram}</CardContent>
81
+ </Card>
82
+ {parameters != null && (
83
+ <Card className='min-w-0' role='region' aria-label={parametersTitle}>
84
+ <CardHeader>
85
+ <CardTitle>
86
+ <h2>{parametersTitle}</h2>
87
+ </CardTitle>
88
+ </CardHeader>
89
+ <CardContent>{parameters}</CardContent>
90
+ </Card>
91
+ )}
92
+ </div>
93
+ </div>
94
+ );
95
+ }
@@ -33,7 +33,7 @@ import { AppBrand } from './app-brand.js';
33
33
 
34
34
  /**
35
35
  * The shell's sidebar state: shadcn's provider, with the desktop icon mode kept in the shared sidebar preference
36
- * (`use-sidebar-preference.ts`) so every layout on this origin opens it the same way. Whether the phone's sheet is
36
+ * (`use-sidebar-preference.ts`) so every layout of this application opens it the same way. Whether the phone's sheet is
37
37
  * open stays local to the layout.
38
38
  */
39
39
  export function AppSidebarProvider({
@@ -1,57 +1,77 @@
1
- import { useSyncExternalStore, type SetStateAction } from 'react';
2
-
3
- // All applications and layout surfaces on this origin share this preference.
4
- const storageKey = 'nocobase:sidebar:collapsed';
5
- const changeEvent = 'nocobase:sidebar-preference-change';
6
- let memoryValue = false;
7
- let memoryOnly = false;
8
-
9
- function getSnapshot(): boolean {
10
- if (memoryOnly) return memoryValue;
11
- try {
12
- memoryValue = localStorage.getItem(storageKey) === 'true';
13
- } catch {
14
- // Storage may be unavailable; keep the current session's preference.
1
+ import {
2
+ createContext,
3
+ useContext,
4
+ useSyncExternalStore,
5
+ type SetStateAction,
6
+ } from 'react';
7
+
8
+ // Each application shares its preference across layouts and tabs, never across scopes.
9
+ export function createSidebarPreferenceStore(scope: string) {
10
+ const storageKey = `nocobase:sidebar:collapsed:${scope}`;
11
+ const changeEvent = `nocobase:sidebar-preference-change:${scope}`;
12
+ let memoryValue = false;
13
+ let memoryOnly = false;
14
+
15
+ function getSnapshot(): boolean {
16
+ if (memoryOnly) return memoryValue;
17
+ try {
18
+ memoryValue = localStorage.getItem(storageKey) === 'true';
19
+ } catch {
20
+ // Storage may be unavailable; keep this application's session preference.
21
+ }
22
+ return memoryValue;
15
23
  }
16
- return memoryValue;
17
- }
18
24
 
19
- function subscribe(notify: () => void): () => void {
20
- const onStorage = (event: StorageEvent) => {
21
- if (event.key === storageKey || event.key === null) notify();
22
- };
23
- window.addEventListener('storage', onStorage);
24
- window.addEventListener(changeEvent, notify);
25
- return () => {
26
- window.removeEventListener('storage', onStorage);
27
- window.removeEventListener(changeEvent, notify);
28
- };
29
- }
25
+ function subscribe(notify: () => void): () => void {
26
+ const onStorage = (event: StorageEvent) => {
27
+ if (event.key === storageKey || event.key === null) notify();
28
+ };
29
+ window.addEventListener('storage', onStorage);
30
+ window.addEventListener(changeEvent, notify);
31
+ return () => {
32
+ window.removeEventListener('storage', onStorage);
33
+ window.removeEventListener(changeEvent, notify);
34
+ };
35
+ }
30
36
 
31
- function setCollapsed(update: SetStateAction<boolean>): void {
32
- const current = getSnapshot();
33
- memoryValue = typeof update === 'function' ? update(current) : update;
34
- try {
35
- localStorage.setItem(storageKey, String(memoryValue));
36
- } catch {
37
- memoryOnly = true;
38
- // The toggle remains usable when storage is blocked.
37
+ function setCollapsed(update: SetStateAction<boolean>): void {
38
+ const current = getSnapshot();
39
+ memoryValue = typeof update === 'function' ? update(current) : update;
40
+ try {
41
+ localStorage.setItem(storageKey, String(memoryValue));
42
+ } catch {
43
+ memoryOnly = true;
44
+ // The toggle remains usable when storage is blocked.
45
+ }
46
+ window.dispatchEvent(new Event(changeEvent));
39
47
  }
40
- window.dispatchEvent(new Event(changeEvent));
48
+
49
+ return { getSnapshot, subscribe, setCollapsed };
41
50
  }
42
51
 
43
- function getServerSnapshot(): boolean {
44
- return false;
52
+ type SidebarPreferenceStore = ReturnType<typeof createSidebarPreferenceStore>;
53
+
54
+ export function createAppSidebarPreferenceStore(basename: string = '') {
55
+ const normalizedBasename = `/${basename.replace(/^\/+|\/+$/g, '')}`;
56
+ return createSidebarPreferenceStore(`app:${normalizedBasename}`);
45
57
  }
46
58
 
59
+ const SidebarPreferenceContext = createContext<SidebarPreferenceStore | null>(
60
+ null,
61
+ );
62
+
63
+ export const SidebarPreferenceProvider = SidebarPreferenceContext.Provider;
64
+
47
65
  export function useSidebarPreference(): readonly [
48
66
  boolean,
49
67
  (update: SetStateAction<boolean>) => void,
50
68
  ] {
69
+ const store = useContext(SidebarPreferenceContext);
70
+ if (!store) throw new Error('SidebarPreferenceProvider is required.');
51
71
  const collapsed = useSyncExternalStore(
52
- subscribe,
53
- getSnapshot,
54
- getServerSnapshot,
72
+ store.subscribe,
73
+ store.getSnapshot,
74
+ () => false,
55
75
  );
56
- return [collapsed, setCollapsed];
76
+ return [collapsed, store.setCollapsed];
57
77
  }
@@ -1,3 +1,4 @@
1
+ import settings from './settings-en-US.js';
1
2
  import type { LocaleResource } from '@nocobase/i18n';
2
3
  import deviceApprovalEnUS from '#extensions/nocobase-device-approval/locales/en-US';
3
4
  import inboxEnUS from '#extensions/nocobase-inbox/locales/en-US';
@@ -7,6 +8,34 @@ import inboxEnUS from '#extensions/nocobase-inbox/locales/en-US';
7
8
  * upgrade replaces this file, so put the application's own copy in `../en-US.ts` rather than here.
8
9
  */
9
10
  const systemEnUS = {
11
+ flowDiagram: {
12
+ configure: 'Configure',
13
+ overview: 'Diagram overview',
14
+ actualSize: 'Actual size',
15
+ states: 'States',
16
+ transitions: 'Transitions',
17
+ configurable: 'Parameters',
18
+ current: 'Current',
19
+ visited: 'Visited',
20
+ initial: 'Initial',
21
+ final: 'Final',
22
+ state: 'State',
23
+ zoomOut: 'Zoom out',
24
+ zoomIn: 'Zoom in',
25
+ fit: 'Fit to view',
26
+
27
+ empty: 'No states to display.',
28
+ loading: 'Laying out diagram…',
29
+ failed: 'Unable to lay out this diagram.',
30
+ nodeHelp: 'Press Enter or Space to select a state.',
31
+ edgeHelp: 'Press Enter or Space to select a transition.',
32
+ },
33
+ settings,
34
+ 'auth.preparingSession': 'Preparing your session…',
35
+ 'auth.connectionFailed': 'Unable to connect',
36
+ 'auth.connectionFailedDescription':
37
+ 'The realtime connection could not be prepared. Retry to enter the application, or sign out.',
38
+
10
39
  'auth.passwordResetSuccess':
11
40
  'Your password has been reset. Sign in with your new password.',
12
41
  'auth.requestNewResetLink': 'Request a new reset link',
@@ -0,0 +1,142 @@
1
+ const settings = {
2
+ unnamedPage: 'Unnamed page ({{path}})',
3
+ revokeRoles: 'Revoke roles',
4
+ keyConflict: 'This role key is already in use.',
5
+ userPermissions: 'User management',
6
+ readUsers: 'View users',
7
+ createUsers: 'Create users',
8
+ updateUsers: 'Edit users',
9
+ inviteUsers: 'Invite users',
10
+ deleteUsers: 'Delete users',
11
+ resetUserPassword: 'Reset password',
12
+ assignRole: 'Assign roles',
13
+
14
+ assignedRoles: 'Assigned roles',
15
+ actionSaved: 'Updated access for "{{name}}"',
16
+
17
+ identityConflict: 'This email or username is already in use.',
18
+ signInAgain: 'Sign in again',
19
+ passwordPolicy: 'Password does not meet the current password policy.',
20
+
21
+ reload: 'Reload role',
22
+ title: 'System settings',
23
+ users: 'Users',
24
+ roles: 'Roles and permissions',
25
+ usersDescription: 'Manage accounts, access and active sessions.',
26
+ rolesDescription: 'Define what each role can access and do.',
27
+ userSearch: 'Search names, usernames or emails',
28
+ roleSearch: 'Search role names',
29
+ newUser: 'New user',
30
+ editUser: 'Edit user',
31
+ newRole: 'New role',
32
+ editRole: 'Edit role',
33
+ name: 'Name',
34
+ username: 'Username',
35
+ email: 'Email',
36
+ password: 'Initial password',
37
+ roleName: 'Role name',
38
+ key: 'Key',
39
+ status: 'Status',
40
+ actions: 'Actions',
41
+ actionsFor: 'Actions for {{name}}',
42
+ all: 'All statuses',
43
+ enabled: 'Enabled',
44
+ disabled: 'Disabled',
45
+ clear: 'Clear filters',
46
+ noResults: 'No matching results',
47
+ clearHint: 'Try a different search or clear the filters.',
48
+ noUsers: 'No users yet',
49
+ noRoles: 'No roles yet',
50
+ create: 'Create',
51
+ edit: 'Edit',
52
+ save: 'Save',
53
+ cancel: 'Cancel',
54
+ saving: 'Saving…',
55
+ confirm: 'Confirm',
56
+ enable: 'Enable',
57
+ disable: 'Disable',
58
+ revokeSessions: 'Revoke sessions',
59
+ disableTitle: 'Disable user "{{name}}"?',
60
+ disableHint:
61
+ 'This user will be unable to sign in and existing sessions will end.',
62
+ enableTitle: 'Enable user "{{name}}"?',
63
+ enableHint: 'This user will be able to sign in again.',
64
+ revokeTitle: 'Revoke sessions for "{{name}}"?',
65
+ revokeHint: 'This user will need to sign in again.',
66
+ revokeRoleTitle: 'Revoke roles from "{{name}}"?',
67
+ revokeRoleHint: 'Access granted by the listed roles will be removed.',
68
+ created: 'Created user "{{name}}"',
69
+ saved: 'Saved "{{name}}"',
70
+ rolesSaved: 'Updated roles for "{{name}}"',
71
+ required: 'Enter a value for this field',
72
+ invalidEmail: 'Enter a valid email address',
73
+ protected: 'Protected',
74
+ custom: 'Custom',
75
+ protectedHint:
76
+ 'This role is protected. Only explicitly permitted changes are available.',
77
+ unsaved: 'Unsaved changes',
78
+ discardTitle: 'Discard changes?',
79
+ discardHint: 'Your unsaved changes will be lost.',
80
+ failed: 'Unable to complete the request. Try again.',
81
+ retry: 'Retry',
82
+ loading: 'Loading',
83
+ denied: 'You do not have access. Contact an administrator.',
84
+ missing: 'This record does not exist or has been deleted.',
85
+ conflict:
86
+ 'This role has changed. Reload it and merge your changes before saving.',
87
+ sessionEnded: 'Your session has ended. Sign in again.',
88
+ previous: 'Previous',
89
+ next: 'Next',
90
+ total_one: '{{count}} user',
91
+ total_other: '{{count}} users',
92
+ access: 'Access',
93
+ permissionSearch: 'Search categories, groups or permissions',
94
+ enabledOnly: 'Enabled only',
95
+ noPermissions: 'No permissions to configure',
96
+ permissionCategories: 'Permission categories',
97
+ selectAllPermissions: 'All',
98
+ selectVisiblePermissions: 'Select visible permissions in {{name}}',
99
+ permissionSummary: '{{enabled}} of {{total}} enabled',
100
+ noRecordScope: 'No records (not authorized)',
101
+ permissionFilter: {
102
+ limit: 'Up to 20 conditions',
103
+ configure: 'Conditions',
104
+ title: 'Configure record filter',
105
+ description:
106
+ 'Apply conditions to the role draft, then save the role to make them effective.',
107
+ all: 'Match all conditions',
108
+ any: 'Match any condition',
109
+ field: 'Field',
110
+ operator: 'Operator',
111
+ valueType: 'Value type',
112
+ value: 'Value',
113
+ text: 'Text',
114
+ number: 'Number',
115
+ boolean: 'Boolean',
116
+ true: 'Yes',
117
+ false: 'No',
118
+ add: 'Add condition',
119
+ remove: 'Remove',
120
+ cancel: 'Cancel',
121
+ apply: 'Apply',
122
+ invalid:
123
+ 'Choose valid fields and complete all values. Include at least one condition.',
124
+ advanced:
125
+ 'This existing filter uses advanced conditions. It will be preserved unless you explicitly reconfigure it.',
126
+ reconfigure: 'Reconfigure',
127
+ operators: {
128
+ $eq: 'Equals',
129
+ $ne: 'Does not equal',
130
+ $includes: 'Contains',
131
+ $gt: 'Greater than',
132
+ $gte: 'At least',
133
+ $lt: 'Less than',
134
+ $lte: 'At most',
135
+ $empty: 'Is empty',
136
+ $notEmpty: 'Is not empty',
137
+ },
138
+ },
139
+ recordScope: 'Record scope',
140
+ levelFor: 'Scope for {{name}}',
141
+ };
142
+ export default settings;