@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.
- package/.agents/rules/components.md +4 -0
- package/AGENTS.md +3 -1
- package/client/components/flow-diagram-css.d.ts +2 -0
- package/client/components/flow-diagram.tsx +674 -0
- package/client/components/flow-layout.ts +250 -0
- package/client/components/lifecycle-diagram-model.ts +217 -0
- package/client/components/lifecycle-diagram.tsx +310 -0
- package/client/extensions/nocobase-workflow-management/README.md +70 -0
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +148 -0
- package/client/extensions/nocobase-workflow-management/execution-detail.tsx +230 -0
- package/client/extensions/nocobase-workflow-management/execution-list.tsx +175 -0
- package/client/extensions/nocobase-workflow-management/layout.tsx +95 -0
- package/client/locales/system/en-US.ts +22 -0
- package/client/locales/system/settings-en-US.ts +1 -0
- package/client/locales/system/settings-zh-CN.ts +1 -0
- package/client/locales/system/zh-CN.ts +22 -0
- package/client/pages/settings/page-permission-titles.ts +7 -0
- package/client/pages/settings/page-permissions.ts +42 -0
- package/client/pages/settings/role-form.tsx +8 -29
- package/package.json +13 -11
- package/tests/logic/page-permission-titles.test.ts +223 -0
|
@@ -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',
|
|
@@ -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
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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 &&
|