diff --git a/packages/admin-next/dashboard/src/i18n/translations/en.json b/packages/admin-next/dashboard/src/i18n/translations/en.json index af7c299d4c..14a3d8dd26 100644 --- a/packages/admin-next/dashboard/src/i18n/translations/en.json +++ b/packages/admin-next/dashboard/src/i18n/translations/en.json @@ -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.", diff --git a/packages/admin-next/dashboard/src/routes/products/product-media/components/edit-product-media-form/edit-product-media-form.tsx b/packages/admin-next/dashboard/src/routes/products/product-media/components/edit-product-media-form/edit-product-media-form.tsx index 878e97e2df..55f05f9a0c 100644 --- a/packages/admin-next/dashboard/src/routes/products/product-media/components/edit-product-media-form/edit-product-media-form.tsx +++ b/packages/admin-next/dashboard/src/routes/products/product-media/components/edit-product-media-form/edit-product-media-form.tsx @@ -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")} - diff --git a/packages/core/js-sdk/src/admin/index.ts b/packages/core/js-sdk/src/admin/index.ts index d4fc596479..212165322a 100644 --- a/packages/core/js-sdk/src/admin/index.ts +++ b/packages/core/js-sdk/src/admin/index.ts @@ -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>(`/admin/uploads/${id}`, { + method: "DELETE", + headers, + }) + }, + } } diff --git a/packages/core/js-sdk/src/client.ts b/packages/core/js-sdk/src/client.ts index bc2e90dbbc..fd12db8634 100644 --- a/packages/core/js-sdk/src/client.ts +++ b/packages/core/js-sdk/src/client.ts @@ -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 diff --git a/packages/core/js-sdk/src/types.ts b/packages/core/js-sdk/src/types.ts index 4e47683638..f9eb72a530 100644 --- a/packages/core/js-sdk/src/types.ts +++ b/packages/core/js-sdk/src/types.ts @@ -23,7 +23,7 @@ export type FetchParams = Parameters export type ClientHeaders = // The `tags` header is specifically added for nextJS, as they follow a non-standard header format - Record + Record export type FetchInput = FetchParams[0] diff --git a/packages/core/types/src/http/file/admin.ts b/packages/core/types/src/http/file/admin.ts new file mode 100644 index 0000000000..911e00bc11 --- /dev/null +++ b/packages/core/types/src/http/file/admin.ts @@ -0,0 +1,5 @@ +import { BaseFile, BaseUploadFile } from "./common" + +export interface AdminFile extends BaseFile {} + +export type AdminUploadFile = BaseUploadFile diff --git a/packages/core/types/src/http/file/common.ts b/packages/core/types/src/http/file/common.ts new file mode 100644 index 0000000000..0d87af9cb2 --- /dev/null +++ b/packages/core/types/src/http/file/common.ts @@ -0,0 +1,10 @@ +export interface BaseFile { + id: string + url: string +} + +export type BaseUploadFile = + | { + files: ({ name: string; content: string } | File)[] + } + | FileList diff --git a/packages/core/types/src/http/file/index.ts b/packages/core/types/src/http/file/index.ts new file mode 100644 index 0000000000..3bd2bd2cc0 --- /dev/null +++ b/packages/core/types/src/http/file/index.ts @@ -0,0 +1,2 @@ +export * from "./admin" +export * from "./store" diff --git a/packages/core/types/src/http/file/store.ts b/packages/core/types/src/http/file/store.ts new file mode 100644 index 0000000000..8afd5d3058 --- /dev/null +++ b/packages/core/types/src/http/file/store.ts @@ -0,0 +1,3 @@ +import { BaseFile } from "./common" + +export interface StoreFile extends BaseFile {} diff --git a/packages/core/types/src/http/index.ts b/packages/core/types/src/http/index.ts index 253e909b72..92e9a58f0f 100644 --- a/packages/core/types/src/http/index.ts +++ b/packages/core/types/src/http/index.ts @@ -19,3 +19,4 @@ export * from "./sales-channel" export * from "./stock-locations" export * from "./tax" export * from "./user" +export * from "./file" diff --git a/packages/modules/providers/file-s3/src/services/s3-file.ts b/packages/modules/providers/file-s3/src/services/s3-file.ts index aed705203f..a1a6e005ac 100644 --- a/packages/modules/providers/file-s3/src/services/s3-file.ts +++ b/packages/modules/providers/file-s3/src/services/s3-file.ts @@ -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,