[WEB-310] dev: private bucket implementation (#5793)
* chore: migrations and backmigration to move attachments to file asset * chore: move attachments to file assets * chore: update migration file to include created by and updated by and size * chore: remove uninmport errors * chore: make size as float field * fix: file asset uploads * chore: asset uploads migration changes * chore: v2 assets endpoint * chore: remove unused imports * chore: issue attachments * chore: issue attachments * chore: workspace logo endpoints * chore: private bucket changes * chore: user asset endpoint * chore: add logo_url validation * chore: cover image urlk * chore: change asset max length * chore: pages endpoint * chore: store the storage_metadata only when none * chore: attachment asset apis * chore: update create private bucket * chore: make bucket private * chore: fix response of user uploads * fix: response of user uploads * fix: job to fix file asset uploads * fix: user asset endpoints * chore: avatar for user profile * chore: external apis user url endpoint * chore: upload workspace and user asset actions updated * chore: analytics endpoint * fix: analytics export * chore: avatar urls * chore: update user avatar instances * chore: avatar urls for assignees and creators * chore: bucket permission script * fix: all user avatr instances in the web app * chore: update project cover image logic * fix: issue attachment endpoint * chore: patch endpoint for issue attachment * chore: attachments * chore: change attachment storage class * chore: update issue attachment endpoints * fix: issue attachment * chore: update issue attachment implementation * chore: page asset endpoints * fix: web build errors * chore: attachments * chore: page asset urls * chore: comment and issue asset endpoints * chore: asset endpoints * chore: attachment endpoints * chore: bulk asset endpoint * chore: restore endpoint * chore: project assets endpoints * chore: asset url * chore: add delete asset endpoints * chore: fix asset upload endpoint * chore: update patch endpoints * chore: update patch endpoint * chore: update editor image handling * chore: asset restore endpoints * chore: avatar url for space assets * chore: space app assets migration * fix: space app urls * chore: space endpoints * fix: old editor images rendering logic * fix: issue archive and attachment activity * chore: asset deletes * chore: attachment delete * fix: issue attachment * fix: issue attachment get * chore: cover image url for projects * chore: remove duplicate py file * fix: url check function * chore: chore project cover asset delete * fix: migrations * chore: delete migration files * chore: update bucket * fix: build errors * chore: add asset url in intake attachment * chore: project cover fix * chore: update next.config * chore: delete old workspace logos * chore: workspace assets * chore: asset get for space * chore: update project modal * chore: remove unused imports * fix: space app editor helper * chore: update rich-text read-only editor * chore: create multiple column for entity identifiers * chore: update migrations * chore: remove entity identifier * fix: issue assets * chore: update maximum file size logic * chore: update editor max file size logic * fix: close modal after removing workspace logo * chore: update uploaded asstes' status post issue creation * chore: added file size limit to the space app * dev: add file size limit restriction on all endpoints * fix: remove old workspace logo and user avatar --------- Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
This commit is contained in:
parent
c9580ab794
commit
7e334203f1
241 changed files with 5326 additions and 2518 deletions
|
|
@ -7,16 +7,18 @@ import { useParams } from "next/navigation";
|
|||
import { useDropzone } from "react-dropzone";
|
||||
import { Control, Controller } from "react-hook-form";
|
||||
import useSWR from "swr";
|
||||
// headless ui
|
||||
import { Tab, Popover } from "@headlessui/react";
|
||||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/helpers";
|
||||
// plane types
|
||||
import { EFileAssetType } from "@plane/types/src/enums";
|
||||
// ui
|
||||
import { Button, Input, Loader } from "@plane/ui";
|
||||
// constants
|
||||
import { MAX_FILE_SIZE } from "@/constants/common";
|
||||
import { MAX_STATIC_FILE_SIZE } from "@/constants/common";
|
||||
// helpers
|
||||
import { getFileURL } from "@/helpers/file.helper";
|
||||
// hooks
|
||||
import { useWorkspace, useInstance } from "@/hooks/store";
|
||||
import { useDropdownKeyDown } from "@/hooks/use-dropdown-key-down";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
|
|
@ -44,13 +46,14 @@ type Props = {
|
|||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
isProfileCover?: boolean;
|
||||
projectId?: string | null;
|
||||
};
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
||||
const { label, value, control, onChange, disabled = false, tabIndex, isProfileCover = false } = props;
|
||||
const { label, value, control, onChange, disabled = false, tabIndex, isProfileCover = false, projectId } = props;
|
||||
// states
|
||||
const [image, setImage] = useState<File | null>(null);
|
||||
const [isImageUploading, setIsImageUploading] = useState(false);
|
||||
|
|
@ -63,9 +66,6 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
|||
const ref = useRef<HTMLDivElement>(null);
|
||||
// router params
|
||||
const { workspaceSlug } = useParams();
|
||||
// store hooks
|
||||
const { config } = useInstance();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
|
||||
const { data: unsplashImages, error: unsplashError } = useSWR(
|
||||
`UNSPLASH_IMAGES_${searchParams}`,
|
||||
|
|
@ -92,52 +92,42 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
|||
accept: {
|
||||
"image/*": [".png", ".jpg", ".jpeg", ".webp"],
|
||||
},
|
||||
maxSize: config?.file_size_limit ?? MAX_FILE_SIZE,
|
||||
maxSize: MAX_STATIC_FILE_SIZE,
|
||||
});
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!image) return;
|
||||
setIsImageUploading(true);
|
||||
|
||||
if (!image) return;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("asset", image);
|
||||
formData.append("attributes", JSON.stringify({}));
|
||||
|
||||
const oldValue = value;
|
||||
const isUnsplashImage = oldValue?.split("/")[2] === "images.unsplash.com";
|
||||
|
||||
const uploadCallback = (res: any) => {
|
||||
const imageUrl = res.asset;
|
||||
onChange(imageUrl);
|
||||
const uploadCallback = (url: string) => {
|
||||
onChange(url);
|
||||
setIsImageUploading(false);
|
||||
setImage(null);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
if (isProfileCover) {
|
||||
fileService
|
||||
.uploadUserFile(formData)
|
||||
.then((res) => {
|
||||
uploadCallback(res);
|
||||
if (isUnsplashImage) return;
|
||||
if (oldValue && currentWorkspace) fileService.deleteUserFile(oldValue);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
await fileService
|
||||
.uploadUserAsset(
|
||||
{
|
||||
entity_identifier: "",
|
||||
entity_type: EFileAssetType.USER_COVER,
|
||||
},
|
||||
image
|
||||
)
|
||||
.then((res) => uploadCallback(res.asset_url));
|
||||
} else {
|
||||
if (!workspaceSlug) return;
|
||||
fileService
|
||||
.uploadFile(workspaceSlug.toString(), formData)
|
||||
.then((res) => {
|
||||
uploadCallback(res);
|
||||
if (isUnsplashImage) return;
|
||||
if (oldValue && currentWorkspace) fileService.deleteFile(currentWorkspace.id, oldValue);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
});
|
||||
await fileService
|
||||
.uploadWorkspaceAsset(
|
||||
workspaceSlug.toString(),
|
||||
{
|
||||
entity_identifier: projectId?.toString() ?? "",
|
||||
entity_type: EFileAssetType.PROJECT_COVER,
|
||||
},
|
||||
image
|
||||
)
|
||||
.then((res) => uploadCallback(res.asset_url));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -332,7 +322,7 @@ export const ImagePickerPopover: React.FC<Props> = observer((props) => {
|
|||
<Image
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
src={image ? URL.createObjectURL(image) : value ? value : ""}
|
||||
src={image ? URL.createObjectURL(image) : value ? (getFileURL(value) ?? "") : ""}
|
||||
alt="image"
|
||||
className="rounded-lg"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import React, { useEffect, useState, useRef, Fragment, Ref } from "react";
|
||||
import { Placement } from "@popperjs/core";
|
||||
import { useParams } from "next/navigation";
|
||||
import { Controller, useForm } from "react-hook-form"; // services
|
||||
import { usePopper } from "react-popper";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
|
@ -23,6 +22,8 @@ type Props = {
|
|||
prompt?: string;
|
||||
button: JSX.Element;
|
||||
className?: string;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
type FormData = {
|
||||
|
|
@ -33,7 +34,18 @@ type FormData = {
|
|||
const aiService = new AIService();
|
||||
|
||||
export const GptAssistantPopover: React.FC<Props> = (props) => {
|
||||
const { isOpen, handleClose, onResponse, onError, placement, prompt, button, className = "" } = props;
|
||||
const {
|
||||
isOpen,
|
||||
handleClose,
|
||||
onResponse,
|
||||
onError,
|
||||
placement,
|
||||
prompt,
|
||||
button,
|
||||
className = "",
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
} = props;
|
||||
// states
|
||||
const [response, setResponse] = useState("");
|
||||
const [invalidResponse, setInvalidResponse] = useState(false);
|
||||
|
|
@ -41,8 +53,6 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
|||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
const editorRef = useRef<any>(null);
|
||||
const responseRef = useRef<any>(null);
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
// popper
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: placement ?? "auto",
|
||||
|
|
@ -208,6 +218,8 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
|||
initialValue={prompt}
|
||||
containerClassName="-m-3"
|
||||
ref={editorRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -218,6 +230,8 @@ export const GptAssistantPopover: React.FC<Props> = (props) => {
|
|||
id="ai-assistant-response"
|
||||
initialValue={`<p>${response}</p>`}
|
||||
ref={responseRef}
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,34 +5,33 @@ import { observer } from "mobx-react";
|
|||
import { useDropzone } from "react-dropzone";
|
||||
import { UserCircle2 } from "lucide-react";
|
||||
import { Transition, Dialog } from "@headlessui/react";
|
||||
// plane types
|
||||
import { EFileAssetType } from "@plane/types/src/enums";
|
||||
// hooks
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
// constants
|
||||
import { MAX_FILE_SIZE } from "@/constants/common";
|
||||
// hooks
|
||||
import { useInstance } from "@/hooks/store";
|
||||
import { MAX_STATIC_FILE_SIZE } from "@/constants/common";
|
||||
// helpers
|
||||
import { getAssetIdFromUrl, getFileURL } from "@/helpers/file.helper";
|
||||
import { checkURLValidity } from "@/helpers/string.helper";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
const fileService = new FileService();
|
||||
|
||||
type Props = {
|
||||
handleDelete?: () => void;
|
||||
handleRemove: () => Promise<void>;
|
||||
isOpen: boolean;
|
||||
isRemoving: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: (url: string) => void;
|
||||
value: string | null;
|
||||
};
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
export const UserImageUploadModal: React.FC<Props> = observer((props) => {
|
||||
const { value, onSuccess, isOpen, onClose, isRemoving, handleDelete } = props;
|
||||
const { handleRemove, isOpen, onClose, onSuccess, value } = props;
|
||||
// states
|
||||
const [image, setImage] = useState<File | null>(null);
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
const [isImageUploading, setIsImageUploading] = useState(false);
|
||||
// store hooks
|
||||
const { config } = useInstance();
|
||||
|
||||
const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]);
|
||||
|
||||
|
|
@ -41,7 +40,7 @@ export const UserImageUploadModal: React.FC<Props> = observer((props) => {
|
|||
accept: {
|
||||
"image/*": [".png", ".jpg", ".jpeg", ".webp"],
|
||||
},
|
||||
maxSize: config?.file_size_limit ?? MAX_FILE_SIZE,
|
||||
maxSize: MAX_STATIC_FILE_SIZE,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
|
|
@ -53,31 +52,46 @@ export const UserImageUploadModal: React.FC<Props> = observer((props) => {
|
|||
|
||||
const handleSubmit = async () => {
|
||||
if (!image) return;
|
||||
|
||||
setIsImageUploading(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("asset", image);
|
||||
formData.append("attributes", JSON.stringify({}));
|
||||
try {
|
||||
const { asset_url } = await fileService.uploadUserAsset(
|
||||
{
|
||||
entity_identifier: "",
|
||||
entity_type: EFileAssetType.USER_AVATAR,
|
||||
},
|
||||
image
|
||||
);
|
||||
onSuccess(asset_url);
|
||||
setImage(null);
|
||||
} catch (error) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: error?.toString() ?? "Something went wrong. Please try again.",
|
||||
});
|
||||
throw new Error("Error in uploading file.");
|
||||
} finally {
|
||||
setIsImageUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fileService
|
||||
.uploadUserFile(formData)
|
||||
.then((res) => {
|
||||
const imageUrl = res.asset;
|
||||
|
||||
onSuccess(imageUrl);
|
||||
setImage(null);
|
||||
|
||||
if (value) fileService.deleteUserFile(value);
|
||||
})
|
||||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
})
|
||||
)
|
||||
.finally(() => setIsImageUploading(false));
|
||||
const handleImageRemove = async () => {
|
||||
if (!value) return;
|
||||
setIsRemoving(true);
|
||||
try {
|
||||
if (checkURLValidity(value)) {
|
||||
await fileService.deleteOldUserAsset(value);
|
||||
} else {
|
||||
const assetId = getAssetIdFromUrl(value);
|
||||
await fileService.deleteUserAsset(assetId);
|
||||
}
|
||||
await handleRemove();
|
||||
} catch (error) {
|
||||
console.log("Error in uploading user asset:", error);
|
||||
} finally {
|
||||
setIsRemoving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -130,7 +144,7 @@ export const UserImageUploadModal: React.FC<Props> = observer((props) => {
|
|||
Edit
|
||||
</button>
|
||||
<img
|
||||
src={image ? URL.createObjectURL(image) : value ? value : ""}
|
||||
src={image ? URL.createObjectURL(image) : value ? getFileURL(value) : ""}
|
||||
alt="image"
|
||||
className="absolute left-0 top-0 h-full w-full rounded-md object-cover"
|
||||
/>
|
||||
|
|
@ -158,11 +172,9 @@ export const UserImageUploadModal: React.FC<Props> = observer((props) => {
|
|||
</div>
|
||||
<p className="my-4 text-sm text-custom-text-200">File formats supported- .jpeg, .jpg, .png, .webp</p>
|
||||
<div className="flex items-center justify-between">
|
||||
{handleDelete && (
|
||||
<Button variant="danger" size="sm" onClick={handleDelete} disabled={!value}>
|
||||
{isRemoving ? "Removing..." : "Remove"}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="danger" size="sm" onClick={handleImageRemove} disabled={!value}>
|
||||
{isRemoving ? "Removing" : "Remove"}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
|
|
@ -174,7 +186,7 @@ export const UserImageUploadModal: React.FC<Props> = observer((props) => {
|
|||
disabled={!image}
|
||||
loading={isImageUploading}
|
||||
>
|
||||
{isImageUploading ? "Uploading..." : "Upload & Save"}
|
||||
{isImageUploading ? "Uploading" : "Upload & Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,27 @@
|
|||
"use client";
|
||||
import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useParams, usePathname } from "next/navigation";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { UserCircle2 } from "lucide-react";
|
||||
import { Transition, Dialog } from "@headlessui/react";
|
||||
// plane types
|
||||
import { EFileAssetType } from "@plane/types/src/enums";
|
||||
// hooks
|
||||
import { Button, TOAST_TYPE, setToast } from "@plane/ui";
|
||||
import { Button } from "@plane/ui";
|
||||
// constants
|
||||
import { MAX_FILE_SIZE } from "@/constants/common";
|
||||
import { MAX_STATIC_FILE_SIZE } from "@/constants/common";
|
||||
// helpers
|
||||
import { getAssetIdFromUrl, getFileURL } from "@/helpers/file.helper";
|
||||
import { checkURLValidity } from "@/helpers/string.helper";
|
||||
// hooks
|
||||
import { useWorkspace, useInstance } from "@/hooks/store";
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
// services
|
||||
import { FileService } from "@/services/file.service";
|
||||
|
||||
type Props = {
|
||||
handleRemove?: () => void;
|
||||
handleRemove: () => Promise<void>;
|
||||
isOpen: boolean;
|
||||
isRemoving: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess: (url: string) => void;
|
||||
value: string | null;
|
||||
|
|
@ -27,16 +31,15 @@ type Props = {
|
|||
const fileService = new FileService();
|
||||
|
||||
export const WorkspaceImageUploadModal: React.FC<Props> = observer((props) => {
|
||||
const { value, onSuccess, isOpen, onClose, isRemoving, handleRemove } = props;
|
||||
const { handleRemove, isOpen, onClose, onSuccess, value } = props;
|
||||
// states
|
||||
const [image, setImage] = useState<File | null>(null);
|
||||
const [isRemoving, setIsRemoving] = useState(false);
|
||||
const [isImageUploading, setIsImageUploading] = useState(false);
|
||||
// router
|
||||
const { workspaceSlug } = useParams();
|
||||
const pathname = usePathname();
|
||||
// store hooks
|
||||
const { config } = useInstance();
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { currentWorkspace, updateWorkspaceLogo } = useWorkspace();
|
||||
|
||||
const onDrop = (acceptedFiles: File[]) => setImage(acceptedFiles[0]);
|
||||
|
||||
|
|
@ -45,45 +48,58 @@ export const WorkspaceImageUploadModal: React.FC<Props> = observer((props) => {
|
|||
accept: {
|
||||
"image/*": [".png", ".jpg", ".jpeg", ".webp"],
|
||||
},
|
||||
maxSize: config?.file_size_limit ?? MAX_FILE_SIZE,
|
||||
maxSize: MAX_STATIC_FILE_SIZE,
|
||||
multiple: false,
|
||||
});
|
||||
|
||||
const handleClose = () => {
|
||||
setImage(null);
|
||||
setIsImageUploading(false);
|
||||
onClose();
|
||||
setTimeout(() => {
|
||||
setImage(null);
|
||||
}, 300);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!image || (!workspaceSlug && pathname !== "/onboarding")) return;
|
||||
|
||||
if (!image || !workspaceSlug || !currentWorkspace) return;
|
||||
setIsImageUploading(true);
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("asset", image);
|
||||
formData.append("attributes", JSON.stringify({}));
|
||||
try {
|
||||
const { asset_url } = await fileService.uploadWorkspaceAsset(
|
||||
workspaceSlug.toString(),
|
||||
{
|
||||
entity_identifier: currentWorkspace.id,
|
||||
entity_type: EFileAssetType.WORKSPACE_LOGO,
|
||||
},
|
||||
image
|
||||
);
|
||||
updateWorkspaceLogo(workspaceSlug.toString(), asset_url);
|
||||
onSuccess(asset_url);
|
||||
} catch (error) {
|
||||
console.log("error", error);
|
||||
throw new Error("Error in uploading file.");
|
||||
} finally {
|
||||
setIsImageUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!workspaceSlug) return;
|
||||
|
||||
fileService
|
||||
.uploadFile(workspaceSlug.toString(), formData)
|
||||
.then((res) => {
|
||||
const imageUrl = res.asset;
|
||||
|
||||
onSuccess(imageUrl);
|
||||
setImage(null);
|
||||
|
||||
if (value && currentWorkspace) fileService.deleteFile(currentWorkspace.id, value);
|
||||
})
|
||||
.catch((err) =>
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: err?.error ?? "Something went wrong. Please try again.",
|
||||
})
|
||||
)
|
||||
.finally(() => setIsImageUploading(false));
|
||||
const handleImageRemove = async () => {
|
||||
if (!workspaceSlug || !value) return;
|
||||
setIsRemoving(true);
|
||||
try {
|
||||
if (checkURLValidity(value)) {
|
||||
await fileService.deleteOldWorkspaceAsset(currentWorkspace?.id ?? "", value);
|
||||
} else {
|
||||
const assetId = getAssetIdFromUrl(value);
|
||||
await fileService.deleteWorkspaceAsset(workspaceSlug.toString(), assetId);
|
||||
}
|
||||
await handleRemove();
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
console.log("Error in removing workspace asset:", error);
|
||||
} finally {
|
||||
setIsRemoving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -115,7 +131,7 @@ export const WorkspaceImageUploadModal: React.FC<Props> = observer((props) => {
|
|||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 px-5 py-8 text-left shadow-custom-shadow-md transition-all sm:w-full sm:max-w-xl sm:p-6">
|
||||
<div className="space-y-5">
|
||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-100">
|
||||
Upload Image
|
||||
Upload image
|
||||
</Dialog.Title>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
|
|
@ -136,7 +152,7 @@ export const WorkspaceImageUploadModal: React.FC<Props> = observer((props) => {
|
|||
Edit
|
||||
</button>
|
||||
<img
|
||||
src={image ? URL.createObjectURL(image) : value ? value : ""}
|
||||
src={image ? URL.createObjectURL(image) : value ? getFileURL(value) : ""}
|
||||
alt="image"
|
||||
className="absolute left-0 top-0 h-full w-full rounded-md object-cover"
|
||||
/>
|
||||
|
|
@ -164,11 +180,9 @@ export const WorkspaceImageUploadModal: React.FC<Props> = observer((props) => {
|
|||
</div>
|
||||
<p className="my-4 text-sm text-custom-text-200">File formats supported- .jpeg, .jpg, .png, .webp</p>
|
||||
<div className="flex items-center justify-between">
|
||||
{handleRemove && (
|
||||
<Button variant="danger" size="sm" onClick={handleRemove} disabled={!value}>
|
||||
{isRemoving ? "Removing..." : "Remove"}
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="danger" size="sm" onClick={handleImageRemove} disabled={!value} loading={isRemoving}>
|
||||
{isRemoving ? "Removing" : "Remove"}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
|
|
@ -180,7 +194,7 @@ export const WorkspaceImageUploadModal: React.FC<Props> = observer((props) => {
|
|||
disabled={!image}
|
||||
loading={isImageUploading}
|
||||
>
|
||||
{isImageUploading ? "Uploading..." : "Upload & Save"}
|
||||
{isImageUploading ? "Uploading" : "Upload & Save"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue