@nocobase/app-template-default 1.0.0-beta.80 → 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.
Files changed (48) hide show
  1. package/.agents/rules/shell.md +2 -0
  2. package/.agents/rules/workspace.md +9 -0
  3. package/.prettierignore +3 -0
  4. package/AGENTS.md +2 -1
  5. package/README.MD +4 -0
  6. package/client/components/attachment-list.tsx +589 -0
  7. package/client/components/back-button.tsx +15 -4
  8. package/client/components/breadcrumbs.tsx +12 -1
  9. package/client/extensions/nocobase-file-component-ui/components/file-list.tsx +68 -71
  10. package/client/extensions/nocobase-file-component-ui/components/file-preview-dialog.tsx +26 -12
  11. package/client/extensions/nocobase-file-component-ui/components/file-preview-field.tsx +6 -6
  12. package/client/extensions/nocobase-file-component-ui/components/file-thumbnail.tsx +3 -3
  13. package/client/extensions/nocobase-file-component-ui/components/file-upload-field.tsx +19 -19
  14. package/client/extensions/nocobase-file-component-ui/components/previewers/file-preview-content.tsx +27 -19
  15. package/client/extensions/nocobase-file-component-ui/components/previewers/office-open-xml-preview.tsx +12 -8
  16. package/client/extensions/nocobase-file-component-ui/index.ts +6 -6
  17. package/client/extensions/nocobase-file-component-ui/lib/message-key.ts +14 -11
  18. package/client/extensions/nocobase-file-component-ui/locales/de-DE.ts +49 -0
  19. package/client/extensions/nocobase-file-component-ui/locales/en-US.ts +47 -0
  20. package/client/extensions/nocobase-file-component-ui/locales/es-ES.ts +48 -0
  21. package/client/extensions/nocobase-file-component-ui/locales/fr-FR.ts +49 -0
  22. package/client/extensions/nocobase-file-component-ui/locales/id-ID.ts +46 -0
  23. package/client/extensions/nocobase-file-component-ui/locales/ja-JP.ts +51 -0
  24. package/client/extensions/nocobase-file-component-ui/locales/pt-BR.ts +49 -0
  25. package/client/extensions/nocobase-file-component-ui/locales/vi-VN.ts +47 -0
  26. package/client/extensions/nocobase-file-component-ui/locales/zh-CN.ts +43 -0
  27. package/client/extensions/nocobase-file-component-ui/locales/zh-TW.ts +43 -0
  28. package/client/extensions/nocobase-file-component-ui/types.ts +3 -1
  29. package/client/locales/en-US.ts +2 -0
  30. package/client/locales/system/settings-en-US.ts +3 -0
  31. package/client/locales/system/settings-zh-CN.ts +2 -0
  32. package/client/locales/zh-CN.ts +2 -0
  33. package/client/pages/settings/role-form.tsx +5 -1
  34. package/client/pages/settings/user-form.tsx +13 -1
  35. package/client/pages/settings/users.tsx +1 -1
  36. package/client/routing/route-context.tsx +33 -1
  37. package/eslint.config.js +2 -0
  38. package/package.json +23 -21
  39. package/scripts/check-packages.mjs +46 -0
  40. package/tailwind.config.mjs +3 -9
  41. package/tests/client/list-return-state.test.tsx +202 -0
  42. package/tests/client/role-return.test.tsx +104 -0
  43. package/tests/client/user-drawer.test.tsx +48 -0
  44. package/tests/project/internal-packages.test.ts +98 -0
  45. package/tests/project/tailwind-sources.test.ts +2 -2
  46. package/tests/server/authorization-assignment-safety.test.ts +10 -9
  47. package/tests/server/member-role.test.ts +299 -0
  48. 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 the current query string, as closing a route overlay does, so
11
- * the list a child page covers keeps its search and filters.
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={to ?? { pathname: '..', search: location.search }}
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 ? { to: entry.pathname } : {}),
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 { Button } from '#components/ui/button';
7
- import { resolveSafeFileUrl } from '../lib/file-url';
8
- import { FilePreviewDialog } from './file-preview-dialog';
9
- import { FileThumbnail } from './file-thumbnail';
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('@nocobase/app-plugin-file');
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 ?? t('files.download', { defaultValue: '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
- <ul data-slot='file-list' className='grid gap-3 sm:grid-cols-2'>
80
- {files.map((file, index) => (
81
- <li
82
- key={file.id}
83
- className='flex min-w-0 items-center gap-3 rounded-md border p-3'
84
- >
85
- <div className='h-12 w-12 shrink-0 overflow-hidden rounded-md'>
86
- <FileThumbnail file={file} />
87
- </div>
88
- <div className='min-w-0 flex-1'>
89
- <div className='truncate font-medium' title={file.filename}>
90
- {file.filename}
91
- </div>
92
- <div className='text-sm text-muted-foreground'>
93
- {file.mimeType}
94
- </div>
95
- </div>
96
- <div className='flex shrink-0 items-center gap-1'>
97
- <Button
98
- type='button'
99
- size='icon'
100
- variant='ghost'
101
- aria-label={`${previewLabel}: ${file.filename}`}
102
- title={previewLabel}
103
- onClick={() => {
104
- onPreview?.(file);
105
- setPreviewIndex(index);
106
- setPreviewOpen(true);
107
- }}
108
- >
109
- <Eye aria-hidden='true' />
110
- </Button>
111
- <Button
112
- type='button'
113
- size='icon'
114
- variant='ghost'
115
- aria-label={`${downloadLabel}: ${file.filename}`}
116
- title={downloadLabel}
117
- onClick={() => downloadFile(file)}
118
- >
119
- <Download aria-hidden='true' />
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('@nocobase/app-plugin-file');
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 ?? t('files.download', { defaultValue: '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', { defaultValue: 'Next file' })}
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((error: unknown) =>
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(file: FileRecord): Promise<void> {
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('@nocobase/app-plugin-file');
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('@nocobase/app-plugin-file');
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>