feat: Add support for uploading media in admin (#7564)

This commit is contained in:
Stevche Radevski
2024-05-31 13:11:34 +02:00
committed by GitHub
parent 11528526fa
commit cec9af1b80
11 changed files with 106 additions and 20 deletions

View File

@@ -210,6 +210,7 @@
"uploadImagesLabel": "Upload images",
"uploadImagesHint": "Drag and drop images here or click to upload.",
"invalidFileType": "'{{name}}' is not a supported file type. Supported file types are: {{types}}.",
"failedToUpload": "Failed to upload the added media. Please try again.",
"deleteWarning_one": "You are about to delete {{count}} image. This action cannot be undone.",
"deleteWarning_other": "You are about to delete {{count}} images. This action cannot be undone.",
"deleteWarningWithThumbnail_one": "You are about to delete {{count}} image including the thumbnail. This action cannot be undone.",

View File

@@ -1,7 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { CheckMini, Spinner, ThumbnailBadge } from "@medusajs/icons"
import { Image, Product } from "@medusajs/medusa"
import { Button, CommandBar, Tooltip, clx } from "@medusajs/ui"
import { Button, CommandBar, Tooltip, clx, toast } from "@medusajs/ui"
import { AnimatePresence, motion } from "framer-motion"
import { Fragment, useCallback, useState } from "react"
import { useFieldArray, useForm } from "react-hook-form"
@@ -19,6 +19,7 @@ import {
useRouteModal,
} from "../../../../../components/route-modal"
import { useUpdateProduct } from "../../../../../hooks/api/products"
import { sdk } from "../../../../../lib/client"
type ProductMediaViewProps = {
product: Product
@@ -73,7 +74,7 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
keyName: "field_id",
})
const { mutateAsync, isLoading } = useUpdateProduct(product.id)
const { mutateAsync, isPending } = useUpdateProduct(product.id)
const handleSubmit = form.handleSubmit(async ({ media }) => {
const urls = media.map((m) => m.url)
@@ -85,20 +86,17 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
if (filesToUpload.length) {
const files = filesToUpload.map((m) => m.file) as File[]
// TODO: Implement upload to Medusa
// const uploads = await client.admin.uploads
// .create(files)
// .then((res) => {
// return res.uploads
// })
// .catch((_err) => {
// // Show error message
// return null
// })
const uploads = files.map((file) => ({
url: URL.createObjectURL(file),
}))
const uploads = await sdk.admin.uploads
.create({ files })
.then((res) => {
return res.files
})
.catch(() => {
form.setError("media", {
type: "invalid_file",
message: t("products.media.failedToUpload"),
})
})
if (!uploads) {
return
@@ -116,7 +114,7 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
await mutateAsync(
{
images: urls,
images: urls.map((url) => ({ url })),
// Set thumbnail to empty string if no thumbnail is selected, as the API does not accept null
thumbnail: thumbnail || "",
},
@@ -216,7 +214,7 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
{t("products.media.galleryLabel")}
</Link>
</Button>
<Button size="small" type="submit" isLoading={isLoading}>
<Button size="small" type="submit" isLoading={isPending}>
{t("actions.save")}
</Button>
</div>

View File

@@ -314,4 +314,65 @@ export class Admin {
)
},
}
public uploads = {
// Note: The creation/upload flow be made more advanced, with support for streaming and progress, but for now we keep it simple
create: async (
body: HttpTypes.AdminUploadFile,
query?: SelectParams,
headers?: ClientHeaders
) => {
const form = new FormData()
if (body instanceof FileList) {
Array.from(body).forEach((file) => {
form.append("files", file)
})
} else {
body.files.forEach((file) => {
form.append(
"files",
"content" in file
? new Blob([file.content], {
type: "text/plain",
})
: file,
file.name
)
})
}
return this.client.fetch<{ files: HttpTypes.AdminFile[] }>(
`/admin/uploads`,
{
method: "POST",
headers: {
...headers,
// Let the browser determine the content type.
"content-type": null,
},
body: form,
query,
}
)
},
retrieve: async (
id: string,
query?: SelectParams,
headers?: ClientHeaders
) => {
return this.client.fetch<{ file: HttpTypes.AdminFile }>(
`/admin/uploads/${id}`,
{
query,
headers,
}
)
},
delete: async (id: string, headers?: ClientHeaders) => {
return this.client.fetch<DeleteResponse<"file">>(`/admin/uploads/${id}`, {
method: "DELETE",
headers,
})
},
}
}

View File

@@ -164,7 +164,11 @@ export class Client {
}
// We use `headers.set` in order to ensure headers are overwritten in a case-insensitive manner.
Object.entries(customHeaders).forEach(([key, value]) => {
headers.set(key, value)
if (value === null) {
headers.delete(key)
} else {
headers.set(key, value)
}
})
let normalizedInput: RequestInfo | URL = input

View File

@@ -23,7 +23,7 @@ export type FetchParams = Parameters<typeof fetch>
export type ClientHeaders =
// The `tags` header is specifically added for nextJS, as they follow a non-standard header format
Record<string, string | { tags: string[] }>
Record<string, string | null | { tags: string[] }>
export type FetchInput = FetchParams[0]

View File

@@ -0,0 +1,5 @@
import { BaseFile, BaseUploadFile } from "./common"
export interface AdminFile extends BaseFile {}
export type AdminUploadFile = BaseUploadFile

View File

@@ -0,0 +1,10 @@
export interface BaseFile {
id: string
url: string
}
export type BaseUploadFile =
| {
files: ({ name: string; content: string } | File)[]
}
| FileList

View File

@@ -0,0 +1,2 @@
export * from "./admin"
export * from "./store"

View File

@@ -0,0 +1,3 @@
import { BaseFile } from "./common"
export interface StoreFile extends BaseFile {}

View File

@@ -19,3 +19,4 @@ export * from "./sales-channel"
export * from "./stock-locations"
export * from "./tax"
export * from "./user"
export * from "./file"

View File

@@ -99,6 +99,7 @@ export class S3FileService extends AbstractFileProviderService {
// protected private_secret_access_key_: string
// ACL: options.acl ?? (options.isProtected ? "private" : "public-read"),
ACL: "public-read",
Bucket: this.config_.bucket,
Body: content,
Key: fileKey,