medusa-variant-images-updated
Version:
A variant images plugin for Medusa V2.
323 lines (322 loc) • 12.5 kB
JavaScript
import { jsxs, Fragment, jsx } from "react/jsx-runtime";
import { useFieldArray, Controller } from "react-hook-form";
import FileUploadField from "./components/FileUploadField.js";
import { CheckCircleSolid, CircleDottedLine } from "@medusajs/icons";
import { clx } from "@medusajs/ui";
import { useState, useEffect, useRef } from "react";
const VariantsImagesMediaForm = ({ form, type, onDeleteImage }) => {
const { control, path, setValue, getValues, watch } = form;
const { fields, append, remove } = useFieldArray({
control,
name: path("images")
});
const [hasNewUploads, setHasNewUploads] = useState(false);
const [keyActionPerformed, setKeyActionPerformed] = useState(false);
const [selectionCounter, setSelectionCounter] = useState(1);
const imagesInfo = watch(path("images")) || [];
const hasSelectedImages = imagesInfo.some((img) => img.info.selected);
const isThumbnailSet = imagesInfo.some((img) => img.type === "thumbnail");
const getThumbnailImage = (images) => images.find((img) => img.type === "thumbnail");
useEffect(() => {
const handleGlobalKeyDown = (e) => {
if (e.target && e.target.tagName === "INPUT") return;
if (e.key.toLowerCase() === "d") {
e.preventDefault();
const currentImages = getValues(path("images"));
const toDeleteIndices = [];
const toDeleteImages = [];
currentImages.forEach((image, i) => {
var _a;
if ((_a = image == null ? void 0 : image.info) == null ? void 0 : _a.selected) {
toDeleteIndices.push(i);
toDeleteImages.push(image);
}
});
if (toDeleteIndices.length > 0) {
toDeleteIndices.sort((a, b) => b - a).forEach((i) => remove(i));
toDeleteImages.forEach((image) => {
if (typeof image.id === "number" && Number.isInteger(image.id)) {
onDeleteImage(image);
}
});
const remainingImages = getValues(path("images"));
const prevThumbnail = getThumbnailImage(currentImages);
let clearedSelection = remainingImages.map((img) => ({
...img,
info: {
...img.info,
selected: false,
selectedNumber: -1
}
}));
if (prevThumbnail && clearedSelection.some((img) => img.url === prevThumbnail.url)) {
clearedSelection = clearedSelection.map(
(img) => img.url === prevThumbnail.url ? { ...img, type: "thumbnail" } : { ...img, type: void 0 }
);
} else {
clearedSelection = clearedSelection.map((img) => ({
...img,
type: void 0
}));
}
setValue(path("images"), clearedSelection, { shouldDirty: true });
setKeyActionPerformed(true);
}
}
if (type === "media" && e.key.toLowerCase() === "t") {
e.preventDefault();
const currentImages = getValues(path("images"));
const selectedImages = currentImages.map((img, idx) => ({ ...img, idx })).filter((img) => img.info.selected && img.info.selectedNumber > 0);
if (selectedImages.length > 0) {
selectedImages.sort((a, b) => a.info.selectedNumber - b.info.selectedNumber);
const firstSelectedIdx = selectedImages[0].idx;
const updatedImages = currentImages.map((img, i) => ({
...img,
type: i === firstSelectedIdx ? "thumbnail" : "media",
info: {
...img.info,
selected: i === firstSelectedIdx,
selectedNumber: i === firstSelectedIdx ? img.info.selectedNumber : -1
}
}));
setValue(path("images"), updatedImages, { shouldDirty: true });
setKeyActionPerformed(true);
}
}
};
document.addEventListener("keydown", handleGlobalKeyDown);
return () => document.removeEventListener("keydown", handleGlobalKeyDown);
}, [getValues, path, remove, setValue, onDeleteImage, type]);
const handleFilesChosen = (files) => {
if (files.length) {
const toAppend = files.map((file) => ({
url: URL.createObjectURL(file),
name: file.name,
size: file.size,
nativeFile: file,
info: {
selected: false,
selectedNumber: -1
}
}));
const currentImages = getValues(path("images"));
const prevThumbnail = getThumbnailImage(currentImages);
append(toAppend);
setHasNewUploads(true);
setTimeout(() => {
const updatedImages = getValues(path("images"));
if (prevThumbnail && updatedImages.some((img) => img.url === prevThumbnail.url)) {
setValue(
path("images"),
updatedImages.map(
(img) => img.url === prevThumbnail.url ? { ...img, type: "thumbnail" } : { ...img, type: void 0 }
),
{ shouldDirty: true }
);
}
}, 0);
}
};
const toggleSelected = (index) => {
const currentImages = getValues(path("images"));
const updatedImages = currentImages.map((img, i) => {
if (i === index) {
const isSelecting = !img.info.selected;
return {
...img,
info: {
...img.info,
selected: isSelecting,
selectedNumber: isSelecting ? selectionCounter : -1
}
};
}
if (type === "thumbnail" && i !== index) {
return {
...img,
info: {
...img.info,
selected: false,
selectedNumber: -1
}
};
}
return img;
});
setValue(path("images"), updatedImages, { shouldDirty: true });
if (updatedImages[index].info.selected) {
setSelectionCounter(selectionCounter + 1);
if (type === "thumbnail") {
setKeyActionPerformed(true);
}
} else if (type === "thumbnail" && !updatedImages.some((img) => img.info.selected)) {
setKeyActionPerformed(false);
}
};
const handleRemoveImage = (index, image) => {
const currentImages = getValues(path("images"));
const prevThumbnail = getThumbnailImage(currentImages);
remove(index);
if (typeof image.id === "number" && Number.isInteger(image.id)) {
onDeleteImage(image);
}
setTimeout(() => {
const updatedImages = getValues(path("images"));
if (prevThumbnail && updatedImages.some((img) => img.url === prevThumbnail.url)) {
setValue(
path("images"),
updatedImages.map(
(img) => img.url === prevThumbnail.url ? { ...img, type: "thumbnail" } : { ...img, type: void 0 }
),
{ shouldDirty: true }
);
} else {
setValue(
path("images"),
updatedImages.map((img) => ({
...img,
type: void 0
})),
{ shouldDirty: true }
);
}
}, 0);
};
const canSaveAndClose = type === "thumbnail" ? hasSelectedImages || hasNewUploads || keyActionPerformed : isThumbnailSet || hasSelectedImages || hasNewUploads || keyActionPerformed;
return /* @__PURE__ */ jsxs(Fragment, { children: [
/* @__PURE__ */ jsxs("div", { className: "flex size-full flex-col-reverse lg:grid lg:grid-cols-[1fr_560px]", children: [
fields.length > 0 ? /* @__PURE__ */ jsxs("div", { className: "bg-ui-bg-subtle size-full overflow-auto divide-y", children: [
/* @__PURE__ */ jsxs("div", { className: "p-6 flex flex-col space-y-1", children: [
/* @__PURE__ */ jsx("h2", { children: "Uploads" }),
/* @__PURE__ */ jsx("p", { className: "txt-small text-ui-fg-subtle", children: type === "thumbnail" ? /* @__PURE__ */ jsx("span", { children: "Select an image to use as the variant thumbnail." }) : /* @__PURE__ */ jsx("span", { children: "Select images to use as the variant images." }) })
] }),
/* @__PURE__ */ jsx("div", { className: "grid h-fit auto-rows-auto grid-cols-[repeat(auto-fill,minmax(150px,1fr))] gap-6 p-6", children: fields.map((field, index) => /* @__PURE__ */ jsx(
Image,
{
image: field,
index,
form,
onSelected: () => {
},
onRemove: handleRemoveImage,
type,
toggleSelected
},
field.id || field.url
)) })
] }) : /* @__PURE__ */ jsx("div", { className: "w-[70%]" }),
/* @__PURE__ */ jsx("div", { className: "bg-ui-bg-base overflow-auto border-b px-6 py-4 lg:border-b-0 lg:border-l", children: /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-2", children: [
/* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-y-1", children: [
/* @__PURE__ */ jsx("h2", { children: "Media" }),
/* @__PURE__ */ jsx("p", { className: "txt-small text-ui-fg-subtle", children: "Add images to your product media." })
] }),
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(
FileUploadField,
{
onFileChosen: handleFilesChosen,
placeholder: "Drag and drop images, or click to upload.",
multiple: true,
filetypes: ["image/gif", "image/jpeg", "image/png", "image/webp"],
className: "py-large"
}
) })
] }) })
] }),
/* @__PURE__ */ jsx("div", { className: "mt-4 flex justify-end", children: /* @__PURE__ */ jsx(
"button",
{
type: "button",
disabled: !canSaveAndClose,
className: clx(
"px-6 py-2 rounded font-semibold",
canSaveAndClose ? "bg-blue-600 text-white hover:bg-blue-700" : "bg-gray-300 text-gray-500 cursor-not-allowed"
),
onClick: () => {
setHasNewUploads(false);
setKeyActionPerformed(false);
},
children: "Save and Close"
}
) })
] });
};
const Image = ({
image,
index,
form,
onSelected,
onRemove,
type,
toggleSelected
}) => {
const { control, path } = form;
const buttonRef = useRef(null);
const handleKeyDown = (e) => {
if (e.key.toLowerCase() === "d") {
e.preventDefault();
onRemove(index, image);
}
if (e.key.toLowerCase() === "t") {
e.preventDefault();
toggleSelected(index);
}
};
return /* @__PURE__ */ jsx(
Controller,
{
name: path(`images.${index}.info`),
control,
render: ({ field: { value } }) => /* @__PURE__ */ jsxs(
"button",
{
ref: buttonRef,
className: clx(
"relative shadow-elevation-card-rest hover:shadow-elevation-card-hover focus-visible:shadow-borders-focus bg-ui-bg-subtle-hover group aspect-square h-auto max-w-full overflow-hidden rounded-lg outline-none",
{
"bg-grey-500": value.selected
}
),
type: "button",
tabIndex: 0,
onClick: () => {
toggleSelected(index);
onSelected(index);
},
onKeyDown: handleKeyDown,
children: [
(image == null ? void 0 : image.type) === "thumbnail" && /* @__PURE__ */ jsx("span", { className: "absolute top-2 left-2 px-2 py-1 rounded bg-yellow-400 text-xs font-bold z-10", children: "Thumbnail" }),
/* @__PURE__ */ jsx(
"img",
{
src: image.url,
alt: image.name || "Uploaded image",
className: clx(
"rounded-lg object-cover bg-clip-border w-full h-full duration-300 brightness-100",
{
"brightness-50": value.selected
}
)
}
),
/* @__PURE__ */ jsx(
"span",
{
className: clx(
"hidden group-hover:block absolute top-2 right-2 mix-blend-difference",
{
"block mix-blend-normal": value.selected,
"bg-neutral-950 flex rounded-full group-hover:flex flex-row items-center w-10 h-[19px] px-0.5": value.selected && type === "media"
}
),
children: value.selected ? /* @__PURE__ */ jsx(CheckCircleSolid, { className: "text-green-300" }) : /* @__PURE__ */ jsx(CircleDottedLine, {})
}
)
]
}
)
}
);
};
export {
VariantsImagesMediaForm as default
};