@spree/dashboard-core 1.0.0-rc.1 → 1.0.0-rc.3
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/dist/vite/discover.js +9 -14
- package/package.json +3 -5
- package/src/api-client.ts +22 -34
- package/src/components/address-form-dialog.test.ts +4 -4
- package/src/components/address-form-dialog.tsx +10 -10
- package/src/components/app-shell.tsx +12 -1
- package/src/components/app-sidebar.tsx +18 -2
- package/src/components/country-combobox.tsx +52 -29
- package/src/components/currency-select.tsx +16 -10
- package/src/components/file-upload-field.tsx +4 -4
- package/src/components/import-button.tsx +4 -1
- package/src/components/mobile-top-bar.tsx +6 -0
- package/src/components/money.tsx +23 -0
- package/src/components/preferences-form.tsx +247 -52
- package/src/components/resource-table.tsx +7 -3
- package/src/hooks/use-display-name.ts +11 -3
- package/src/hooks/use-file-upload.ts +51 -0
- package/src/hooks/use-import.ts +5 -5
- package/src/hooks/use-money-locale.ts +14 -0
- package/src/index.ts +8 -3
- package/src/lib/currencies.ts +5 -2
- package/src/lib/form-mappers.ts +25 -5
- package/src/lib/formatters.ts +34 -4
- package/src/lib/fulfillment-items.test.ts +2 -1
- package/src/lib/fulfillment-items.ts +9 -3
- package/src/lib/i18n.ts +4 -4
- package/src/lib/iso-codes.ts +45 -0
- package/src/lib/money.test.ts +63 -0
- package/src/lib/money.ts +99 -0
- package/src/{products → lib}/normalize-money.ts +2 -2
- package/src/lib/return-refunds.test.ts +62 -0
- package/src/lib/return-refunds.ts +70 -0
- package/src/lib/route-registry.ts +58 -15
- package/src/lib/slot-registry.ts +10 -0
- package/src/lib/storefront.ts +2 -2
- package/src/locales/ar.json +4 -1
- package/src/locales/de.json +4 -1
- package/src/locales/en.json +4 -1
- package/src/locales/es.json +4 -1
- package/src/locales/fr.json +4 -1
- package/src/locales/pl.json +4 -1
- package/src/locales/zh-CN.json +4 -1
- package/src/plugin.ts +5 -5
- package/src/products/bulk-variants-dialog.tsx +1 -1
- package/src/products/product-bulk-price-editor.tsx +18 -29
- package/src/products/product-form-cards.tsx +12 -8
- package/src/products/product-form-mapping.ts +15 -11
- package/src/products/product-schema.ts +6 -8
- package/src/products/product-types.ts +5 -3
- package/src/products/use-product-form-data.ts +0 -28
- package/src/providers/store-provider.tsx +9 -9
- package/src/schemas/stock-location.ts +3 -2
- package/src/tables/package-types.tsx +1 -1
- package/src/tables/stock-locations.tsx +1 -1
- package/src/vite/discover.ts +33 -28
- package/src/hooks/use-direct-upload.ts +0 -117
- /package/src/{products → lib}/currency-parts.ts +0 -0
- /package/src/{products → lib}/normalize-money.test.ts +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { CountryCode, CurrencyCode } from '@spree/admin-sdk'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* What the shared product form reads off a product, its variants and its
|
|
3
5
|
* media.
|
|
@@ -42,12 +44,12 @@ export interface PanelVariant {
|
|
|
42
44
|
price?: { amount?: string | null; currency?: string | null } | null
|
|
43
45
|
prices?: Array<{
|
|
44
46
|
amount?: string | null
|
|
45
|
-
currency?:
|
|
47
|
+
currency?: CurrencyCode | null
|
|
46
48
|
compare_at_amount?: string | null
|
|
47
49
|
price_list_id?: string | null
|
|
48
50
|
}>
|
|
49
51
|
cost_price?: string | null
|
|
50
|
-
cost_currency?:
|
|
52
|
+
cost_currency?: CurrencyCode | null
|
|
51
53
|
weight?: number | null
|
|
52
54
|
height?: number | null
|
|
53
55
|
width?: number | null
|
|
@@ -55,7 +57,7 @@ export interface PanelVariant {
|
|
|
55
57
|
weight_unit?: string | null
|
|
56
58
|
dimensions_unit?: string | null
|
|
57
59
|
hs_code?: string | null
|
|
58
|
-
country_of_origin?:
|
|
60
|
+
country_of_origin?: CountryCode | null
|
|
59
61
|
customs_description?: string | null
|
|
60
62
|
minimum_order_quantity?: number | null
|
|
61
63
|
order_multiple?: number | null
|
|
@@ -2,7 +2,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
|
|
2
2
|
import type {
|
|
3
3
|
PanelCollection,
|
|
4
4
|
PanelDeliveryProfile,
|
|
5
|
-
PanelMarket,
|
|
6
5
|
PanelNamedRecord,
|
|
7
6
|
PanelOptionType,
|
|
8
7
|
PanelPackageType,
|
|
@@ -244,30 +243,3 @@ export function collectionAutocompleteProps(queryKey: string) {
|
|
|
244
243
|
getOptionLabel: (collection: PanelCollection) => collection.name ?? collection.id,
|
|
245
244
|
}
|
|
246
245
|
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Resolves a currency to the locale its amounts are typed and shown in, so a
|
|
250
|
-
* EUR field accepts `1.234,56` and normalizes it to `1234.56`. Mirrors the
|
|
251
|
-
* operator dashboard's `useCurrencyLocale`, reading through the registered
|
|
252
|
-
* client so the shared price editor works in either panel; a panel with no
|
|
253
|
-
* markets answers undefined and callers fall back to the UI language.
|
|
254
|
-
*/
|
|
255
|
-
export function useFormCurrencyLocale(): (currency: string | undefined) => string | undefined {
|
|
256
|
-
const client = getApiClient()
|
|
257
|
-
|
|
258
|
-
const { data } = useQuery({
|
|
259
|
-
queryKey: useResourceKey('markets', 'panel-form'),
|
|
260
|
-
queryFn: async (): Promise<{ data: PanelMarket[] }> =>
|
|
261
|
-
(await client.markets?.list({ limit: 100 })) ?? { data: [] },
|
|
262
|
-
enabled: Boolean(client.markets),
|
|
263
|
-
staleTime: FIVE_MINUTES,
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
const markets = data?.data ?? []
|
|
267
|
-
|
|
268
|
-
return (currency) => {
|
|
269
|
-
if (!currency) return undefined
|
|
270
|
-
|
|
271
|
-
return markets.find((market) => market.currency === currency)?.default_locale ?? undefined
|
|
272
|
-
}
|
|
273
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Store } from '@spree/admin-sdk'
|
|
1
|
+
import type { CurrencyCode, LocaleCode, Store } from '@spree/admin-sdk'
|
|
2
2
|
import { useQuery } from '@tanstack/react-query'
|
|
3
3
|
import { createContext, type ReactNode, useContext, useEffect, useRef } from 'react'
|
|
4
4
|
import { adminClient } from '../client'
|
|
@@ -19,15 +19,15 @@ interface StoreContextValue {
|
|
|
19
19
|
store: Store | null
|
|
20
20
|
storeId: string
|
|
21
21
|
isLoading: boolean
|
|
22
|
-
currencies:
|
|
22
|
+
currencies: CurrencyCode[]
|
|
23
23
|
/** Locales this store is configured to translate content into. */
|
|
24
|
-
locales:
|
|
24
|
+
locales: LocaleCode[]
|
|
25
25
|
/** Every locale a merchant may translate content into — the full canonical
|
|
26
26
|
* set, independent of which locales the store currently uses. Drives locale
|
|
27
27
|
* pickers so a new locale can always be added. */
|
|
28
|
-
availableLocales:
|
|
29
|
-
defaultCurrency:
|
|
30
|
-
defaultLocale:
|
|
28
|
+
availableLocales: LocaleCode[]
|
|
29
|
+
defaultCurrency: CurrencyCode
|
|
30
|
+
defaultLocale: LocaleCode
|
|
31
31
|
/** ISO country the store sells from — used to pre-fill new address forms. */
|
|
32
32
|
defaultCountry: string
|
|
33
33
|
/** IANA timezone for the store (e.g. `Europe/Berlin`). Falls back to the
|
|
@@ -64,7 +64,7 @@ export function StoreProvider({ storeId, children }: { storeId: string; children
|
|
|
64
64
|
// that path already owns the locale via `switchAdminLocale`, and re-applying
|
|
65
65
|
// would race that PATCH and could reload before it lands. Tracking the
|
|
66
66
|
// storeId it last ran for re-arms it per store, so a multi-store admin still
|
|
67
|
-
// inherits each store's
|
|
67
|
+
// inherits each store's admin_locale.
|
|
68
68
|
const localeFallbackStoreRef = useRef<string | null>(null)
|
|
69
69
|
|
|
70
70
|
const query = useQuery({
|
|
@@ -83,7 +83,7 @@ export function StoreProvider({ storeId, children }: { storeId: string; children
|
|
|
83
83
|
|
|
84
84
|
localeFallbackStoreRef.current = storeId
|
|
85
85
|
reconcileStoreDefaultLocale(
|
|
86
|
-
store.
|
|
86
|
+
store.admin_locale,
|
|
87
87
|
storeId,
|
|
88
88
|
accountLocaleRef.current,
|
|
89
89
|
coreLocaleCodes(),
|
|
@@ -99,7 +99,7 @@ export function StoreProvider({ storeId, children }: { storeId: string; children
|
|
|
99
99
|
// A store's zone preference defaults to the host app's `Time.zone.name`,
|
|
100
100
|
// which may be a Rails name ("Eastern Time (US & Canada)") that Intl and
|
|
101
101
|
// date-fns-tz reject — falling back keeps every date control working.
|
|
102
|
-
const timezone = resolveTimezone(store?.
|
|
102
|
+
const timezone = resolveTimezone(store?.timezone)
|
|
103
103
|
|
|
104
104
|
return (
|
|
105
105
|
<StoreContext.Provider
|
|
@@ -2,6 +2,7 @@ import { requiredMessage } from '@spree/dashboard-ui'
|
|
|
2
2
|
import { z } from 'zod/v4'
|
|
3
3
|
import type { PanelStockLocation, PanelStockLocationCreateParams } from '../api-client'
|
|
4
4
|
import { blankToUndefined, emptyToUndefined } from '../lib/form-mappers'
|
|
5
|
+
import { countryCodeSchema } from '../lib/iso-codes'
|
|
5
6
|
|
|
6
7
|
// Labels live in `en.json` under `admin.stock_locations.kinds.*` and
|
|
7
8
|
// `admin.stock_locations.pickup_stock_policies.*`. Consumers map these
|
|
@@ -27,7 +28,7 @@ export const stockLocationFormSchema = z.object({
|
|
|
27
28
|
zipcode: z.string().optional(),
|
|
28
29
|
phone: z.string().optional(),
|
|
29
30
|
company: z.string().optional(),
|
|
30
|
-
country_code: z.
|
|
31
|
+
country_code: countryCodeSchema.or(z.literal('')).optional(),
|
|
31
32
|
state_code: z.string().optional(),
|
|
32
33
|
state_name: z.string().optional(),
|
|
33
34
|
pickup_enabled: z.boolean(),
|
|
@@ -114,7 +115,7 @@ export function formValuesToParams(v: StockLocationFormValues): PanelStockLocati
|
|
|
114
115
|
zipcode: blankToUndefined(v.zipcode),
|
|
115
116
|
phone: blankToUndefined(v.phone),
|
|
116
117
|
company: blankToUndefined(v.company),
|
|
117
|
-
country_code:
|
|
118
|
+
country_code: v.country_code || undefined,
|
|
118
119
|
state_code: blankToUndefined(v.state_code),
|
|
119
120
|
state_name: blankToUndefined(v.state_name),
|
|
120
121
|
pickup_enabled: v.pickup_enabled,
|
|
@@ -8,7 +8,7 @@ defineTable<PanelPackageType>('package-types', {
|
|
|
8
8
|
docsPath: 'settings/package-types',
|
|
9
9
|
description: i18n.t('admin.package_types.description'),
|
|
10
10
|
title: i18n.t('admin.package_types.table_title'),
|
|
11
|
-
searchParam: '
|
|
11
|
+
searchParam: 'search',
|
|
12
12
|
searchPlaceholder: i18n.t('admin.package_types.search_placeholder'),
|
|
13
13
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
14
14
|
emptyIcon: <PackageIcon className="size-8 text-muted-foreground" />,
|
|
@@ -8,7 +8,7 @@ defineTable<PanelStockLocation>('stock-locations', {
|
|
|
8
8
|
docsPath: 'settings/locations',
|
|
9
9
|
description: i18n.t('admin.stock_locations.description'),
|
|
10
10
|
title: i18n.t('admin.settings_nav.items.stock_locations'),
|
|
11
|
-
searchParam: '
|
|
11
|
+
searchParam: 'search',
|
|
12
12
|
searchPlaceholder: i18n.t('admin.stock_locations.search_placeholder'),
|
|
13
13
|
defaultSort: { field: 'name', direction: 'asc' },
|
|
14
14
|
emptyIcon: <WarehouseIcon className="size-8 text-muted-foreground" />,
|
package/src/vite/discover.ts
CHANGED
|
@@ -39,6 +39,17 @@ export interface DashboardPluginManifest {
|
|
|
39
39
|
* Compiled into the host's route tree by `@spree/dashboard/vite`.
|
|
40
40
|
*/
|
|
41
41
|
routesDir?: string
|
|
42
|
+
/**
|
|
43
|
+
* Absolute path to the plugin's directory of signed-in pages outside any
|
|
44
|
+
* store (`"authenticatedRoutes"` in the marker), mounted at the root inside
|
|
45
|
+
* the dashboard's auth guard.
|
|
46
|
+
*/
|
|
47
|
+
authenticatedRoutesDir?: string
|
|
48
|
+
/**
|
|
49
|
+
* Absolute path to the plugin's directory of public pages
|
|
50
|
+
* (`"publicRoutes"` in the marker), mounted at the root with no sign-in.
|
|
51
|
+
*/
|
|
52
|
+
publicRoutesDir?: string
|
|
42
53
|
}
|
|
43
54
|
|
|
44
55
|
/**
|
|
@@ -68,12 +79,14 @@ export function discoverDashboardPluginManifests(
|
|
|
68
79
|
manifests.push({ name })
|
|
69
80
|
continue
|
|
70
81
|
}
|
|
71
|
-
const
|
|
82
|
+
const marker = pkg.spree?.dashboard
|
|
83
|
+
const resolveDir = (dir?: string) =>
|
|
84
|
+
dir ? path.resolve(path.dirname(manifestPath), dir) : undefined
|
|
72
85
|
manifests.push({
|
|
73
86
|
name,
|
|
74
|
-
routesDir: routes
|
|
75
|
-
|
|
76
|
-
|
|
87
|
+
routesDir: resolveDir(marker?.routes),
|
|
88
|
+
authenticatedRoutesDir: resolveDir(marker?.authenticatedRoutes),
|
|
89
|
+
publicRoutesDir: resolveDir(marker?.publicRoutes),
|
|
77
90
|
})
|
|
78
91
|
}
|
|
79
92
|
return manifests
|
|
@@ -128,34 +141,24 @@ export function discoverDashboardPlugins({ root, onWarn }: DiscoverOptions): str
|
|
|
128
141
|
}
|
|
129
142
|
|
|
130
143
|
/**
|
|
131
|
-
*
|
|
132
|
-
* `
|
|
144
|
+
* The routes directory of an installed dashboard shell (`@spree/dashboard`,
|
|
145
|
+
* `@spree/seller-dashboard`), at its real location. The shell's `.` export
|
|
146
|
+
* maps to a file in `src/`, so `routes/` is its sibling directory.
|
|
133
147
|
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* and
|
|
140
|
-
*
|
|
148
|
+
* The route generator imports these files from the host's `routeTree.gen.ts`.
|
|
149
|
+
* Under pnpm a package is reached through a `node_modules/<name>` link, while
|
|
150
|
+
* its own dependencies sit next to its real location under
|
|
151
|
+
* `node_modules/.pnpm/`. TypeScript does not follow the link for a relative
|
|
152
|
+
* import, so a route file imported through it cannot see the shell's
|
|
153
|
+
* dependencies and the host's type check fails. Plugin routes directories
|
|
154
|
+
* resolve to their real location for the same reason.
|
|
141
155
|
*
|
|
142
156
|
* @param root host project root
|
|
143
|
-
* @param packageName the package the
|
|
144
|
-
* @param resolvedPath a path inside that package, as Node resolved it
|
|
145
|
-
* @returns the same path through the link, or `resolvedPath` when no link
|
|
146
|
-
* leads to it (the package resolving itself, an unusual layout)
|
|
157
|
+
* @param packageName the shell package the host depends on
|
|
147
158
|
*/
|
|
148
|
-
export function
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
const link = path.join(dir, 'node_modules', packageName)
|
|
152
|
-
if (fs.existsSync(path.join(link, 'package.json'))) {
|
|
153
|
-
const inside = path.relative(fs.realpathSync(link), realPath)
|
|
154
|
-
if (inside.startsWith('..') || path.isAbsolute(inside)) return resolvedPath
|
|
155
|
-
return path.join(link, inside)
|
|
156
|
-
}
|
|
157
|
-
if (path.dirname(dir) === dir) return resolvedPath
|
|
158
|
-
}
|
|
159
|
+
export function resolveShellRoutesDir(root: string, packageName: string): string {
|
|
160
|
+
const require = createRequire(path.join(root, 'package.json'))
|
|
161
|
+
return path.join(path.dirname(fs.realpathSync(require.resolve(packageName))), 'routes')
|
|
159
162
|
}
|
|
160
163
|
|
|
161
164
|
interface HostManifest {
|
|
@@ -168,6 +171,8 @@ interface PluginManifest {
|
|
|
168
171
|
dashboard?: {
|
|
169
172
|
plugin?: boolean
|
|
170
173
|
routes?: string
|
|
174
|
+
authenticatedRoutes?: string
|
|
175
|
+
publicRoutes?: string
|
|
171
176
|
}
|
|
172
177
|
}
|
|
173
178
|
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { useMutation } from '@tanstack/react-query'
|
|
2
|
-
import SparkMD5 from 'spark-md5'
|
|
3
|
-
import { getApiClient } from '../api-client'
|
|
4
|
-
|
|
5
|
-
function computeMD5Checksum(file: File): Promise<string> {
|
|
6
|
-
return new Promise((resolve, reject) => {
|
|
7
|
-
const chunkSize = 2097152 // 2MB chunks
|
|
8
|
-
const spark = new SparkMD5.ArrayBuffer()
|
|
9
|
-
const reader = new FileReader()
|
|
10
|
-
let currentChunk = 0
|
|
11
|
-
const chunks = Math.ceil(file.size / chunkSize)
|
|
12
|
-
|
|
13
|
-
reader.onload = (e) => {
|
|
14
|
-
spark.append(e.target!.result as ArrayBuffer)
|
|
15
|
-
currentChunk++
|
|
16
|
-
|
|
17
|
-
if (currentChunk < chunks) {
|
|
18
|
-
loadNext()
|
|
19
|
-
} else {
|
|
20
|
-
// Active Storage expects base64-encoded raw MD5 digest
|
|
21
|
-
resolve(btoa(spark.end(true)))
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
reader.onerror = () => reject(reader.error)
|
|
26
|
-
|
|
27
|
-
function loadNext() {
|
|
28
|
-
const start = currentChunk * chunkSize
|
|
29
|
-
const end = Math.min(start + chunkSize, file.size)
|
|
30
|
-
reader.readAsArrayBuffer(file.slice(start, end))
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
loadNext()
|
|
34
|
-
})
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface UploadResult {
|
|
38
|
-
signedId: string
|
|
39
|
-
previewUrl: string
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function sameOriginIfRailsDisk(url: string): string {
|
|
43
|
-
try {
|
|
44
|
-
const parsed = new URL(url)
|
|
45
|
-
if (parsed.pathname.startsWith('/rails/active_storage/')) {
|
|
46
|
-
return parsed.pathname + parsed.search
|
|
47
|
-
}
|
|
48
|
-
return url
|
|
49
|
-
} catch {
|
|
50
|
-
return url
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
interface DirectUploadOptions {
|
|
55
|
-
/**
|
|
56
|
-
* Upload to private storage instead of the public bucket. Required for files
|
|
57
|
-
* that are only ever served through an authorized, signed link — attaching a
|
|
58
|
-
* blob never moves it between services, so this has to be decided up front.
|
|
59
|
-
*/
|
|
60
|
-
private?: boolean
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function useDirectUpload(options: DirectUploadOptions = {}) {
|
|
64
|
-
return useMutation({
|
|
65
|
-
mutationFn: async (file: File): Promise<UploadResult> => {
|
|
66
|
-
const checksum = await computeMD5Checksum(file)
|
|
67
|
-
|
|
68
|
-
// Step 1: Get presigned upload URL
|
|
69
|
-
let response: {
|
|
70
|
-
direct_upload: { url: string; headers: Record<string, string> }
|
|
71
|
-
signed_id: string
|
|
72
|
-
}
|
|
73
|
-
try {
|
|
74
|
-
const presign = getApiClient().createDirectUpload
|
|
75
|
-
if (!presign) throw new Error('This panel cannot upload files.')
|
|
76
|
-
|
|
77
|
-
response = await presign({
|
|
78
|
-
blob: {
|
|
79
|
-
filename: file.name,
|
|
80
|
-
byte_size: file.size,
|
|
81
|
-
checksum,
|
|
82
|
-
content_type: file.type || 'application/octet-stream',
|
|
83
|
-
},
|
|
84
|
-
...(options.private ? { private: true } : {}),
|
|
85
|
-
})
|
|
86
|
-
} catch (err) {
|
|
87
|
-
throw new Error(`Presign failed: ${err instanceof Error ? err.message : err}`)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Step 2: Upload file directly to storage.
|
|
91
|
-
//
|
|
92
|
-
// Active Storage's Disk service issues an absolute URL against the Rails
|
|
93
|
-
// origin (e.g. http://localhost:3000/rails/active_storage/disk/...). In
|
|
94
|
-
// dev the SPA runs on a different port and that controller doesn't speak
|
|
95
|
-
// CORS, so the browser blocks the cross-origin PUT before any response
|
|
96
|
-
// ("Failed to fetch"). When the URL points at a `/rails/active_storage/`
|
|
97
|
-
// path we drop the host so the request goes through Vite's `/rails`
|
|
98
|
-
// proxy and stays same-origin. Production S3 URLs are untouched.
|
|
99
|
-
const uploadUrl = sameOriginIfRailsDisk(response.direct_upload.url)
|
|
100
|
-
const uploadResponse = await fetch(uploadUrl, {
|
|
101
|
-
method: 'PUT',
|
|
102
|
-
headers: response.direct_upload.headers,
|
|
103
|
-
body: file,
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
if (!uploadResponse.ok) {
|
|
107
|
-
const text = await uploadResponse.text().catch(() => '')
|
|
108
|
-
throw new Error(`Storage upload failed (${uploadResponse.status}): ${text}`)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
signedId: response.signed_id,
|
|
113
|
-
previewUrl: URL.createObjectURL(file),
|
|
114
|
-
}
|
|
115
|
-
},
|
|
116
|
-
})
|
|
117
|
-
}
|
|
File without changes
|
|
File without changes
|