@nocobase/app-template-default 1.0.0-beta.81 → 1.0.0-beta.82
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/shell.md +2 -0
- package/.agents/rules/workspace.md +9 -0
- package/.prettierignore +3 -0
- package/AGENTS.md +2 -1
- package/README.MD +4 -0
- package/client/components/attachment-list.tsx +589 -0
- package/client/components/back-button.tsx +15 -4
- package/client/components/breadcrumbs.tsx +12 -1
- package/client/extensions/nocobase-file-component-ui/components/file-list.tsx +68 -71
- package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +26 -12
- package/client/extensions/nocobase-file-component-ui/components/file-preview-field.tsx +6 -6
- package/client/extensions/nocobase-file-component-ui/components/file-thumbnail.tsx +3 -3
- package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +19 -19
- package/client/extensions/nocobase-file-component-ui/components/previewers/file-preview-content.tsx +27 -19
- package/client/extensions/nocobase-file-component-ui/components/previewers/office-open-xml-preview.tsx +12 -8
- package/client/extensions/nocobase-file-component-ui/index.ts +6 -6
- package/client/extensions/nocobase-file-component-ui/lib/message-key.ts +14 -11
- package/client/extensions/nocobase-file-component-ui/locales/de-DE.ts +49 -0
- package/client/extensions/nocobase-file-component-ui/locales/en-US.ts +47 -0
- package/client/extensions/nocobase-file-component-ui/locales/es-ES.ts +48 -0
- package/client/extensions/nocobase-file-component-ui/locales/fr-FR.ts +49 -0
- package/client/extensions/nocobase-file-component-ui/locales/id-ID.ts +46 -0
- package/client/extensions/nocobase-file-component-ui/locales/ja-JP.ts +51 -0
- package/client/extensions/nocobase-file-component-ui/locales/pt-BR.ts +49 -0
- package/client/extensions/nocobase-file-component-ui/locales/vi-VN.ts +47 -0
- package/client/extensions/nocobase-file-component-ui/locales/zh-CN.ts +43 -0
- package/client/extensions/nocobase-file-component-ui/locales/zh-TW.ts +43 -0
- package/client/extensions/nocobase-file-component-ui/types.ts +3 -1
- package/client/locales/en-US.ts +2 -0
- package/client/locales/zh-CN.ts +2 -0
- package/client/pages/settings/role-form.tsx +5 -1
- package/client/routing/route-context.tsx +33 -1
- package/eslint.config.js +2 -0
- package/package.json +19 -17
- package/scripts/check-packages.mjs +46 -0
- package/tailwind.config.mjs +3 -9
- package/tests/client/list-return-state.test.tsx +202 -0
- package/tests/client/role-return.test.tsx +104 -0
- package/tests/project/internal-packages.test.ts +98 -0
- package/tests/project/tailwind-sources.test.ts +2 -2
- package/client/extensions/nocobase-file-component-ui/README.md +0 -13
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
2
2
|
import { ArrowLeftIcon } from 'lucide-react';
|
|
3
3
|
import type { ReactElement, ReactNode } from 'react';
|
|
4
|
-
import { Link, type To, useLocation } from 'react-router';
|
|
4
|
+
import { Link, type To, useLocation, useResolvedPath } from 'react-router';
|
|
5
|
+
|
|
6
|
+
import { useRouteSearches } from '../routing/route-context.js';
|
|
5
7
|
|
|
6
8
|
import { cn } from 'cn';
|
|
7
9
|
|
|
8
10
|
export interface BackButtonProps {
|
|
9
11
|
/**
|
|
10
|
-
* Where it leads. Defaults to the parent route with
|
|
11
|
-
* the
|
|
12
|
+
* Where it leads. Defaults to the parent route with its last visited query; a directly opened detail falls back
|
|
13
|
+
* to the current query, as closing a route overlay does. An explicit destination is kept unchanged.
|
|
12
14
|
*/
|
|
13
15
|
readonly to?: To;
|
|
14
16
|
/** The label, "Back" by default. */
|
|
@@ -32,6 +34,8 @@ export function BackButton({
|
|
|
32
34
|
}: BackButtonProps = {}): ReactElement {
|
|
33
35
|
const { t } = useTranslation();
|
|
34
36
|
const location = useLocation();
|
|
37
|
+
const parent = useResolvedPath('..');
|
|
38
|
+
const searches = useRouteSearches();
|
|
35
39
|
|
|
36
40
|
return (
|
|
37
41
|
<Link
|
|
@@ -40,7 +44,14 @@ export function BackButton({
|
|
|
40
44
|
'inline-flex w-fit items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-foreground',
|
|
41
45
|
className,
|
|
42
46
|
)}
|
|
43
|
-
to={
|
|
47
|
+
to={
|
|
48
|
+
to ?? {
|
|
49
|
+
pathname: '..',
|
|
50
|
+
search:
|
|
51
|
+
searches.get(parent.pathname.replace(/\/+$/u, '') || '/') ??
|
|
52
|
+
location.search,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
44
55
|
>
|
|
45
56
|
<ArrowLeftIcon className='size-4' />
|
|
46
57
|
{children ?? t('navigation.back', { defaultValue: 'Back' })}
|
|
@@ -23,6 +23,7 @@ import { cn } from 'cn';
|
|
|
23
23
|
|
|
24
24
|
import {
|
|
25
25
|
useRouteTrail,
|
|
26
|
+
useRouteSearches,
|
|
26
27
|
type RouteTrailEntry,
|
|
27
28
|
} from '../routing/route-context.js';
|
|
28
29
|
import { routeKey } from '../routing/route-navigation.js';
|
|
@@ -51,6 +52,7 @@ export function Breadcrumbs({
|
|
|
51
52
|
denied,
|
|
52
53
|
}: BreadcrumbsProps = {}): ReactElement | null {
|
|
53
54
|
const trail = useRouteTrail();
|
|
55
|
+
const searches = useRouteSearches();
|
|
54
56
|
const declared = usePageBreadcrumbLevels();
|
|
55
57
|
const { t } = useTranslation();
|
|
56
58
|
const trailLevels =
|
|
@@ -64,7 +66,16 @@ export function Breadcrumbs({
|
|
|
64
66
|
ns: entry.route.packageName,
|
|
65
67
|
defaultValue: title,
|
|
66
68
|
}),
|
|
67
|
-
...(entry.route.componentLoader
|
|
69
|
+
...(entry.route.componentLoader
|
|
70
|
+
? {
|
|
71
|
+
to: {
|
|
72
|
+
pathname: entry.pathname,
|
|
73
|
+
search:
|
|
74
|
+
searches.get(entry.pathname.replace(/\/+$/u, '') || '/') ??
|
|
75
|
+
'',
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
: {}),
|
|
68
79
|
},
|
|
69
80
|
];
|
|
70
81
|
});
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
2
|
-
import { Download, Eye, Trash2 } from 'lucide-react';
|
|
3
2
|
import { useState, type ReactElement } from 'react';
|
|
4
3
|
|
|
5
|
-
import type { FileListProps, FileRecord } from '../types';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
import type { FileListProps, FileRecord } from '../types.js';
|
|
5
|
+
import {
|
|
6
|
+
AttachmentList,
|
|
7
|
+
type AttachmentFile,
|
|
8
|
+
} from '#components/attachment-list';
|
|
9
|
+
import { resolveSafeFileUrl } from '../lib/file-url.js';
|
|
10
|
+
import { FilePreviewDialog } from './file-preview-dialog.js';
|
|
10
11
|
|
|
11
12
|
function triggerDownload(url: string, filename: string): void {
|
|
12
13
|
const link = document.createElement('a');
|
|
@@ -17,7 +18,7 @@ function triggerDownload(url: string, filename: string): void {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function FileList(inputProps: FileListProps): ReactElement {
|
|
20
|
-
const { t } = useTranslation(
|
|
21
|
+
const { t } = useTranslation();
|
|
21
22
|
const {
|
|
22
23
|
files,
|
|
23
24
|
onPreview,
|
|
@@ -31,18 +32,19 @@ export function FileList(inputProps: FileListProps): ReactElement {
|
|
|
31
32
|
const [previewIndex, setPreviewIndex] = useState(0);
|
|
32
33
|
const [previewOpen, setPreviewOpen] = useState(false);
|
|
33
34
|
const previewLabel =
|
|
34
|
-
labels?.preview ?? t('files.preview', { defaultValue: 'Preview' });
|
|
35
|
+
labels?.preview ?? t('fileUi.files.preview', { defaultValue: 'Preview' });
|
|
35
36
|
const downloadLabel =
|
|
36
|
-
labels?.download ??
|
|
37
|
+
labels?.download ??
|
|
38
|
+
t('fileUi.files.download', { defaultValue: 'Download' });
|
|
37
39
|
const removeLabel =
|
|
38
|
-
labels?.remove ?? t('files.remove', { defaultValue: 'Remove' });
|
|
40
|
+
labels?.remove ?? t('fileUi.files.remove', { defaultValue: 'Remove' });
|
|
39
41
|
|
|
40
42
|
if (!files.length)
|
|
41
43
|
return (
|
|
42
44
|
<div role='status'>
|
|
43
45
|
{emptyState ??
|
|
44
46
|
labels?.empty ??
|
|
45
|
-
t('files.empty', { defaultValue: 'No files.' })}
|
|
47
|
+
t('fileUi.files.empty', { defaultValue: 'No files.' })}
|
|
46
48
|
</div>
|
|
47
49
|
);
|
|
48
50
|
|
|
@@ -56,7 +58,7 @@ export function FileList(inputProps: FileListProps): ReactElement {
|
|
|
56
58
|
const url = raw ? resolveSafeFileUrl(raw) : undefined;
|
|
57
59
|
if (!url)
|
|
58
60
|
throw new Error(
|
|
59
|
-
t('urlNotAllowed', {
|
|
61
|
+
t('fileUi.urlNotAllowed', {
|
|
60
62
|
defaultValue: 'File URL is not allowed.',
|
|
61
63
|
}),
|
|
62
64
|
);
|
|
@@ -66,7 +68,7 @@ export function FileList(inputProps: FileListProps): ReactElement {
|
|
|
66
68
|
error instanceof Error
|
|
67
69
|
? error
|
|
68
70
|
: new Error(
|
|
69
|
-
t('downloadFailed', {
|
|
71
|
+
t('fileUi.downloadFailed', {
|
|
70
72
|
defaultValue: 'File download failed.',
|
|
71
73
|
}),
|
|
72
74
|
),
|
|
@@ -74,71 +76,66 @@ export function FileList(inputProps: FileListProps): ReactElement {
|
|
|
74
76
|
});
|
|
75
77
|
};
|
|
76
78
|
|
|
79
|
+
const attachments: readonly AttachmentFile[] = files.map((file) => ({
|
|
80
|
+
id: file.id,
|
|
81
|
+
name: file.filename,
|
|
82
|
+
size: Number(file.size),
|
|
83
|
+
url: file.contentUrl ? (resolveSafeFileUrl(file.contentUrl) ?? '') : '',
|
|
84
|
+
downloadUrl: file.contentUrl
|
|
85
|
+
? (resolveSafeFileUrl(file.contentUrl) ?? '')
|
|
86
|
+
: '',
|
|
87
|
+
image: file.mimeType.startsWith('image/'),
|
|
88
|
+
canRemove: Boolean(onRemove),
|
|
89
|
+
}));
|
|
77
90
|
return (
|
|
78
91
|
<>
|
|
79
|
-
<
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
</Button>
|
|
121
|
-
{onRemove ? (
|
|
122
|
-
<Button
|
|
123
|
-
type='button'
|
|
124
|
-
size='icon'
|
|
125
|
-
variant='ghost'
|
|
126
|
-
aria-label={`${removeLabel}: ${file.filename}`}
|
|
127
|
-
title={removeLabel}
|
|
128
|
-
onClick={() => void onRemove(file)}
|
|
129
|
-
>
|
|
130
|
-
<Trash2 aria-hidden='true' />
|
|
131
|
-
</Button>
|
|
132
|
-
) : null}
|
|
133
|
-
</div>
|
|
134
|
-
</li>
|
|
135
|
-
))}
|
|
136
|
-
</ul>
|
|
92
|
+
<AttachmentList
|
|
93
|
+
files={attachments}
|
|
94
|
+
labels={{
|
|
95
|
+
title: '',
|
|
96
|
+
images: '',
|
|
97
|
+
files: '',
|
|
98
|
+
pending: '',
|
|
99
|
+
upload: '',
|
|
100
|
+
uploading: '',
|
|
101
|
+
removeTitle: '',
|
|
102
|
+
removeDescription: '',
|
|
103
|
+
removeConfirm: '',
|
|
104
|
+
cancel: '',
|
|
105
|
+
previous: '',
|
|
106
|
+
next: '',
|
|
107
|
+
preview: `${previewLabel}: {name}`,
|
|
108
|
+
download: `${downloadLabel}: {name}`,
|
|
109
|
+
remove: `${removeLabel}: {name}`,
|
|
110
|
+
}}
|
|
111
|
+
onPreview={(ordered, index) => {
|
|
112
|
+
const selected = files.findIndex(
|
|
113
|
+
(file) => file.id === ordered[index]?.id,
|
|
114
|
+
);
|
|
115
|
+
if (selected < 0) return;
|
|
116
|
+
onPreview?.(files[selected]);
|
|
117
|
+
setPreviewIndex(selected);
|
|
118
|
+
setPreviewOpen(true);
|
|
119
|
+
}}
|
|
120
|
+
onDownload={(item) => {
|
|
121
|
+
const file = files.find((file) => file.id === item.id);
|
|
122
|
+
if (file) downloadFile(file);
|
|
123
|
+
}}
|
|
124
|
+
onRemove={
|
|
125
|
+
onRemove
|
|
126
|
+
? (item) => {
|
|
127
|
+
const file = files.find((file) => file.id === item.id);
|
|
128
|
+
if (file) void onRemove(file);
|
|
129
|
+
}
|
|
130
|
+
: undefined
|
|
131
|
+
}
|
|
132
|
+
/>
|
|
137
133
|
<FilePreviewDialog
|
|
138
134
|
files={files}
|
|
139
135
|
initialIndex={previewIndex}
|
|
140
136
|
open={previewOpen}
|
|
141
137
|
onOpenChange={setPreviewOpen}
|
|
138
|
+
onDownload={onDownload}
|
|
142
139
|
labels={labels}
|
|
143
140
|
onError={onError}
|
|
144
141
|
/>
|
|
@@ -6,16 +6,16 @@ import type {
|
|
|
6
6
|
FilePreviewDialogProps,
|
|
7
7
|
FileRecord,
|
|
8
8
|
FileUiLabels,
|
|
9
|
-
} from '../types';
|
|
9
|
+
} from '../types.js';
|
|
10
10
|
import {
|
|
11
11
|
isActiveMarkupMimeType,
|
|
12
12
|
resolveFilePreviewKind,
|
|
13
13
|
type FilePreviewKind,
|
|
14
|
-
} from '../lib/file-preview';
|
|
14
|
+
} from '../lib/file-preview.js';
|
|
15
15
|
import { Button } from '#components/ui/button';
|
|
16
16
|
import { Dialog, DialogContent, DialogTitle } from '#components/ui/dialog';
|
|
17
|
-
import { fileUrlCredentials, resolveSafeFileUrl } from '../lib/file-url';
|
|
18
|
-
import { FilePreviewContent } from './previewers/file-preview-content';
|
|
17
|
+
import { fileUrlCredentials, resolveSafeFileUrl } from '../lib/file-url.js';
|
|
18
|
+
import { FilePreviewContent } from './previewers/file-preview-content.js';
|
|
19
19
|
|
|
20
20
|
const PDF_MIME_TYPE = 'application/pdf';
|
|
21
21
|
const PDF_PREVIEW_ERROR = 'Unable to load the PDF preview.';
|
|
@@ -27,6 +27,7 @@ export function FilePreviewDialog({
|
|
|
27
27
|
initialIndex = 0,
|
|
28
28
|
open,
|
|
29
29
|
onOpenChange,
|
|
30
|
+
onDownload,
|
|
30
31
|
download: allowDownload = true,
|
|
31
32
|
labels,
|
|
32
33
|
onError,
|
|
@@ -39,6 +40,7 @@ export function FilePreviewDialog({
|
|
|
39
40
|
files={files}
|
|
40
41
|
initialIndex={normalizedIndex}
|
|
41
42
|
onOpenChange={onOpenChange}
|
|
43
|
+
onDownload={onDownload}
|
|
42
44
|
download={allowDownload}
|
|
43
45
|
labels={labels}
|
|
44
46
|
onError={onError}
|
|
@@ -50,6 +52,7 @@ interface OpenFilePreviewDialogProps {
|
|
|
50
52
|
readonly files: readonly FileRecord[];
|
|
51
53
|
readonly initialIndex: number;
|
|
52
54
|
readonly onOpenChange: (open: boolean) => void;
|
|
55
|
+
readonly onDownload?: FilePreviewDialogProps['onDownload'];
|
|
53
56
|
readonly download: boolean;
|
|
54
57
|
readonly labels?: FileUiLabels;
|
|
55
58
|
readonly onError?: (error: Error) => void;
|
|
@@ -58,11 +61,12 @@ interface OpenFilePreviewDialogProps {
|
|
|
58
61
|
function OpenFilePreviewDialog(
|
|
59
62
|
inputProps: OpenFilePreviewDialogProps,
|
|
60
63
|
): ReactElement {
|
|
61
|
-
const { t } = useTranslation(
|
|
64
|
+
const { t } = useTranslation();
|
|
62
65
|
const {
|
|
63
66
|
files,
|
|
64
67
|
initialIndex,
|
|
65
68
|
onOpenChange,
|
|
69
|
+
onDownload,
|
|
66
70
|
download: allowDownload,
|
|
67
71
|
labels,
|
|
68
72
|
onError,
|
|
@@ -72,7 +76,8 @@ function OpenFilePreviewDialog(
|
|
|
72
76
|
const file = files[index];
|
|
73
77
|
if (!file) throw new Error('A preview file is required.');
|
|
74
78
|
const downloadLabel =
|
|
75
|
-
labels?.download ??
|
|
79
|
+
labels?.download ??
|
|
80
|
+
t('fileUi.files.download', { defaultValue: 'Download' });
|
|
76
81
|
return (
|
|
77
82
|
<Dialog open onOpenChange={onOpenChange}>
|
|
78
83
|
<DialogContent
|
|
@@ -91,7 +96,7 @@ function OpenFilePreviewDialog(
|
|
|
91
96
|
type='button'
|
|
92
97
|
size='icon'
|
|
93
98
|
variant='ghost'
|
|
94
|
-
aria-label={t('files.previous', {
|
|
99
|
+
aria-label={t('fileUi.files.previous', {
|
|
95
100
|
defaultValue: 'Previous file',
|
|
96
101
|
})}
|
|
97
102
|
onClick={() =>
|
|
@@ -106,7 +111,9 @@ function OpenFilePreviewDialog(
|
|
|
106
111
|
type='button'
|
|
107
112
|
size='icon'
|
|
108
113
|
variant='ghost'
|
|
109
|
-
aria-label={t('files.next', {
|
|
114
|
+
aria-label={t('fileUi.files.next', {
|
|
115
|
+
defaultValue: 'Next file',
|
|
116
|
+
})}
|
|
110
117
|
onClick={() =>
|
|
111
118
|
setIndex((value) => (value + 1) % files.length)
|
|
112
119
|
}
|
|
@@ -122,7 +129,7 @@ function OpenFilePreviewDialog(
|
|
|
122
129
|
variant='ghost'
|
|
123
130
|
aria-label={`${downloadLabel}: ${file.filename}`}
|
|
124
131
|
onClick={() =>
|
|
125
|
-
void downloadFile(file).catch((error: unknown) =>
|
|
132
|
+
void downloadFile(file, onDownload).catch((error: unknown) =>
|
|
126
133
|
reportDownloadError(onError, error),
|
|
127
134
|
)
|
|
128
135
|
}
|
|
@@ -140,8 +147,8 @@ function OpenFilePreviewDialog(
|
|
|
140
147
|
onDownload={
|
|
141
148
|
allowDownload
|
|
142
149
|
? () =>
|
|
143
|
-
void downloadFile(file).catch(
|
|
144
|
-
reportDownloadError(onError, error),
|
|
150
|
+
void downloadFile(file, onDownload).catch(
|
|
151
|
+
(error: unknown) => reportDownloadError(onError, error),
|
|
145
152
|
)
|
|
146
153
|
: undefined
|
|
147
154
|
}
|
|
@@ -152,7 +159,14 @@ function OpenFilePreviewDialog(
|
|
|
152
159
|
);
|
|
153
160
|
}
|
|
154
161
|
|
|
155
|
-
async function downloadFile(
|
|
162
|
+
async function downloadFile(
|
|
163
|
+
file: FileRecord,
|
|
164
|
+
onDownload?: FilePreviewDialogProps['onDownload'],
|
|
165
|
+
): Promise<void> {
|
|
166
|
+
if (onDownload) {
|
|
167
|
+
await onDownload(file);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
156
170
|
const raw = file.contentUrl;
|
|
157
171
|
const url = raw ? resolveSafeFileUrl(raw) : undefined;
|
|
158
172
|
if (!url) throw new Error('File URL is not allowed.');
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { useTranslation } from '@nocobase/i18n/client';
|
|
2
2
|
import { useState, type ReactElement } from 'react';
|
|
3
|
-
import type { FilePreviewFieldProps } from '../types';
|
|
3
|
+
import type { FilePreviewFieldProps } from '../types.js';
|
|
4
4
|
import { Button } from '#components/ui/button';
|
|
5
|
-
import { FilePreviewDialog } from './file-preview-dialog';
|
|
6
|
-
import { FileThumbnail } from './file-thumbnail';
|
|
5
|
+
import { FilePreviewDialog } from './file-preview-dialog.js';
|
|
6
|
+
import { FileThumbnail } from './file-thumbnail.js';
|
|
7
7
|
|
|
8
8
|
export function FilePreviewField(
|
|
9
9
|
inputProps: FilePreviewFieldProps,
|
|
10
10
|
): ReactElement {
|
|
11
|
-
const { t } = useTranslation(
|
|
11
|
+
const { t } = useTranslation();
|
|
12
12
|
const {
|
|
13
13
|
files,
|
|
14
14
|
labels,
|
|
@@ -24,7 +24,7 @@ export function FilePreviewField(
|
|
|
24
24
|
<>
|
|
25
25
|
{emptyState ?? (
|
|
26
26
|
<span role='status'>
|
|
27
|
-
{t('files.empty', { defaultValue: 'No files.' })}
|
|
27
|
+
{t('fileUi.files.empty', { defaultValue: 'No files.' })}
|
|
28
28
|
</span>
|
|
29
29
|
)}
|
|
30
30
|
</>
|
|
@@ -39,7 +39,7 @@ export function FilePreviewField(
|
|
|
39
39
|
variant='ghost'
|
|
40
40
|
size='icon'
|
|
41
41
|
className='h-12 w-12 overflow-hidden'
|
|
42
|
-
aria-label={`${labels?.preview ?? t('files.preview', { defaultValue: 'Preview' })}: ${file.filename}`}
|
|
42
|
+
aria-label={`${labels?.preview ?? t('fileUi.files.preview', { defaultValue: 'Preview' })}: ${file.filename}`}
|
|
43
43
|
onClick={() => {
|
|
44
44
|
setInitialIndex(index);
|
|
45
45
|
setOpen(true);
|
|
@@ -8,9 +8,9 @@ import {
|
|
|
8
8
|
} from 'lucide-react';
|
|
9
9
|
import { useState, type ReactElement } from 'react';
|
|
10
10
|
|
|
11
|
-
import { isSafeImagePreview } from '../lib/file-preview';
|
|
12
|
-
import type { FileThumbnailProps } from '../types';
|
|
13
|
-
import { resolveSafeFileUrl } from '../lib/file-url';
|
|
11
|
+
import { isSafeImagePreview } from '../lib/file-preview.js';
|
|
12
|
+
import type { FileThumbnailProps } from '../types.js';
|
|
13
|
+
import { resolveSafeFileUrl } from '../lib/file-url.js';
|
|
14
14
|
|
|
15
15
|
function extension(filename: string): string {
|
|
16
16
|
const dot = filename.lastIndexOf('.');
|
|
@@ -10,10 +10,10 @@ import {
|
|
|
10
10
|
type ReactElement,
|
|
11
11
|
} from 'react';
|
|
12
12
|
|
|
13
|
-
import type { FileRecord, FileUploadFieldProps } from '../types';
|
|
13
|
+
import type { FileRecord, FileUploadFieldProps } from '../types.js';
|
|
14
14
|
import { Button } from '#components/ui/button';
|
|
15
15
|
import { Spinner } from '#components/ui/spinner';
|
|
16
|
-
import { FileThumbnail } from './file-thumbnail';
|
|
16
|
+
import { FileThumbnail } from './file-thumbnail.js';
|
|
17
17
|
|
|
18
18
|
type UploadItem = {
|
|
19
19
|
readonly key: string;
|
|
@@ -40,7 +40,7 @@ function accepts(file: File, rules: readonly string[]): boolean {
|
|
|
40
40
|
export function FileUploadField(
|
|
41
41
|
inputProps: FileUploadFieldProps,
|
|
42
42
|
): ReactElement {
|
|
43
|
-
const { t } = useTranslation(
|
|
43
|
+
const { t } = useTranslation();
|
|
44
44
|
const {
|
|
45
45
|
repository,
|
|
46
46
|
value,
|
|
@@ -69,12 +69,12 @@ export function FileUploadField(
|
|
|
69
69
|
const chooseLabel =
|
|
70
70
|
labels?.choose ??
|
|
71
71
|
(multiple
|
|
72
|
-
? t('files.chooseMany', { defaultValue: 'Choose files' })
|
|
73
|
-
: t('files.chooseOne', { defaultValue: 'Choose file' }));
|
|
72
|
+
? t('fileUi.files.chooseMany', { defaultValue: 'Choose files' })
|
|
73
|
+
: t('fileUi.files.chooseOne', { defaultValue: 'Choose file' }));
|
|
74
74
|
const removeLabel =
|
|
75
|
-
labels?.remove ?? t('files.remove', { defaultValue: 'Remove' });
|
|
75
|
+
labels?.remove ?? t('fileUi.files.remove', { defaultValue: 'Remove' });
|
|
76
76
|
const retryLabel =
|
|
77
|
-
labels?.retry ?? t('files.retry', { defaultValue: 'Retry' });
|
|
77
|
+
labels?.retry ?? t('fileUi.files.retry', { defaultValue: 'Retry' });
|
|
78
78
|
|
|
79
79
|
useEffect(() => {
|
|
80
80
|
valueRef.current = value;
|
|
@@ -170,7 +170,7 @@ export function FileUploadField(
|
|
|
170
170
|
if (files.length + (multiple ? value.length : 0) + items.length > maximum) {
|
|
171
171
|
onError?.(
|
|
172
172
|
new Error(
|
|
173
|
-
t('fileLimitReached', {
|
|
173
|
+
t('fileUi.fileLimitReached', {
|
|
174
174
|
defaultValue: 'The maximum number of files has been reached.',
|
|
175
175
|
}),
|
|
176
176
|
),
|
|
@@ -181,7 +181,7 @@ export function FileUploadField(
|
|
|
181
181
|
if (maxSize !== undefined && file.size > maxSize) {
|
|
182
182
|
onError?.(
|
|
183
183
|
new Error(
|
|
184
|
-
t('fileTooLarge', {
|
|
184
|
+
t('fileUi.fileTooLarge', {
|
|
185
185
|
defaultValue: 'File exceeds the maximum size.',
|
|
186
186
|
}),
|
|
187
187
|
),
|
|
@@ -191,7 +191,7 @@ export function FileUploadField(
|
|
|
191
191
|
if (!accepts(file, accept)) {
|
|
192
192
|
onError?.(
|
|
193
193
|
new Error(
|
|
194
|
-
t('fileTypeNotAllowed', {
|
|
194
|
+
t('fileUi.fileTypeNotAllowed', {
|
|
195
195
|
defaultValue: 'File type is not allowed.',
|
|
196
196
|
}),
|
|
197
197
|
),
|
|
@@ -218,7 +218,7 @@ export function FileUploadField(
|
|
|
218
218
|
error instanceof Error
|
|
219
219
|
? error
|
|
220
220
|
: new Error(
|
|
221
|
-
t('removalFailed', {
|
|
221
|
+
t('fileUi.removalFailed', {
|
|
222
222
|
defaultValue: 'File removal failed.',
|
|
223
223
|
}),
|
|
224
224
|
),
|
|
@@ -272,7 +272,7 @@ export function FileUploadField(
|
|
|
272
272
|
{record.filename}
|
|
273
273
|
</div>
|
|
274
274
|
<div className='text-xs text-muted-foreground'>
|
|
275
|
-
{t('files.done', { defaultValue: 'Done' })}
|
|
275
|
+
{t('fileUi.files.done', { defaultValue: 'Done' })}
|
|
276
276
|
</div>
|
|
277
277
|
<Button
|
|
278
278
|
type='button'
|
|
@@ -291,7 +291,7 @@ export function FileUploadField(
|
|
|
291
291
|
<div className='flex h-20 items-center justify-center overflow-hidden rounded-sm bg-muted/30'>
|
|
292
292
|
{item.status === 'uploading' ? (
|
|
293
293
|
<Spinner
|
|
294
|
-
aria-label={t('files.uploading', {
|
|
294
|
+
aria-label={t('fileUi.files.uploading', {
|
|
295
295
|
defaultValue: 'Uploading',
|
|
296
296
|
})}
|
|
297
297
|
/>
|
|
@@ -309,10 +309,10 @@ export function FileUploadField(
|
|
|
309
309
|
defaultValue: item.error.message,
|
|
310
310
|
})
|
|
311
311
|
: undefined) ??
|
|
312
|
-
t('files.failed', { defaultValue: 'Failed' }))
|
|
312
|
+
t('fileUi.files.failed', { defaultValue: 'Failed' }))
|
|
313
313
|
: item.status === 'uploading'
|
|
314
|
-
? t('files.uploading', { defaultValue: 'Uploading' })
|
|
315
|
-
: t('files.pending', { defaultValue: 'Pending' })}
|
|
314
|
+
? t('fileUi.files.uploading', { defaultValue: 'Uploading' })
|
|
315
|
+
: t('fileUi.files.pending', { defaultValue: 'Pending' })}
|
|
316
316
|
</div>
|
|
317
317
|
<div className='mt-2 flex gap-1'>
|
|
318
318
|
{item.status === 'error' ? (
|
|
@@ -331,7 +331,7 @@ export function FileUploadField(
|
|
|
331
331
|
type='button'
|
|
332
332
|
size='icon'
|
|
333
333
|
variant='ghost'
|
|
334
|
-
aria-label={t('files.cancelFile', {
|
|
334
|
+
aria-label={t('fileUi.files.cancelFile', {
|
|
335
335
|
filename: item.file.name,
|
|
336
336
|
defaultValue: `Cancel ${item.file.name}`,
|
|
337
337
|
})}
|
|
@@ -373,11 +373,11 @@ export function FileUploadField(
|
|
|
373
373
|
{[
|
|
374
374
|
...value.map(
|
|
375
375
|
(record) =>
|
|
376
|
-
`${record.filename}: ${t('files.done', { defaultValue: 'done' })}`,
|
|
376
|
+
`${record.filename}: ${t('fileUi.files.done', { defaultValue: 'done' })}`,
|
|
377
377
|
),
|
|
378
378
|
...items.map(
|
|
379
379
|
(item) =>
|
|
380
|
-
`${item.file.name}: ${t(`files.status.${item.status}`, { defaultValue: item.status })}`,
|
|
380
|
+
`${item.file.name}: ${t(`fileUi.files.status.${item.status}`, { defaultValue: item.status })}`,
|
|
381
381
|
),
|
|
382
382
|
].join('. ')}
|
|
383
383
|
</div>
|