@nocobase/app-template-default 1.0.0-beta.69 → 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.
@@ -0,0 +1,230 @@
1
+ import { useState, type ReactElement, type ReactNode } from 'react';
2
+ import { Button } from '#components/ui/button';
3
+ import { Card, CardContent, CardHeader, CardTitle } from '#components/ui/card';
4
+ import {
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableHead,
9
+ TableHeader,
10
+ TableRow,
11
+ } from '#components/ui/table';
12
+
13
+ export interface WorkflowHistoryEntry {
14
+ readonly id: string;
15
+ readonly version: number;
16
+ readonly at: string;
17
+ readonly transition: string;
18
+ readonly from: string | null;
19
+ readonly to: string;
20
+ readonly actorId?: string | null;
21
+ }
22
+ export interface WorkflowEffectEntry {
23
+ readonly id: string;
24
+ readonly effect: string;
25
+ readonly status: string;
26
+ readonly attempts: number;
27
+ readonly maxAttempts: number;
28
+ readonly error?: string | null;
29
+ }
30
+ export interface WorkflowExecutionLabels {
31
+ processDiagram: string;
32
+ history: string;
33
+ historyHint: string;
34
+ step: string;
35
+ transition: string;
36
+ from: string;
37
+ to: string;
38
+ actor: string;
39
+ time: string;
40
+ noHistory: string;
41
+ effects: string;
42
+ effect: string;
43
+ state: string;
44
+ attempts: string;
45
+ error: string;
46
+ noEffects: string;
47
+ executionParameters: string;
48
+ snapshotHint: string;
49
+ showStep: (step: number) => string;
50
+ }
51
+ /** Historical selection only changes the display; execution and recovery remain caller-owned. */
52
+ export interface WorkflowExecutionDetailProps<T extends WorkflowHistoryEntry> {
53
+ transitions: readonly T[];
54
+ state: string;
55
+ effects: readonly WorkflowEffectEntry[];
56
+ parameters?: ReactNode;
57
+ labels: WorkflowExecutionLabels;
58
+ stateLabel: (name: string) => ReactNode;
59
+ transitionLabel: (name: string) => ReactNode;
60
+ dateLabel: (at: string) => ReactNode;
61
+ effectStatusLabel: (status: string) => ReactNode;
62
+ renderDiagram: (selection: {
63
+ current: string;
64
+ history: readonly T[];
65
+ }) => ReactNode;
66
+ }
67
+
68
+ export function WorkflowExecutionDetail<T extends WorkflowHistoryEntry>({
69
+ transitions: transitionsInput,
70
+ state,
71
+ effects,
72
+ parameters,
73
+ labels,
74
+ stateLabel,
75
+ transitionLabel,
76
+ dateLabel,
77
+ effectStatusLabel,
78
+ renderDiagram,
79
+ }: WorkflowExecutionDetailProps<T>): ReactElement {
80
+ const [selected, setSelected] = useState<string>();
81
+ const transitions = [...transitionsInput].sort(
82
+ (a, b) =>
83
+ a.version - b.version ||
84
+ a.at.localeCompare(b.at) ||
85
+ a.id.localeCompare(b.id),
86
+ );
87
+ const index = selected
88
+ ? transitions.findIndex((entry) => entry.id === selected)
89
+ : -1;
90
+ const history = index < 0 ? transitions : transitions.slice(0, index + 1);
91
+ const current = index < 0 ? state : transitions[index].to;
92
+ return (
93
+ <div className='flex flex-col gap-4'>
94
+ <div className='grid min-w-0 grid-cols-1 items-stretch gap-6 lg:grid-cols-2'>
95
+ <Card
96
+ className='min-w-0'
97
+ role='region'
98
+ aria-label={labels.processDiagram}
99
+ >
100
+ <CardHeader>
101
+ <CardTitle>
102
+ <h2>{labels.processDiagram}</h2>
103
+ </CardTitle>
104
+ </CardHeader>
105
+ <CardContent className='flex flex-1 flex-col gap-4'>
106
+ {renderDiagram({ current, history })}
107
+ </CardContent>
108
+ </Card>
109
+ <div className='flex min-w-0 flex-col gap-6'>
110
+ <Card className='min-w-0' role='region' aria-label={labels.history}>
111
+ <CardHeader>
112
+ <CardTitle>
113
+ <h2>{labels.history}</h2>
114
+ </CardTitle>
115
+ </CardHeader>
116
+ <CardContent className='min-w-0'>
117
+ <p className='mb-4 text-sm text-muted-foreground'>
118
+ {labels.historyHint}
119
+ </p>
120
+ <Table>
121
+ <TableHeader>
122
+ <TableRow>
123
+ <TableHead>{labels.step}</TableHead>
124
+ <TableHead>{labels.transition}</TableHead>
125
+ <TableHead>{labels.from}</TableHead>
126
+ <TableHead>{labels.to}</TableHead>
127
+ <TableHead>{labels.actor}</TableHead>
128
+ <TableHead>{labels.time}</TableHead>
129
+ </TableRow>
130
+ </TableHeader>
131
+ <TableBody>
132
+ {transitions.map((entry, step) => (
133
+ <TableRow
134
+ key={entry.id}
135
+ data-selected={entry.id === selected ? 'true' : undefined}
136
+ >
137
+ <TableCell>
138
+ <Button
139
+ size='sm'
140
+ variant={
141
+ entry.id === selected ? 'secondary' : 'ghost'
142
+ }
143
+ aria-label={labels.showStep(step + 1)}
144
+ aria-pressed={entry.id === selected}
145
+ onClick={() => setSelected(entry.id)}
146
+ >
147
+ {step + 1}
148
+ </Button>
149
+ </TableCell>
150
+ <TableCell>{transitionLabel(entry.transition)}</TableCell>
151
+ <TableCell>
152
+ {entry.from ? stateLabel(entry.from) : '—'}
153
+ </TableCell>
154
+ <TableCell>{stateLabel(entry.to)}</TableCell>
155
+ <TableCell>{entry.actorId}</TableCell>
156
+ <TableCell>{dateLabel(entry.at)}</TableCell>
157
+ </TableRow>
158
+ ))}
159
+ </TableBody>
160
+ </Table>
161
+ {!transitions.length && (
162
+ <p className='text-sm text-muted-foreground'>
163
+ {labels.noHistory}
164
+ </p>
165
+ )}
166
+ </CardContent>
167
+ </Card>
168
+ <Card className='min-w-0' role='region' aria-label={labels.effects}>
169
+ <CardHeader>
170
+ <CardTitle>
171
+ <h2>{labels.effects}</h2>
172
+ </CardTitle>
173
+ </CardHeader>
174
+ <CardContent className='min-w-0'>
175
+ {effects.length ? (
176
+ <Table>
177
+ <TableHeader>
178
+ <TableRow>
179
+ <TableHead>{labels.effect}</TableHead>
180
+ <TableHead>{labels.state}</TableHead>
181
+ <TableHead>{labels.attempts}</TableHead>
182
+ <TableHead>{labels.error}</TableHead>
183
+ </TableRow>
184
+ </TableHeader>
185
+ <TableBody>
186
+ {effects.map((run) => (
187
+ <TableRow key={run.id}>
188
+ <TableCell>{run.effect}</TableCell>
189
+ <TableCell>{effectStatusLabel(run.status)}</TableCell>
190
+ <TableCell>
191
+ {run.attempts} / {run.maxAttempts}
192
+ </TableCell>
193
+ <TableCell className='max-w-80 whitespace-normal break-words text-destructive'>
194
+ {run.error || '—'}
195
+ </TableCell>
196
+ </TableRow>
197
+ ))}
198
+ </TableBody>
199
+ </Table>
200
+ ) : (
201
+ <p className='text-sm text-muted-foreground'>
202
+ {labels.noEffects}
203
+ </p>
204
+ )}
205
+ </CardContent>
206
+ </Card>
207
+ {parameters && (
208
+ <Card
209
+ className='min-w-0'
210
+ role='region'
211
+ aria-label={labels.executionParameters}
212
+ >
213
+ <CardHeader>
214
+ <CardTitle>
215
+ <h2>{labels.executionParameters}</h2>
216
+ </CardTitle>
217
+ </CardHeader>
218
+ <CardContent className='min-w-0'>
219
+ <p className='mb-4 text-sm text-muted-foreground'>
220
+ {labels.snapshotHint}
221
+ </p>
222
+ {parameters}
223
+ </CardContent>
224
+ </Card>
225
+ )}
226
+ </div>
227
+ </div>
228
+ </div>
229
+ );
230
+ }
@@ -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
+ }
@@ -8,6 +8,28 @@ import inboxEnUS from '#extensions/nocobase-inbox/locales/en-US';
8
8
  * upgrade replaces this file, so put the application's own copy in `../en-US.ts` rather than here.
