@nocobase/app-template-default 1.0.0-beta.88 → 1.0.0-beta.89
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 +3 -1
- package/.agents/rules/shell.md +8 -2
- package/AGENTS.md +1 -1
- package/client/components/back-button.tsx +37 -12
- package/client/components/brand-loading-mark.tsx +5 -2
- package/client/components/brand-mark-geometry.ts +3 -3
- package/client/components/breadcrumbs.tsx +47 -3
- package/client/components/data-table/data-table.tsx +90 -19
- package/client/components/data-table/pagination.tsx +17 -2
- package/client/components/list-feedback.tsx +31 -22
- package/client/components/loading.tsx +127 -16
- package/client/components/pending-button.tsx +111 -0
- package/client/components/role-list.tsx +7 -0
- package/client/components/route-child-page.tsx +72 -1
- package/client/components/route-overlay.tsx +149 -15
- package/client/components/task-progress.tsx +155 -0
- package/client/components/use-route-overlay.ts +8 -11
- package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +62 -32
- package/client/extensions/nocobase-file-component-ui/locales/de-DE.ts +2 -0
- package/client/extensions/nocobase-file-component-ui/locales/en-US.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/es-ES.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/fr-FR.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/id-ID.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/ja-JP.ts +2 -0
- package/client/extensions/nocobase-file-component-ui/locales/pt-BR.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/vi-VN.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/zh-CN.ts +1 -0
- package/client/extensions/nocobase-file-component-ui/locales/zh-TW.ts +1 -0
- package/client/extensions/nocobase-workflow-management/README.md +5 -5
- package/client/extensions/nocobase-workflow-management/definition-list.tsx +2 -9
- package/client/extensions/nocobase-workflow-management/execution-list.tsx +2 -4
- package/client/layouts/app-layout.tsx +0 -4
- package/client/locales/system/en-US.ts +19 -0
- package/client/locales/system/zh-CN.ts +19 -0
- package/client/pages/settings/default-access.tsx +1 -1
- package/client/pages/settings/roles.tsx +27 -27
- package/client/pages/settings/routes.ts +15 -7
- package/client/pages/settings/user-detail.tsx +3 -17
- package/client/pages/settings/user-form.tsx +9 -15
- package/client/pages/settings/users.tsx +11 -27
- package/client/routing/client-route.tsx +199 -37
- package/client/routing/navigation-destination.ts +42 -0
- package/client/routing/route-leave-boundary.tsx +13 -1
- package/client/styles.css +18 -1
- package/package.json +8 -8
- package/tests/client/auth-i18n.test.tsx +1 -1
- package/tests/client/authenticated-client-fixture.test.tsx +37 -0
- package/tests/client/back-button.test.tsx +34 -0
- package/tests/client/client-route-recovery.test.tsx +93 -1
- package/tests/client/list-feedback.test.tsx +5 -5
- package/tests/client/list-loading.test.tsx +397 -0
- package/tests/client/loading.test.tsx +20 -11
- package/tests/client/manual-list-retry.test.tsx +2 -2
- package/tests/client/navigation-parent.test.tsx +172 -0
- package/tests/client/pending-button.test.tsx +106 -0
- package/tests/client/permission-management-tabs.test.tsx +48 -12
- package/tests/client/primitive-labels.test.tsx +2 -2
- package/tests/client/role-return.test.tsx +21 -3
- package/tests/client/roles.test.tsx +84 -3
- package/tests/client/route-leave-recheck.test.tsx +74 -0
- package/tests/client/route-presentation.test.tsx +557 -0
- package/tests/client/session-preparation.test.tsx +20 -17
- package/tests/client/settings-continuity.test.tsx +35 -11
- package/tests/client/settings-home.test.tsx +4 -2
- package/tests/client/settings-idle-list.test.tsx +124 -0
- package/tests/client/user-drawer.test.tsx +3 -4
- package/tests/fixtures/dialog-footer.html +12 -0
- package/tests/fixtures/dialog-footer.tsx +215 -0
- package/tests/helpers/authenticated-client.tsx +70 -22
- package/tests/playwright/dialog-footer.test.ts +227 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Progress } from '@base-ui/react/progress';
|
|
2
|
+
import { useTranslation } from '@nocobase/i18n/client';
|
|
3
|
+
import type { ReactElement, ReactNode } from 'react';
|
|
4
|
+
import { cn } from 'cn';
|
|
5
|
+
|
|
6
|
+
export type TaskProgressStatus =
|
|
7
|
+
'queued' | 'running' | 'completed' | 'failed' | 'canceled';
|
|
8
|
+
|
|
9
|
+
export interface TaskProgressProps {
|
|
10
|
+
/** The task's real lifecycle status, from the server's task record. */
|
|
11
|
+
readonly status: TaskProgressStatus;
|
|
12
|
+
/** What the task is, such as "Customer export". */
|
|
13
|
+
readonly label: string;
|
|
14
|
+
/** The application's own word for the status; defaults to the standard copy. */
|
|
15
|
+
readonly statusLabel?: string;
|
|
16
|
+
/** Units actually processed, as reported by the task. */
|
|
17
|
+
readonly processed?: number;
|
|
18
|
+
/** The real total; leave it out or pass null while it is unknown. */
|
|
19
|
+
readonly total?: number | null;
|
|
20
|
+
/** For tasks that report only a percentage (0–100) instead of counts. */
|
|
21
|
+
readonly percent?: number | null;
|
|
22
|
+
/** Show that the task keeps running after the user leaves the page. */
|
|
23
|
+
readonly continuesInBackground?: boolean;
|
|
24
|
+
/** Caller-owned detail: a failure reason, or where the result is. */
|
|
25
|
+
readonly message?: ReactNode;
|
|
26
|
+
/** Real actions only: download, retry, or a cancel the server supports. */
|
|
27
|
+
readonly actions?: ReactNode;
|
|
28
|
+
readonly className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One long-running task record. A known total shows a determinate bar and
|
|
33
|
+
* count; an unknown total shows an indeterminate bar without a percentage.
|
|
34
|
+
* Status changes are announced once; percentage updates are not.
|
|
35
|
+
*/
|
|
36
|
+
export function TaskProgress({
|
|
37
|
+
status,
|
|
38
|
+
label,
|
|
39
|
+
statusLabel,
|
|
40
|
+
processed,
|
|
41
|
+
total,
|
|
42
|
+
percent: reportedPercent,
|
|
43
|
+
continuesInBackground = false,
|
|
44
|
+
message,
|
|
45
|
+
actions,
|
|
46
|
+
className,
|
|
47
|
+
}: TaskProgressProps): ReactElement {
|
|
48
|
+
const { t } = useTranslation();
|
|
49
|
+
const active = status === 'queued' || status === 'running';
|
|
50
|
+
const counted = total !== undefined && total !== null && total > 0;
|
|
51
|
+
const done = processed ?? 0;
|
|
52
|
+
const known =
|
|
53
|
+
counted || (reportedPercent !== undefined && reportedPercent !== null);
|
|
54
|
+
const percent = counted
|
|
55
|
+
? Math.min(100, Math.floor((done / total) * 100))
|
|
56
|
+
: Math.min(100, Math.max(0, Math.floor(reportedPercent ?? 0)));
|
|
57
|
+
const statusText =
|
|
58
|
+
statusLabel ??
|
|
59
|
+
{
|
|
60
|
+
queued: t('taskProgress.queued', { defaultValue: 'Waiting to start' }),
|
|
61
|
+
running: t('taskProgress.running', { defaultValue: 'In progress' }),
|
|
62
|
+
completed: t('taskProgress.completed', { defaultValue: 'Completed' }),
|
|
63
|
+
failed: t('taskProgress.failed', { defaultValue: 'Failed' }),
|
|
64
|
+
canceled: t('taskProgress.canceled', { defaultValue: 'Canceled' }),
|
|
65
|
+
}[status];
|
|
66
|
+
const countText = counted
|
|
67
|
+
? t('taskProgress.count', {
|
|
68
|
+
defaultValue: '{{processed}} of {{total}}',
|
|
69
|
+
processed: done,
|
|
70
|
+
total,
|
|
71
|
+
})
|
|
72
|
+
: processed
|
|
73
|
+
? t('taskProgress.processed', {
|
|
74
|
+
defaultValue: '{{processed}} processed',
|
|
75
|
+
processed: done,
|
|
76
|
+
})
|
|
77
|
+
: undefined;
|
|
78
|
+
return (
|
|
79
|
+
<div
|
|
80
|
+
data-slot='task-progress'
|
|
81
|
+
data-status={status}
|
|
82
|
+
className={cn('flex flex-col gap-2', className)}
|
|
83
|
+
>
|
|
84
|
+
<div className='flex flex-wrap items-baseline justify-between gap-x-3 gap-y-1'>
|
|
85
|
+
<span className='min-w-0 text-sm font-medium wrap-anywhere'>
|
|
86
|
+
{label}
|
|
87
|
+
</span>
|
|
88
|
+
<span
|
|
89
|
+
className={cn(
|
|
90
|
+
'text-sm',
|
|
91
|
+
status === 'failed' ? 'text-destructive' : 'text-muted-foreground',
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
{statusText}
|
|
95
|
+
</span>
|
|
96
|
+
</div>
|
|
97
|
+
{active ? (
|
|
98
|
+
<Progress.Root
|
|
99
|
+
value={known ? percent : null}
|
|
100
|
+
aria-label={label}
|
|
101
|
+
getAriaValueText={() =>
|
|
102
|
+
[statusText, countText, known ? `${percent}%` : undefined]
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.join(', ')
|
|
105
|
+
}
|
|
106
|
+
className='flex flex-col gap-1.5'
|
|
107
|
+
>
|
|
108
|
+
<Progress.Track className='relative h-1.5 w-full overflow-hidden rounded-full bg-muted'>
|
|
109
|
+
{/* An unknown amount fills the whole track faintly, so no segment can be read as a fraction done. */}
|
|
110
|
+
<Progress.Indicator
|
|
111
|
+
data-slot='task-progress-indicator'
|
|
112
|
+
className={
|
|
113
|
+
known
|
|
114
|
+
? 'h-full bg-primary motion-safe:transition-[width] motion-safe:duration-500'
|
|
115
|
+
: 'h-full w-full bg-primary/25 motion-safe:animate-pulse'
|
|
116
|
+
}
|
|
117
|
+
/>
|
|
118
|
+
</Progress.Track>
|
|
119
|
+
{countText || known ? (
|
|
120
|
+
<span className='text-xs text-muted-foreground tabular-nums'>
|
|
121
|
+
{[countText, known ? `${percent}%` : undefined]
|
|
122
|
+
.filter(Boolean)
|
|
123
|
+
.join(' · ')}
|
|
124
|
+
</span>
|
|
125
|
+
) : null}
|
|
126
|
+
</Progress.Root>
|
|
127
|
+
) : countText && status !== 'completed' ? (
|
|
128
|
+
<span className='text-xs text-muted-foreground tabular-nums'>
|
|
129
|
+
{countText}
|
|
130
|
+
</span>
|
|
131
|
+
) : null}
|
|
132
|
+
{active && continuesInBackground ? (
|
|
133
|
+
<p className='text-xs text-muted-foreground'>
|
|
134
|
+
{t('taskProgress.background', {
|
|
135
|
+
defaultValue: 'It keeps running if you leave this page.',
|
|
136
|
+
})}
|
|
137
|
+
</p>
|
|
138
|
+
) : null}
|
|
139
|
+
{message ? (
|
|
140
|
+
<div
|
|
141
|
+
className={cn(
|
|
142
|
+
'text-sm',
|
|
143
|
+
status === 'failed' ? 'text-destructive' : 'text-muted-foreground',
|
|
144
|
+
)}
|
|
145
|
+
>
|
|
146
|
+
{message}
|
|
147
|
+
</div>
|
|
148
|
+
) : null}
|
|
149
|
+
{actions ? <div className='flex flex-wrap gap-2'>{actions}</div> : null}
|
|
150
|
+
<span role='status' aria-live='polite' className='sr-only'>
|
|
151
|
+
{`${label}: ${statusText}`}
|
|
152
|
+
</span>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
RouteOverlayContext,
|
|
3
|
+
type RouteOverlayContextValue,
|
|
4
|
+
} from '@nocobase/app-client/route-containers';
|
|
5
|
+
import { useContext } from 'react';
|
|
2
6
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
/** True while a close request for the current location is pending. */
|
|
7
|
-
readonly isClosing: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/** Internal context shared by both route overlay components. */
|
|
11
|
-
export const RouteOverlayContext: Context<RouteOverlayContextValue | null> =
|
|
12
|
-
createContext<RouteOverlayContextValue | null>(null);
|
|
7
|
+
// The context lives in `@nocobase/app-client` so every copy of the route overlay components, including a plugin's,
|
|
8
|
+
// shares it: a footer button inside a panel the application opened reaches that panel's close request.
|
|
9
|
+
export { RouteOverlayContext, type RouteOverlayContextValue };
|
|
13
10
|
|
|
14
11
|
/**
|
|
15
12
|
* Closes the enclosing `RouteDialog` or `RouteDrawer`. Call it from a component rendered inside the overlay — its
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { messageKey } from '../lib/message-key.js';
|
|
2
|
+
import { usePendingFeedback } from '@nocobase/app-client';
|
|
2
3
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
3
4
|
import { RotateCcw, Trash2, UploadCloud, X } from 'lucide-react';
|
|
4
5
|
import {
|
|
@@ -8,11 +9,12 @@ import {
|
|
|
8
9
|
type ChangeEvent,
|
|
9
10
|
type DragEvent,
|
|
10
11
|
type ReactElement,
|
|
12
|
+
type ReactNode,
|
|
11
13
|
} from 'react';
|
|
12
14
|
|
|
13
15
|
import type { FileRecord, FileUploadFieldProps } from '../types.js';
|
|
14
16
|
import { Button } from '#components/ui/button';
|
|
15
|
-
import {
|
|
17
|
+
import { BrandLoadingMark } from '#components/brand-loading-mark';
|
|
16
18
|
import { FileThumbnail } from './file-thumbnail.js';
|
|
17
19
|
|
|
18
20
|
type UploadItem = {
|
|
@@ -23,6 +25,48 @@ type UploadItem = {
|
|
|
23
25
|
readonly error?: Error;
|
|
24
26
|
};
|
|
25
27
|
|
|
28
|
+
/**
|
|
29
|
+
* The upload request reports no byte progress, so the item shows an honest
|
|
30
|
+
* indeterminate wait from one display hook: the mark appears after the shared
|
|
31
|
+
* display delay and a long upload says so after 10 seconds, next to the real
|
|
32
|
+
* cancel action.
|
|
33
|
+
*/
|
|
34
|
+
function UploadItemBody({
|
|
35
|
+
status,
|
|
36
|
+
failure,
|
|
37
|
+
children,
|
|
38
|
+
}: {
|
|
39
|
+
readonly status: UploadItem['status'];
|
|
40
|
+
readonly failure?: string;
|
|
41
|
+
readonly children: ReactNode;
|
|
42
|
+
}): ReactElement {
|
|
43
|
+
const { t } = useTranslation();
|
|
44
|
+
const feedback = usePendingFeedback(status === 'uploading');
|
|
45
|
+
return (
|
|
46
|
+
<>
|
|
47
|
+
<div className='flex h-20 items-center justify-center overflow-hidden rounded-sm bg-muted/30'>
|
|
48
|
+
{feedback.visible ? (
|
|
49
|
+
<BrandLoadingMark className='size-8 text-muted-foreground' />
|
|
50
|
+
) : (
|
|
51
|
+
<UploadCloud aria-hidden='true' />
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
{children}
|
|
55
|
+
<div className='text-xs text-muted-foreground'>
|
|
56
|
+
{status === 'error'
|
|
57
|
+
? (failure ?? t('fileUi.files.failed', { defaultValue: 'Failed' }))
|
|
58
|
+
: status === 'uploading'
|
|
59
|
+
? feedback.longWaiting
|
|
60
|
+
? t('fileUi.files.longWait', {
|
|
61
|
+
defaultValue: 'Still uploading. Keep waiting or cancel.',
|
|
62
|
+
})
|
|
63
|
+
: t('fileUi.files.uploading', { defaultValue: 'Uploading' })
|
|
64
|
+
: t('fileUi.files.pending', { defaultValue: 'Pending' })}
|
|
65
|
+
</div>
|
|
66
|
+
</>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
26
70
|
function accepts(file: File, rules: readonly string[]): boolean {
|
|
27
71
|
if (!rules.length) return true;
|
|
28
72
|
const name = file.name.toLowerCase();
|
|
@@ -249,12 +293,10 @@ export function FileUploadField(
|
|
|
249
293
|
};
|
|
250
294
|
|
|
251
295
|
return (
|
|
252
|
-
<div
|
|
253
|
-
|
|
254
|
-
className='space-y-3'
|
|
255
|
-
aria-busy={items.some((item) => item.status === 'uploading')}
|
|
256
|
-
>
|
|
296
|
+
<div data-slot='file-upload-field' className='space-y-3'>
|
|
297
|
+
{/* The polite summary below stays outside the busy drop zone. */}
|
|
257
298
|
<div
|
|
299
|
+
aria-busy={items.some((item) => item.status === 'uploading')}
|
|
258
300
|
className={`flex min-h-24 flex-wrap gap-3 rounded-md border border-dashed p-3 ${dragging ? 'border-primary bg-primary/5' : ''}`}
|
|
259
301
|
onDragOver={(event) => {
|
|
260
302
|
event.preventDefault();
|
|
@@ -288,32 +330,20 @@ export function FileUploadField(
|
|
|
288
330
|
))}
|
|
289
331
|
{items.map((item) => (
|
|
290
332
|
<div key={item.key} className='w-36 rounded-md border p-2'>
|
|
291
|
-
<
|
|
292
|
-
{item.status
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
</
|
|
305
|
-
<div className='text-xs text-muted-foreground'>
|
|
306
|
-
{item.status === 'error'
|
|
307
|
-
? ((item.error
|
|
308
|
-
? t(messageKey(item.error.message), {
|
|
309
|
-
defaultValue: item.error.message,
|
|
310
|
-
})
|
|
311
|
-
: undefined) ??
|
|
312
|
-
t('fileUi.files.failed', { defaultValue: 'Failed' }))
|
|
313
|
-
: item.status === 'uploading'
|
|
314
|
-
? t('fileUi.files.uploading', { defaultValue: 'Uploading' })
|
|
315
|
-
: t('fileUi.files.pending', { defaultValue: 'Pending' })}
|
|
316
|
-
</div>
|
|
333
|
+
<UploadItemBody
|
|
334
|
+
status={item.status}
|
|
335
|
+
failure={
|
|
336
|
+
item.status === 'error' && item.error
|
|
337
|
+
? t(messageKey(item.error.message), {
|
|
338
|
+
defaultValue: item.error.message,
|
|
339
|
+
})
|
|
340
|
+
: undefined
|
|
341
|
+
}
|
|
342
|
+
>
|
|
343
|
+
<div className='mt-2 truncate text-sm' title={item.file.name}>
|
|
344
|
+
{item.file.name}
|
|
345
|
+
</div>
|
|
346
|
+
</UploadItemBody>
|
|
317
347
|
<div className='mt-2 flex gap-1'>
|
|
318
348
|
{item.status === 'error' ? (
|
|
319
349
|
<Button
|
|
@@ -26,6 +26,8 @@ const messages = {
|
|
|
26
26
|
'files.retry': 'Erneut versuchen',
|
|
27
27
|
'files.done': 'Erledigt',
|
|
28
28
|
'files.uploading': 'Hochladen',
|
|
29
|
+
'files.longWait':
|
|
30
|
+
'Wird noch hochgeladen. Warten Sie weiter oder brechen Sie ab.',
|
|
29
31
|
'files.failed': 'Fehlgeschlagen',
|
|
30
32
|
'files.pending': 'Ausstehend',
|
|
31
33
|
'files.previous': 'Vorherige Datei',
|
|
@@ -24,6 +24,7 @@ const messages = {
|
|
|
24
24
|
'files.retry': 'Retry',
|
|
25
25
|
'files.done': 'Done',
|
|
26
26
|
'files.uploading': 'Uploading',
|
|
27
|
+
'files.longWait': 'Still uploading. Keep waiting or cancel.',
|
|
27
28
|
'files.failed': 'Failed',
|
|
28
29
|
'files.pending': 'Pending',
|
|
29
30
|
'files.previous': 'Previous file',
|
|
@@ -25,6 +25,7 @@ const messages = {
|
|
|
25
25
|
'files.retry': 'Reintentar',
|
|
26
26
|
'files.done': 'Hecho',
|
|
27
27
|
'files.uploading': 'Subiendo',
|
|
28
|
+
'files.longWait': 'Aún se está subiendo. Siga esperando o cancele.',
|
|
28
29
|
'files.failed': 'Falló',
|
|
29
30
|
'files.pending': 'Pendiente',
|
|
30
31
|
'files.previous': 'Archivo anterior',
|
|
@@ -26,6 +26,7 @@ const messages = {
|
|
|
26
26
|
'files.retry': 'Réessayer',
|
|
27
27
|
'files.done': 'Terminé',
|
|
28
28
|
'files.uploading': 'Téléchargement',
|
|
29
|
+
'files.longWait': 'Envoi toujours en cours. Patientez ou annulez.',
|
|
29
30
|
'files.failed': 'Échec',
|
|
30
31
|
'files.pending': 'En attente',
|
|
31
32
|
'files.previous': 'Fichier précédent',
|
|
@@ -24,6 +24,7 @@ const messages = {
|
|
|
24
24
|
'files.retry': 'Coba lagi',
|
|
25
25
|
'files.done': 'Selesai',
|
|
26
26
|
'files.uploading': 'Mengunggah',
|
|
27
|
+
'files.longWait': 'Masih mengunggah. Tunggu atau batalkan.',
|
|
27
28
|
'files.failed': 'Gagal',
|
|
28
29
|
'files.pending': 'Tertunda',
|
|
29
30
|
'files.previous': 'Berkas sebelumnya',
|
|
@@ -26,6 +26,7 @@ const messages = {
|
|
|
26
26
|
'files.retry': 'Tentar novamente',
|
|
27
27
|
'files.done': 'Concluído',
|
|
28
28
|
'files.uploading': 'Carregando',
|
|
29
|
+
'files.longWait': 'Ainda enviando. Continue aguardando ou cancele.',
|
|
29
30
|
'files.failed': 'Falha',
|
|
30
31
|
'files.pending': 'Pendente',
|
|
31
32
|
'files.previous': 'Arquivo anterior',
|
|
@@ -25,6 +25,7 @@ const messages = {
|
|
|
25
25
|
'files.retry': 'Thử lại',
|
|
26
26
|
'files.done': 'Hoàn thành',
|
|
27
27
|
'files.uploading': 'Đang tải lên',
|
|
28
|
+
'files.longWait': 'Vẫn đang tải lên. Hãy tiếp tục chờ hoặc hủy.',
|
|
28
29
|
'files.failed': 'Thất bại',
|
|
29
30
|
'files.pending': 'Đang chờ',
|
|
30
31
|
'files.previous': 'Tệp trước đó',
|
|
@@ -5,8 +5,8 @@ Install with `yes n | pnpm exec shadcn add @nocobase/workflow-management`. This
|
|
|
5
5
|
| File | Exports and responsibility |
|
|
6
6
|
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
7
7
|
| `layout.tsx` | `WorkflowManagementLayout`: page container, shared header and line-style tabs, hidden in detail mode. `WorkflowDefinitionDetail`: diagram and parameter form cards in two columns. |
|
|
8
|
-
| `definition-list.tsx` | `WorkflowDefinitionList`: optional category rail, search slot
|
|
9
|
-
| `execution-list.tsx` | `WorkflowExecutionList`: filters slot,
|
|
8
|
+
| `definition-list.tsx` | `WorkflowDefinitionList`: optional category rail, search slot and execution-activity sort controls, adapted definition rows and empty/status content. |
|
|
9
|
+
| `execution-list.tsx` | `WorkflowExecutionList`: filters slot, controlled ordering, adapted execution rows, empty/status content and controlled pagination. |
|
|
10
10
|
| `execution-detail.tsx` | `WorkflowExecutionDetail`: ordered history, step selection, diagram rendering callback, effects and optional recorded-parameter content. |
|
|
11
11
|
|
|
12
12
|
## Routes and ownership
|
|
@@ -53,11 +53,11 @@ const routes = defineAppRoutes([
|
|
|
53
53
|
|
|
54
54
|
Add definition and execution detail routes beneath the corresponding tab route, also without `navigation`. The parent page maps the URL to `tab`, navigates in `onTabChange`, and supplies its `Outlet` as children. Reuse an existing system group instead of registering another one.
|
|
55
55
|
|
|
56
|
-
Keep requests, query parameters, IME-aware search, filtering, pagination, category CRUD and permissions in application adapters. Map rows into display values and router links before passing them to the lists. `loaded` distinguishes an empty response from data that has not arrived; `loading`
|
|
56
|
+
Keep requests, query parameters, IME-aware search, filtering, pagination, category CRUD and permissions in application adapters. Map rows into display values and router links before passing them to the lists. `loaded` distinguishes an empty response from data that has not arrived; `loading` supplies pending feedback, and the caller disables pagination while pending. Supply error/loading/retry content through `status`, and separate empty-catalog and no-results wording through `empty`. The search slot stays compact on wider screens; the category rail stacks above the list on small screens. Omit `categories` when classification is unnecessary. The installable demo provides in-memory category filtering, named color selection and add/edit/delete dialogs as an application adapter; deleting a category preserves its workflows and clears the selected category filter. Its definition header also demonstrates a colored category dropdown with an Uncategorized option; assignment uses the same category state as list filtering. The production adapter keeps the assignment API and read/update permission checks in the application.
|
|
57
57
|
|
|
58
58
|
`WorkflowDefinitionDetail` accepts a diagram slot and a parameter form slot. Render the installed `@nocobase/lifecycle-diagram` or an existing compatible diagram in a definite-height or content-sized canvas. Keep schema validation, effective timing, complete-group saves, CAS conflicts and read-only permissions in the application's parameter editor. Show a translated empty state when a definition declares no adjustable parameters.
|
|
59
59
|
|
|
60
|
-
`WorkflowExecutionDetail` sorts transitions by version, timestamp and id without mutating the input. Selecting a step passes only that history prefix and its destination state to `renderDiagram`; no transition or server action occurs. Pass the runtime's transition records directly, retaining extra fields through its generic history type. `effects` displays persisted statuses, attempts and failures; it does not retry them. Pass recorded parameter content through `parameters` and label historical facts separately from current configuration. Remount the detail on record changes
|
|
60
|
+
`WorkflowExecutionDetail` sorts transitions by version, timestamp and id without mutating the input. Selecting a step passes only that history prefix and its destination state to `renderDiagram`; no transition or server action occurs. Pass the runtime's transition records directly, retaining extra fields through its generic history type. `effects` displays persisted statuses, attempts and failures; it does not retry them. Pass recorded parameter content through `parameters` and label historical facts separately from current configuration. Remount the detail on record identity changes; preserve a selected historical step during automatic updates.
|
|
61
61
|
|
|
62
62
|
The application owns authentication, initial administrator grants, navigation and direct-page guards, server read/update authorization and any recovery actions. Configuration permission does not grant business approval permission. Installing this block creates source files, not routes, permissions, runtime services or database collections.
|
|
63
63
|
|
|
@@ -71,4 +71,4 @@ Omit `WorkflowDefinitionDetail.parameters` when the editor opens contextually so
|
|
|
71
71
|
|
|
72
72
|
Ordering is optional: omit `onSort` when activity ordering is not supported so the header stays plain text. Omit a row's optional `name` when its linked `title` already supplies the complete display name. Hosts own filtering, pagination and their URL state; keep that state when opening and returning from a definition or execution.
|
|
73
73
|
|
|
74
|
-
List hosts pass `listState={resolveListState(request, rows.length, hasEffectiveFilters)}` and `onClearFilters`, using the current request error and data. The DataTable dependency supplies shared loading, denial, session and failure copy; merge the `listFeedback` English/Chinese keys from the components README into application translations. `onRefresh`
|
|
74
|
+
List hosts pass `listState={resolveListState(request, rows.length, hasEffectiveFilters)}` and `onClearFilters`, using the current request error and data. The DataTable dependency supplies shared loading, denial, session and failure copy; merge the `listFeedback` English/Chinese keys from the components README into application translations. `onRefresh` handles contextual failed-read retry and must preserve the current query; ready lists render no Refresh action. The host owns subscription/snapshot updates for open progress views. Keep `loaded`, `loading`, custom `empty` content and existing permission checks when reconciling installed copies. A successful empty filter is `no-results`; an empty unfiltered read is `empty`. Hosts that retain the legacy status slot without a listState still own their error rendering.
|
|
@@ -27,7 +27,8 @@ export interface WorkflowDefinitionListProps {
|
|
|
27
27
|
loading: boolean;
|
|
28
28
|
empty?: ReactNode;
|
|
29
29
|
labels: {
|
|
30
|
-
|
|
30
|
+
/** Legacy adapter label; ready lists no longer render a Refresh control. */
|
|
31
|
+
refresh?: string;
|
|
31
32
|
workflow: string;
|
|
32
33
|
executionCount: string;
|
|
33
34
|
executionActivity: string;
|
|
@@ -66,14 +67,6 @@ export function WorkflowDefinitionList({
|
|
|
66
67
|
<div className='flex w-full min-w-0 flex-1 flex-col gap-4'>
|
|
67
68
|
<div className='flex flex-col items-stretch justify-between gap-2 sm:flex-row sm:items-center'>
|
|
68
69
|
<div className='min-w-0 flex-1 sm:max-w-xs'>{search}</div>
|
|
69
|
-
<Button
|
|
70
|
-
className='shrink-0 self-start sm:self-auto'
|
|
71
|
-
variant='outline'
|
|
72
|
-
disabled={loading}
|
|
73
|
-
onClick={onRefresh}
|
|
74
|
-
>
|
|
75
|
-
{labels.refresh}
|
|
76
|
-
</Button>
|
|
77
70
|
</div>
|
|
78
71
|
{status}
|
|
79
72
|
{(listState !== undefined || loaded || loading) && (
|
|
@@ -27,7 +27,8 @@ export interface WorkflowExecutionListProps {
|
|
|
27
27
|
status?: ReactNode;
|
|
28
28
|
empty?: ReactNode;
|
|
29
29
|
labels: {
|
|
30
|
-
|
|
30
|
+
/** Legacy adapter label; ready lists no longer render a Refresh control. */
|
|
31
|
+
refresh?: string;
|
|
31
32
|
record: string;
|
|
32
33
|
workflow: string;
|
|
33
34
|
state: string;
|
|
@@ -72,9 +73,6 @@ export function WorkflowExecutionList({
|
|
|
72
73
|
<section className='flex flex-col gap-4'>
|
|
73
74
|
<div className='flex flex-wrap items-center justify-between gap-3'>
|
|
74
75
|
<div className='flex flex-wrap items-center gap-2'>{filters}</div>
|
|
75
|
-
<Button variant='outline' disabled={loading} onClick={onRefresh}>
|
|
76
|
-
{labels.refresh}
|
|
77
|
-
</Button>
|
|
78
76
|
</div>
|
|
79
77
|
{status}
|
|
80
78
|
{(listState !== undefined || loaded || loading) && (
|
|
@@ -18,7 +18,6 @@ import {
|
|
|
18
18
|
AppSidebarToggle,
|
|
19
19
|
} from './components/app-sidebar.js';
|
|
20
20
|
import { NavigationMenu } from './components/navigation-menu.js';
|
|
21
|
-
import { AppBrand } from './components/app-brand.js';
|
|
22
21
|
import { HeaderActions } from './components/header-actions.js';
|
|
23
22
|
import { AppSidebarFooter } from './components/sidebar-footer.js';
|
|
24
23
|
import {
|
|
@@ -62,9 +61,6 @@ export function AppLayout({ routes }: AppLayoutProps): ReactElement {
|
|
|
62
61
|
<LayoutHeader className='sticky top-0 z-40 justify-between'>
|
|
63
62
|
<div className='flex min-w-0 items-center gap-3'>
|
|
64
63
|
<AppSidebarToggle />
|
|
65
|
-
<div className='md:hidden'>
|
|
66
|
-
<AppBrand />
|
|
67
|
-
</div>
|
|
68
64
|
<div className='h-5 w-px shrink-0 bg-border' />
|
|
69
65
|
{/* The current page's trail: the route tree's, or the one the page declares (`usePageBreadcrumb`). */}
|
|
70
66
|
<Breadcrumbs denied={denied} />
|
|
@@ -8,6 +8,20 @@ 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
|
+
pendingFeedback: {
|
|
12
|
+
working: 'Working…',
|
|
13
|
+
longWait: 'This is taking longer than usual. You can keep waiting.',
|
|
14
|
+
},
|
|
15
|
+
taskProgress: {
|
|
16
|
+
queued: 'Waiting to start',
|
|
17
|
+
running: 'In progress',
|
|
18
|
+
completed: 'Completed',
|
|
19
|
+
failed: 'Failed',
|
|
20
|
+
canceled: 'Canceled',
|
|
21
|
+
count: '{{processed}} of {{total}}',
|
|
22
|
+
processed: '{{processed}} processed',
|
|
23
|
+
background: 'It keeps running if you leave this page.',
|
|
24
|
+
},
|
|
11
25
|
listFeedback: {
|
|
12
26
|
loading: 'Loading',
|
|
13
27
|
empty: 'No data yet',
|
|
@@ -144,10 +158,14 @@ const systemEnUS = {
|
|
|
144
158
|
'auth.foundationDescription': 'Reliable data, access control and governance.',
|
|
145
159
|
'auth.marketingFooter': 'Freedom above. Confidence below.',
|
|
146
160
|
'status.loading': 'Loading',
|
|
161
|
+
'status.refreshing': 'Refreshing',
|
|
147
162
|
'status.loadingPage': 'Loading page',
|
|
148
163
|
'status.denied': 'Access denied',
|
|
149
164
|
'status.pageFailed': 'Unable to load page',
|
|
150
165
|
'status.retry': 'Retry',
|
|
166
|
+
'status.reloadPage': 'Reload page',
|
|
167
|
+
'status.retryFailedDescription':
|
|
168
|
+
'It still could not be loaded. Reloading the whole page may help; unsaved changes on this page may be lost.',
|
|
151
169
|
'navigation.brandHome': 'NocoBase home',
|
|
152
170
|
'navigation.brandApps': 'NocoBase applications',
|
|
153
171
|
'routeOverlay.close': 'Close',
|
|
@@ -412,6 +430,7 @@ const systemEnUS = {
|
|
|
412
430
|
breadcrumb: 'Breadcrumb',
|
|
413
431
|
breadcrumbMore: 'Show the levels in between',
|
|
414
432
|
back: 'Back',
|
|
433
|
+
backTo: 'Back to {{label}}',
|
|
415
434
|
},
|
|
416
435
|
dataTable: {
|
|
417
436
|
noResults: 'No results',
|
|
@@ -5,6 +5,20 @@ 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
|
+
pendingFeedback: {
|
|
9
|
+
working: '正在处理…',
|
|
10
|
+
longWait: '耗时比平常更长,您可以继续等待。',
|
|
11
|
+
},
|
|
12
|
+
taskProgress: {
|
|
13
|
+
queued: '等待开始',
|
|
14
|
+
running: '进行中',
|
|
15
|
+
completed: '已完成',
|
|
16
|
+
failed: '失败',
|
|
17
|
+
canceled: '已取消',
|
|
18
|
+
count: '{{processed}} / {{total}}',
|
|
19
|
+
processed: '已处理 {{processed}}',
|
|
20
|
+
background: '离开此页面后任务会继续运行。',
|
|
21
|
+
},
|
|
8
22
|
listFeedback: {
|
|
9
23
|
loading: '加载中',
|
|
10
24
|
empty: '暂无数据',
|
|
@@ -133,10 +147,14 @@ const systemZhCN: SystemResource = {
|
|
|
133
147
|
'auth.foundationDescription': '可靠的数据、访问控制与治理',
|
|
134
148
|
'auth.marketingFooter': '自由构建,可靠支撑',
|
|
135
149
|
'status.loading': '加载中',
|
|
150
|
+
'status.refreshing': '正在刷新',
|
|
136
151
|
'status.loadingPage': '正在加载页面',
|
|
137
152
|
'status.denied': '无权访问',
|
|
138
153
|
'status.pageFailed': '无法加载页面',
|
|
139
154
|
'status.retry': '重试',
|
|
155
|
+
'status.reloadPage': '重新加载页面',
|
|
156
|
+
'status.retryFailedDescription':
|
|
157
|
+
'仍然无法加载。可以重新加载整个页面,但本页未保存的修改可能丢失。',
|
|
140
158
|
'navigation.brandHome': 'NocoBase 首页',
|
|
141
159
|
'navigation.brandApps': 'NocoBase 应用',
|
|
142
160
|
'routeOverlay.close': '关闭',
|
|
@@ -387,6 +405,7 @@ const systemZhCN: SystemResource = {
|
|
|
387
405
|
breadcrumb: '面包屑',
|
|
388
406
|
breadcrumbMore: '显示中间层级',
|
|
389
407
|
back: '返回',
|
|
408
|
+
backTo: '返回{{label}}',
|
|
390
409
|
},
|
|
391
410
|
dataTable: {
|
|
392
411
|
noResults: '无结果',
|
|
@@ -399,7 +399,7 @@ export default function DefaultAccessPage(): ReactElement {
|
|
|
399
399
|
getRowId={(rule) => rule.key}
|
|
400
400
|
showSelectedCount={false}
|
|
401
401
|
pageSize={20}
|
|
402
|
-
loading={rules.loading || catalog.loading}
|
|
402
|
+
loading={active && (rules.loading || catalog.loading)}
|
|
403
403
|
listState={listState}
|
|
404
404
|
onRetry={() => {
|
|
405
405
|
rules.reload();
|