@spree/dashboard-core 0.13.0 → 1.0.0-beta.1
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/README.md +4 -0
- package/dist/vite/index.js +17 -3
- package/package.json +7 -7
- package/src/api-client.ts +643 -0
- package/src/components/address-form-dialog.test.ts +25 -0
- package/src/components/address-form-dialog.tsx +203 -79
- package/src/components/app-shell.tsx +171 -0
- package/src/components/app-sidebar.tsx +184 -27
- package/src/components/bulk-action-bar.tsx +89 -157
- package/src/components/country-combobox.tsx +69 -34
- package/src/components/country-state-fields.tsx +43 -11
- package/src/components/currency-select.tsx +81 -66
- package/src/components/export-button.tsx +62 -5
- package/src/components/file-upload-field.tsx +32 -10
- package/src/components/import-button.tsx +97 -36
- package/src/components/import-wizard-dialog.tsx +710 -0
- package/src/components/locale-select.tsx +70 -57
- package/src/components/media-picker-sheet.tsx +307 -0
- package/src/components/mobile-top-bar.tsx +75 -0
- package/src/components/nav-main.tsx +197 -27
- package/src/components/page-header.tsx +203 -61
- package/src/components/page-tabs.tsx +4 -1
- package/src/components/preferences-form.tsx +98 -4
- package/src/components/resource-combobox.tsx +52 -4
- package/src/components/resource-picker-sheet.tsx +209 -73
- package/src/components/resource-table.tsx +425 -131
- package/src/components/route-announcer.tsx +64 -0
- package/src/components/section-heading.tsx +95 -0
- package/src/components/settings-sidebar.tsx +290 -60
- package/src/components/sheet-top-bar.tsx +44 -0
- package/src/components/sidebar-search.tsx +83 -0
- package/src/components/sidebar-user.tsx +213 -0
- package/src/components/skip-link.tsx +36 -0
- package/src/components/store-date-picker.tsx +6 -2
- package/src/components/store-switcher.tsx +63 -22
- package/src/components/table-toolbar.tsx +1373 -442
- package/src/components/tag-combobox.tsx +12 -4
- package/src/components/top-bar-breadcrumbs.tsx +235 -0
- package/src/components/wizard-close-guard.test.ts +54 -0
- package/src/hooks/use-auto-collapse-sidebar.ts +57 -0
- package/src/hooks/use-command-palette.tsx +13 -0
- package/src/hooks/use-countries.test.ts +56 -0
- package/src/hooks/use-countries.ts +51 -5
- package/src/hooks/use-direct-upload.ts +16 -3
- package/src/hooks/use-export.ts +51 -16
- package/src/hooks/use-import.ts +261 -14
- package/src/hooks/use-package-types.ts +132 -0
- package/src/hooks/use-resource-mutation.ts +74 -13
- package/src/hooks/use-stock-locations.ts +131 -0
- package/src/hooks/use-switch-admin-locale.ts +2 -2
- package/src/hooks/use-visual-viewport-offset.ts +61 -0
- package/src/index.ts +45 -1
- package/src/lib/cancel-order-form.ts +27 -0
- package/src/lib/create-action-registry.ts +278 -0
- package/src/lib/date-presets.test.ts +86 -0
- package/src/lib/date-presets.ts +98 -0
- package/src/lib/delivery-form.ts +15 -0
- package/src/lib/docs.ts +32 -0
- package/src/lib/download.ts +13 -1
- package/src/lib/filters-to-ransack.test.ts +113 -0
- package/src/lib/filters-to-ransack.ts +22 -1
- package/src/lib/form-errors.ts +122 -10
- package/src/lib/form-mappers.ts +76 -0
- package/src/lib/fulfillment-items.test.ts +213 -0
- package/src/lib/fulfillment-items.ts +171 -0
- package/src/lib/i18n.ts +19 -11
- package/src/lib/import-types.ts +25 -0
- package/src/lib/label-upload-form.ts +22 -0
- package/src/lib/nav-registry.ts +71 -3
- package/src/lib/permissions.ts +46 -1
- package/src/lib/progress.ts +18 -0
- package/src/lib/query-keys.ts +13 -13
- package/src/lib/sample-csv.ts +21 -0
- package/src/lib/search-registry.ts +6 -2
- package/src/lib/settings-nav-registry.ts +104 -11
- package/src/lib/storefront.ts +19 -0
- package/src/lib/table-registry.ts +56 -0
- package/src/lib/type-labels.test.ts +68 -0
- package/src/lib/type-labels.ts +78 -0
- package/src/locales/ar.json +1889 -516
- package/src/locales/de.json +1883 -515
- package/src/locales/en.json +1886 -516
- package/src/locales/fr.json +1884 -516
- package/src/locales/pl.json +1913 -516
- package/src/locales/zh-CN.json +1883 -516
- package/src/pages/package-types.tsx +484 -0
- package/src/pages/settings-index.tsx +112 -0
- package/src/pages/stock-locations.tsx +674 -0
- package/src/plugin.ts +29 -0
- package/src/products/add-video-dialog.tsx +100 -0
- package/src/products/bulk-variants-dialog.tsx +301 -0
- package/src/products/carton-size.test.ts +51 -0
- package/src/products/carton-size.ts +20 -0
- package/src/products/currency-parts.ts +21 -0
- package/src/products/inventory-section.tsx +259 -0
- package/src/products/media-edit-sheet.tsx +336 -0
- package/src/products/normalize-customs.test.ts +39 -0
- package/src/products/normalize-customs.ts +27 -0
- package/src/products/normalize-money.test.ts +58 -0
- package/src/products/normalize-money.ts +73 -0
- package/src/products/normalize-quantity.test.ts +32 -0
- package/src/products/normalize-quantity.ts +31 -0
- package/src/products/product-bulk-price-editor.tsx +165 -0
- package/src/products/product-form-cards.tsx +1167 -0
- package/src/products/product-form-mapping.ts +212 -0
- package/src/products/product-schema.ts +318 -0
- package/src/products/product-types.ts +130 -0
- package/src/products/use-product-form-data.ts +273 -0
- package/src/products/variant-edit-sheet.tsx +725 -0
- package/src/products/variants-matrix.test.ts +154 -0
- package/src/products/variants-matrix.ts +244 -0
- package/src/products/variants-options-builder.tsx +571 -0
- package/src/products/variants-section.tsx +535 -0
- package/src/products/video-url.test.ts +71 -0
- package/src/products/video-url.ts +83 -0
- package/src/providers/auth-provider.tsx +70 -12
- package/src/providers/permission-provider.tsx +68 -28
- package/src/providers/sticky-header-provider.tsx +134 -0
- package/src/providers/store-provider.tsx +36 -3
- package/src/providers/tenant-provider.tsx +33 -0
- package/src/schemas/package-type.ts +95 -0
- package/src/schemas/stock-location.ts +126 -0
- package/src/tables/package-types.tsx +74 -0
- package/src/tables/stock-locations.tsx +124 -0
- package/src/vite/index.ts +40 -7
- package/src/components/top-bar.tsx +0 -210
|
@@ -10,11 +10,11 @@ import {
|
|
|
10
10
|
Field,
|
|
11
11
|
FieldDescription,
|
|
12
12
|
FieldLabel,
|
|
13
|
+
toastManager,
|
|
13
14
|
} from '@spree/dashboard-ui'
|
|
14
|
-
import { FileIcon, UploadCloudIcon, XIcon } from '
|
|
15
|
+
import { FileIcon, UploadCloudIcon, XIcon } from '@spree/dashboard-ui/icons'
|
|
15
16
|
import { type ReactNode, useState } from 'react'
|
|
16
17
|
import { useTranslation } from 'react-i18next'
|
|
17
|
-
import { toast } from 'sonner'
|
|
18
18
|
import { useDirectUpload } from '../hooks/use-direct-upload'
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -43,7 +43,9 @@ export const EMPTY_FILE_UPLOAD_VALUE: FileUploadValue = {
|
|
|
43
43
|
export function formatFileSize(bytes: number): string {
|
|
44
44
|
if (bytes < 1024) return `${bytes} B`
|
|
45
45
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
46
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
46
|
+
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
47
|
+
// Digital products are sold in gigabytes — sample packs, video, disk images.
|
|
48
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
export interface FileUploadFieldProps {
|
|
@@ -72,6 +74,18 @@ export interface FileUploadFieldProps {
|
|
|
72
74
|
disabled?: boolean
|
|
73
75
|
/** Re-wrap the picked file before upload (e.g. force a content type). */
|
|
74
76
|
transformFile?: (file: File) => File
|
|
77
|
+
/**
|
|
78
|
+
* Upload to private storage rather than the public bucket. Required for
|
|
79
|
+
* anything served through an authorized endpoint — attaching a signed id
|
|
80
|
+
* never moves a blob between services, so this has to be right up front.
|
|
81
|
+
*/
|
|
82
|
+
private?: boolean
|
|
83
|
+
/**
|
|
84
|
+
* Called with true while a picked file is uploading. A consumer whose save
|
|
85
|
+
* sends `signedId` must disable it meanwhile — the id does not exist until
|
|
86
|
+
* the upload resolves, so an early submit silently drops the file.
|
|
87
|
+
*/
|
|
88
|
+
onUploadingChange?: (uploading: boolean) => void
|
|
75
89
|
}
|
|
76
90
|
|
|
77
91
|
/**
|
|
@@ -97,9 +111,11 @@ export function FileUploadField({
|
|
|
97
111
|
mediaClassName,
|
|
98
112
|
disabled = false,
|
|
99
113
|
transformFile,
|
|
114
|
+
private: privateStorage = false,
|
|
115
|
+
onUploadingChange,
|
|
100
116
|
}: FileUploadFieldProps) {
|
|
101
117
|
const { t } = useTranslation()
|
|
102
|
-
const directUpload = useDirectUpload()
|
|
118
|
+
const directUpload = useDirectUpload({ private: privateStorage })
|
|
103
119
|
const [pending, setPending] = useState<File | null>(null)
|
|
104
120
|
|
|
105
121
|
const isImage = variant === 'image'
|
|
@@ -126,12 +142,16 @@ export function FileUploadField({
|
|
|
126
142
|
async function handlePicked(picked: File) {
|
|
127
143
|
if (disabled || pending) return
|
|
128
144
|
if (!matchesAccept(picked)) {
|
|
129
|
-
|
|
145
|
+
toastManager.add({
|
|
146
|
+
type: 'error',
|
|
147
|
+
title: t('admin.components.file_upload.invalid_type', { accept }),
|
|
148
|
+
})
|
|
130
149
|
return
|
|
131
150
|
}
|
|
132
151
|
|
|
133
152
|
const file = transformFile ? transformFile(picked) : picked
|
|
134
153
|
setPending(file)
|
|
154
|
+
onUploadingChange?.(true)
|
|
135
155
|
try {
|
|
136
156
|
const result = await directUpload.mutateAsync(file)
|
|
137
157
|
// Revoke a preview we're replacing in-place; the object URL otherwise
|
|
@@ -146,13 +166,15 @@ export function FileUploadField({
|
|
|
146
166
|
byteSize: file.size,
|
|
147
167
|
})
|
|
148
168
|
} catch (err) {
|
|
149
|
-
|
|
150
|
-
|
|
169
|
+
toastManager.add({
|
|
170
|
+
type: 'error',
|
|
171
|
+
title: t('admin.components.file_upload.upload_failed', {
|
|
151
172
|
message: err instanceof Error ? err.message : String(err),
|
|
152
173
|
}),
|
|
153
|
-
)
|
|
174
|
+
})
|
|
154
175
|
} finally {
|
|
155
176
|
setPending(null)
|
|
177
|
+
onUploadingChange?.(false)
|
|
156
178
|
}
|
|
157
179
|
}
|
|
158
180
|
|
|
@@ -191,7 +213,7 @@ export function FileUploadField({
|
|
|
191
213
|
Enter/Space; the label paints a focus ring via has-[:focus-visible]. */}
|
|
192
214
|
<label
|
|
193
215
|
className={cn(
|
|
194
|
-
'flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-border border-dashed
|
|
216
|
+
'flex cursor-pointer flex-col items-center justify-center gap-2 rounded-md border border-border border-dashed px-4 py-8 text-center transition-colors hover:bg-accent/50',
|
|
195
217
|
'has-[:focus-visible]:border-ring has-[:focus-visible]:ring-[3px] has-[:focus-visible]:ring-ring/50',
|
|
196
218
|
(disabled || !!pending) && 'pointer-events-none opacity-60',
|
|
197
219
|
)}
|
|
@@ -206,7 +228,7 @@ export function FileUploadField({
|
|
|
206
228
|
<span className="text-muted-foreground text-sm">
|
|
207
229
|
{dropLabel ?? t('admin.components.file_upload.drop_label')}
|
|
208
230
|
</span>
|
|
209
|
-
<span className="mt-1 inline-flex h-8 items-center rounded-md border border-border bg-background px-3 font-medium text-sm
|
|
231
|
+
<span className="mt-1 inline-flex h-8 items-center rounded-md border border-border bg-background px-3 font-medium text-sm hover:bg-accent">
|
|
210
232
|
{browseLabel ?? t('admin.components.file_upload.browse')}
|
|
211
233
|
</span>
|
|
212
234
|
<input
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Import, ImportType } from '@spree/admin-sdk'
|
|
2
1
|
import {
|
|
3
2
|
Button,
|
|
4
3
|
Field,
|
|
@@ -14,13 +13,20 @@ import {
|
|
|
14
13
|
SheetFooter,
|
|
15
14
|
SheetHeader,
|
|
16
15
|
SheetTitle,
|
|
16
|
+
toastManager,
|
|
17
17
|
} from '@spree/dashboard-ui'
|
|
18
|
-
import { DownloadIcon, FileSpreadsheetIcon, UploadIcon } from '
|
|
18
|
+
import { DownloadIcon, FileSpreadsheetIcon, UploadIcon } from '@spree/dashboard-ui/icons'
|
|
19
19
|
import { useState } from 'react'
|
|
20
20
|
import { useTranslation } from 'react-i18next'
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
21
|
+
import type { PanelImport } from '../api-client'
|
|
22
|
+
import {
|
|
23
|
+
type ImportCreateExtras,
|
|
24
|
+
useCreateImport,
|
|
25
|
+
useDownloadImportExample,
|
|
26
|
+
useDownloadImportTemplate,
|
|
27
|
+
} from '../hooks/use-import'
|
|
23
28
|
import type { SubjectName } from '../lib/permissions'
|
|
29
|
+
import { hasSampleCsv } from '../lib/sample-csv'
|
|
24
30
|
import { Can } from './can'
|
|
25
31
|
import { EMPTY_FILE_UPLOAD_VALUE, FileUploadField, type FileUploadValue } from './file-upload-field'
|
|
26
32
|
|
|
@@ -35,7 +41,7 @@ type Delimiter = (typeof DELIMITERS)[number]['value']
|
|
|
35
41
|
|
|
36
42
|
interface ImportButtonProps {
|
|
37
43
|
/** Which dataset to import. Server validates against `Spree::Import.available_types`. */
|
|
38
|
-
type:
|
|
44
|
+
type: string
|
|
39
45
|
/**
|
|
40
46
|
* CanCanCan subject gating the button — the *imported resource*
|
|
41
47
|
* (e.g. `Subject.Product`), mirroring the server's `write_<resource>`
|
|
@@ -47,9 +53,24 @@ interface ImportButtonProps {
|
|
|
47
53
|
* wizard — typically a full-window dialog driven by an `?import=` search
|
|
48
54
|
* param.
|
|
49
55
|
*/
|
|
50
|
-
onCreated: (imp:
|
|
56
|
+
onCreated: (imp: PanelImport) => void
|
|
51
57
|
/** Label shown on the button. Defaults to the translated "Import" action. */
|
|
52
58
|
label?: string
|
|
59
|
+
/** Optional variant for the button. Defaults to "outline". */
|
|
60
|
+
variant?: 'default' | 'outline' | 'ghost'
|
|
61
|
+
/** Optional size for the button. Defaults to "sm". */
|
|
62
|
+
size?: 'sm' | 'default' | 'lg'
|
|
63
|
+
/**
|
|
64
|
+
* Whatever else the type needs at create — a parent record's id, say. The
|
|
65
|
+
* button then belongs on that record's page, and the sheet asks nothing.
|
|
66
|
+
*/
|
|
67
|
+
params?: ImportCreateExtras
|
|
68
|
+
/**
|
|
69
|
+
* Path the import-done email links back to, relative to this panel's origin.
|
|
70
|
+
* Defaults to the operator dashboard's imports view under the current
|
|
71
|
+
* tenant; a panel filing that page elsewhere passes its own.
|
|
72
|
+
*/
|
|
73
|
+
resultsPath?: string
|
|
53
74
|
}
|
|
54
75
|
|
|
55
76
|
/**
|
|
@@ -58,13 +79,24 @@ interface ImportButtonProps {
|
|
|
58
79
|
* `FileUploadField`; submitting creates the import from the signed blob id
|
|
59
80
|
* and hands it to `onCreated`.
|
|
60
81
|
*/
|
|
61
|
-
export function ImportButton({
|
|
82
|
+
export function ImportButton({
|
|
83
|
+
type,
|
|
84
|
+
subject,
|
|
85
|
+
onCreated,
|
|
86
|
+
label,
|
|
87
|
+
variant,
|
|
88
|
+
size,
|
|
89
|
+
params,
|
|
90
|
+
resultsPath,
|
|
91
|
+
}: ImportButtonProps) {
|
|
62
92
|
const { t } = useTranslation()
|
|
63
93
|
const [open, setOpen] = useState(false)
|
|
64
94
|
const [file, setFile] = useState<FileUploadValue>(EMPTY_FILE_UPLOAD_VALUE)
|
|
65
95
|
const [delimiter, setDelimiter] = useState<Delimiter>(',')
|
|
66
96
|
const createImport = useCreateImport()
|
|
67
97
|
const downloadTemplate = useDownloadImportTemplate()
|
|
98
|
+
const downloadExample = useDownloadImportExample()
|
|
99
|
+
const hasExample = hasSampleCsv(type)
|
|
68
100
|
|
|
69
101
|
const delimiterOptions = DELIMITERS.map(({ value, labelKey }) => ({
|
|
70
102
|
value,
|
|
@@ -80,18 +112,19 @@ export function ImportButton({ type, subject, onCreated, label }: ImportButtonPr
|
|
|
80
112
|
if (!file.signedId || createImport.isPending) return
|
|
81
113
|
|
|
82
114
|
createImport.mutate(
|
|
83
|
-
{ type, signedId: file.signedId, preferredDelimiter: delimiter },
|
|
115
|
+
{ type, signedId: file.signedId, preferredDelimiter: delimiter, params, resultsPath },
|
|
84
116
|
{
|
|
85
117
|
onSuccess: (imp) => {
|
|
86
118
|
handleOpenChange(false)
|
|
87
119
|
onCreated(imp)
|
|
88
120
|
},
|
|
89
121
|
onError: (err) => {
|
|
90
|
-
|
|
91
|
-
|
|
122
|
+
toastManager.add({
|
|
123
|
+
type: 'error',
|
|
124
|
+
title: t('admin.components.import_button.failed', {
|
|
92
125
|
message: err instanceof Error ? err.message : String(err),
|
|
93
126
|
}),
|
|
94
|
-
)
|
|
127
|
+
})
|
|
95
128
|
},
|
|
96
129
|
},
|
|
97
130
|
)
|
|
@@ -100,25 +133,42 @@ export function ImportButton({ type, subject, onCreated, label }: ImportButtonPr
|
|
|
100
133
|
function handleTemplateDownload() {
|
|
101
134
|
downloadTemplate.mutate(type, {
|
|
102
135
|
onError: (err) => {
|
|
103
|
-
|
|
104
|
-
|
|
136
|
+
toastManager.add({
|
|
137
|
+
type: 'error',
|
|
138
|
+
title: t('admin.components.import_button.template_failed', {
|
|
105
139
|
message: err instanceof Error ? err.message : String(err),
|
|
106
140
|
}),
|
|
107
|
-
)
|
|
141
|
+
})
|
|
142
|
+
},
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function handleExampleDownload() {
|
|
147
|
+
downloadExample.mutate(type, {
|
|
148
|
+
onError: (err) => {
|
|
149
|
+
toastManager.add({
|
|
150
|
+
type: 'error',
|
|
151
|
+
title: t('admin.components.import_button.example_failed', {
|
|
152
|
+
message: err instanceof Error ? err.message : String(err),
|
|
153
|
+
}),
|
|
154
|
+
})
|
|
108
155
|
},
|
|
109
156
|
})
|
|
110
157
|
}
|
|
111
158
|
|
|
112
159
|
return (
|
|
113
160
|
<Can I="create" a={subject}>
|
|
161
|
+
{/* Desktop only: the wizard needs a file from disk and column
|
|
162
|
+
mapping across a wide grid, neither of which works on a phone. */}
|
|
114
163
|
<Button
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
164
|
+
type="button"
|
|
165
|
+
size={size ?? 'sm'}
|
|
166
|
+
variant={variant ?? 'outline'}
|
|
167
|
+
className="hidden h-[2.125rem] lg:inline-flex"
|
|
118
168
|
onClick={() => setOpen(true)}
|
|
119
169
|
disabled={createImport.isPending}
|
|
120
170
|
>
|
|
121
|
-
<UploadIcon
|
|
171
|
+
<UploadIcon />
|
|
122
172
|
{label ?? t('admin.actions.import')}
|
|
123
173
|
</Button>
|
|
124
174
|
|
|
@@ -162,31 +212,42 @@ export function ImportButton({ type, subject, onCreated, label }: ImportButtonPr
|
|
|
162
212
|
</Select>
|
|
163
213
|
</Field>
|
|
164
214
|
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
215
|
+
<div className="flex flex-col items-start gap-1">
|
|
216
|
+
<Button
|
|
217
|
+
type="button"
|
|
218
|
+
variant="ghost"
|
|
219
|
+
size="sm"
|
|
220
|
+
onClick={handleTemplateDownload}
|
|
221
|
+
disabled={downloadTemplate.isPending}
|
|
222
|
+
>
|
|
223
|
+
<DownloadIcon className="size-4" />
|
|
224
|
+
{t('admin.components.import_button.download_template')}
|
|
225
|
+
</Button>
|
|
226
|
+
|
|
227
|
+
{/* The populated counterpart to the headers-only template above.
|
|
228
|
+
Fetched through the API so the file matches the installed
|
|
229
|
+
Spree version's schema. */}
|
|
230
|
+
{hasExample && (
|
|
231
|
+
<Button
|
|
232
|
+
type="button"
|
|
233
|
+
variant="ghost"
|
|
234
|
+
size="sm"
|
|
235
|
+
onClick={handleExampleDownload}
|
|
236
|
+
disabled={downloadExample.isPending}
|
|
237
|
+
>
|
|
238
|
+
<FileSpreadsheetIcon className="size-4" />
|
|
239
|
+
{t('admin.components.import_button.download_example')}
|
|
240
|
+
</Button>
|
|
241
|
+
)}
|
|
242
|
+
</div>
|
|
176
243
|
</div>
|
|
177
244
|
|
|
178
245
|
<SheetFooter>
|
|
179
|
-
<Button
|
|
180
|
-
type="button"
|
|
181
|
-
variant="outline"
|
|
182
|
-
size="sm"
|
|
183
|
-
onClick={() => handleOpenChange(false)}
|
|
184
|
-
>
|
|
246
|
+
<Button type="button" variant="outline" onClick={() => handleOpenChange(false)}>
|
|
185
247
|
{t('admin.actions.cancel')}
|
|
186
248
|
</Button>
|
|
187
249
|
<Button
|
|
188
250
|
type="button"
|
|
189
|
-
size="sm"
|
|
190
251
|
onClick={handleSubmit}
|
|
191
252
|
disabled={!file.signedId || createImport.isPending}
|
|
192
253
|
>
|