9
9
  */
10
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
+ },
11
33
  settings,
12
34
  'auth.preparingSession': 'Preparing your session…',
13
35
  'auth.connectionFailed': 'Unable to connect',
@@ -1,4 +1,5 @@
1
1
  const settings = {
2
+ unnamedPage: 'Unnamed page ({{path}})',
2
3
  revokeRoles: 'Revoke roles',
3
4
  keyConflict: 'This role key is already in use.',
4
5
  userPermissions: 'User management',
@@ -1,4 +1,5 @@
1
1
  const settings = {
2
+ unnamedPage: '未命名页面({{path}})',
2
3
  revokeRoles: '撤销角色',
3
4
  keyConflict: '此角色标识已被使用。',
4
5
  userPermissions: '用户管理',
@@ -5,6 +5,28 @@ import inboxZhCN from '#extensions/nocobase-inbox/locales/zh-CN';
5
5
 
6
6
  /** The Chinese copy of what the template ships; see `./en-US.ts`. */
7
7
  const systemZhCN: SystemResource = {
8
+ flowDiagram: {
9
+ configure: '配置',
10
+ overview: '流程缩略导航',
11
+ actualSize: '实际大小',
12
+ states: '状态',
13
+ transitions: '转移',
14
+ configurable: '可配置参数',
15
+ current: '当前',
16
+ visited: '已经过',
17
+ initial: '初始状态',
18
+ final: '终止状态',
19
+ state: '状态',
20
+ zoomOut: '缩小',
21
+ zoomIn: '放大',
22
+ fit: '适应画布',
23
+
24
+ empty: '暂无可展示的状态。',
25
+ loading: '正在布局流程图…',
26
+ failed: '无法布局此流程图。',
27
+ nodeHelp: '按 Enter 或空格键选择状态。',
28
+ edgeHelp: '按 Enter 或空格键选择转移。',
29
+ },
8
30
  settings,
9
31
  'auth.preparingSession': '正在准备登录会话…',
10
32
  'auth.connectionFailed': '连接失败',
@@ -0,0 +1,7 @@
1
+ import type { PagePermissionTitles } from './page-permissions.js';
2
+
3
+ /** Permission labels for application pages without navigation/breadcrumb titles (including overlays).
4
+ * Keys are existing route names; values are application translation keys.
5
+ * Do not add navigation/breadcrumbs or change authorization ids just to name a permission.
6
+ */
7
+ export const pagePermissionTitles: PagePermissionTitles = {};
@@ -0,0 +1,42 @@
1
+ import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
2
+ import type { LocalizedText } from '@nocobase/app-plugin-authorization/client';
3
+ import type { CatalogResource } from './permissions-model.js';
4
+
5
+ /** Application route names are stable within their owner; labels never change stored grants. */
6
+ export type PagePermissionTitles = Readonly<Record<string, string>>;
7
+
8
+ export function pagePermissionTitle(
9
+ route: AppClientRegisteredRoute,
10
+ titles: PagePermissionTitles,
11
+ ): LocalizedText | undefined {
12
+ const key =
13
+ (route.source === 'application' && Object.hasOwn(titles, route.name)
14
+ ? titles[route.name]
15
+ : undefined) ??
16
+ route.navigation?.title ??
17
+ route.breadcrumb?.title;
18
+ return key?.trim() ? { key, ns: route.packageName } : undefined;
19
+ }
20
+
21
+ /** Preserve every existing resource id/action; never use a technical route id as its label. */
22
+ export function pagePermissionResources(
23
+ routes: readonly AppClientRegisteredRoute[],
24
+ titles: PagePermissionTitles,
25
+ unnamed: (path: string) => string,
26
+ ): CatalogResource[] {
27
+ return routes.flatMap((route) => [
28
+ ...(route.componentLoader &&
29
+ typeof route.authz === 'object' &&
30
+ route.authz.resource.type === 'page'
31
+ ? [
32
+ {
33
+ type: 'page',
34
+ id: route.authz.resource.id,
35
+ title: pagePermissionTitle(route, titles) ?? unnamed(route.path),
36
+ actions: [{ name: 'access', title: { key: 'settings.access' } }],
37
+ },
38
+ ]
39
+ : []),
40
+ ...pagePermissionResources(route.children ?? [], titles, unnamed),
41
+ ]);
42
+ }
@@ -16,7 +16,8 @@ import {
16
16
  type PermissionGrant,
17
17
  type PermissionSet,
18
18
  } from '@nocobase/app-plugin-authorization/client';
19
- import type { AppClientRegisteredRoute } from '@nocobase/app-client/plugins';
19
+ import { pagePermissionResources } from './page-permissions.js';
20
+ import { pagePermissionTitles } from './page-permission-titles.js';
20
21
  import { useEffect, useMemo, useRef, useState, type ReactElement } from 'react';
21
22
  import {
22
23
  useLocation,
@@ -40,7 +41,6 @@ import {
40
41
  unconfiguredFilter,
41
42
  setPermissions,
42
43
  setScope,
43
- type CatalogResource,
44
44
  } from './permissions-model.js';
45
45
 
46
46
  function RoleEditor(): ReactElement {
@@ -100,32 +100,11 @@ function RoleEditor(): ReactElement {
100
100
  const catalog = useMemo(() => {
101
101
  if (!options.data) return undefined;
102
102
  const source = options.data.data;
103
- const pages: CatalogResource[] = [];
104
- const collect = (routes: readonly AppClientRegisteredRoute[]) => {
105
- for (const route of routes) {
106
- const pageTitle = route.navigation?.title ?? route.breadcrumb?.title;
107
- if (
108
- route.componentLoader &&
109
- typeof route.authz === 'object' &&
110
- route.authz.resource.type === 'page'
111
- )
112
- pages.push({
113
- type: 'page',
114
- id: route.authz.resource.id,
115
- title: pageTitle
116
- ? { key: pageTitle, ns: route.packageName }
117
- : route.id,
118
- actions: [
119
- {
120
- name: 'access',
121
- title: { key: 'settings.access' },
122
- },
123
- ],
124
- });
125
- collect(route.children ?? []);
126
- }
127
- };
128
- collect(app.runtime.routes);
103
+ const pages = pagePermissionResources(
104
+ app.runtime.routes,
105
+ pagePermissionTitles,
106
+ (path) => t('settings.unnamedPage', { path }),
107
+ );
129
108
  return {
130
109
  ...source,
131
110
  sections: [
@@ -180,7 +159,7 @@ function RoleEditor(): ReactElement {
180
159
  })),
181
160
  ],
182
161
  };
183
- }, [options.data, app]);
162
+ }, [options.data, app, t]);
184
163
  const allowed = creating
185
164
  ? canCreate
186
165
  : canUpdate &&