[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:
Aaryan Khandelwal 2024-10-11 20:13:38 +05:30 committed by GitHub
parent c9580ab794
commit 7e334203f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
241 changed files with 5326 additions and 2518 deletions

View file

@ -1,3 +1,6 @@
// helpers
import { getFileURL } from "@/helpers/file.helper";
type Props = {
logo: string | null | undefined;
name: string | undefined;
@ -11,9 +14,13 @@ export const WorkspaceLogo = (props: Props) => (
} ${props.classNames ? props.classNames : ""}`}
>
{props.logo && props.logo !== "" ? (
<img src={props.logo} className="absolute left-0 top-0 h-full w-full rounded object-cover" alt="Workspace Logo" />
<img
src={getFileURL(props.logo)}
className="absolute left-0 top-0 h-full w-full rounded object-cover"
alt="Workspace Logo"
/>
) : (
props.name?.charAt(0) ?? "..."
(props.name?.charAt(0) ?? "...")
)}
</div>
);

View file

@ -3,10 +3,17 @@ import Link from "next/link";
import { Controller, useForm } from "react-hook-form";
import { Trash2 } from "lucide-react";
import { Disclosure } from "@headlessui/react";
// plane types
import { IUser, IWorkspaceMember } from "@plane/types";
// plane ui
import { CustomSelect, PopoverMenu, TOAST_TYPE, setToast } from "@plane/ui";
// constants
import { ROLE } from "@/constants/workspace";
// helpers
import { getFileURL } from "@/helpers/file.helper";
// hooks
import { useMember, useUser, useUserPermissions } from "@/hooks/store";
// plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
export interface RowData {
@ -29,33 +36,36 @@ type AccountTypeProps = {
export const NameColumn: React.FC<NameProps> = (props) => {
const { rowData, workspaceSlug, isAdmin, currentUser, setRemoveMemberModal } = props;
// derived values
const { avatar_url, display_name, email, first_name, id, last_name } = rowData.member;
return (
<Disclosure>
{({}) => (
<div className="relative group">
<div className="flex items-center gap-x-4 gap-y-2 w-72 justify-between">
<div className="flex items-center gap-x-4 gap-y-2 flex-1">
{rowData.member.avatar && rowData.member.avatar.trim() !== "" ? (
<Link href={`/${workspaceSlug}/profile/${rowData.member.id}`}>
{avatar_url && avatar_url.trim() !== "" ? (
<Link href={`/${workspaceSlug}/profile/${id}`}>
<span className="relative flex h-6 w-6 items-center justify-center rounded-full p-4 capitalize text-white">
<img
src={rowData.member.avatar}
src={getFileURL(avatar_url)}
className="absolute left-0 top-0 h-full w-full rounded-full object-cover"
alt={rowData.member.display_name || rowData.member.email}
alt={display_name || email}
/>
</span>
</Link>
) : (
<Link href={`/${workspaceSlug}/profile/${rowData.member.id}`}>
<Link href={`/${workspaceSlug}/profile/${id}`}>
<span className="relative flex h-6 w-6 items-center justify-center rounded-full bg-gray-700 p-4 capitalize text-white">
{(rowData.member.email ?? rowData.member.display_name ?? "?")[0]}
{(email ?? display_name ?? "?")[0]}
</span>
</Link>
)}
{rowData.member.first_name} {rowData.member.last_name}
{first_name} {last_name}
</div>
{(isAdmin || rowData.member?.id === currentUser?.id) && (
{(isAdmin || id === currentUser?.id) && (
<PopoverMenu
data={[""]}
keyExtractor={(item) => item}
@ -66,8 +76,7 @@ export const NameColumn: React.FC<NameProps> = (props) => {
className="flex items-center gap-x-3 cursor-pointer"
onClick={() => setRemoveMemberModal(rowData)}
>
<Trash2 className="size-3.5 align-middle" />{" "}
{rowData.member?.id === currentUser?.id ? "Leave " : "Remove "}
<Trash2 className="size-3.5 align-middle" /> {id === currentUser?.id ? "Leave " : "Remove "}
</div>
)}
/>

View file

@ -14,29 +14,24 @@ import { WorkspaceImageUploadModal } from "@/components/core";
import { WORKSPACE_UPDATED } from "@/constants/event-tracker";
import { ORGANIZATION_SIZE } from "@/constants/workspace";
// helpers
import { getFileURL } from "@/helpers/file.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks
import { useEventTracker, useUserPermissions, useWorkspace } from "@/hooks/store";
// plane web components
import { DeleteWorkspaceSection } from "@/plane-web/components/workspace";
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
// services
import { FileService } from "@/services/file.service";
const defaultValues: Partial<IWorkspace> = {
name: "",
url: "",
organization_size: "2-10",
logo: null,
logo_url: null,
};
// services
const fileService = new FileService();
export const WorkspaceDetails: FC = observer(() => {
// states
const [isLoading, setIsLoading] = useState(false);
const [isImageRemoving, setIsImageRemoving] = useState(false);
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
// store hooks
const { captureWorkspaceEvent } = useEventTracker();
@ -53,6 +48,8 @@ export const WorkspaceDetails: FC = observer(() => {
} = useForm<IWorkspace>({
defaultValues: { ...defaultValues, ...currentWorkspace },
});
// derived values
const workspaceLogo = watch("logo_url");
const onSubmit = async (formData: IWorkspace) => {
if (!currentWorkspace) return;
@ -60,7 +57,6 @@ export const WorkspaceDetails: FC = observer(() => {
setIsLoading(true);
const payload: Partial<IWorkspace> = {
logo: formData.logo,
name: formData.name,
organization_size: formData.organization_size,
};
@ -96,34 +92,26 @@ export const WorkspaceDetails: FC = observer(() => {
}, 300);
};
const handleRemoveLogo = () => {
const handleRemoveLogo = async () => {
if (!currentWorkspace) return;
const url = currentWorkspace.logo;
if (!url) return;
setIsImageRemoving(true);
fileService.deleteFile(currentWorkspace.id, url).then(() => {
updateWorkspace(currentWorkspace.slug, { logo: "" })
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Workspace picture removed successfully.",
});
setIsImageUploadModalOpen(false);
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "There was some error in deleting your profile picture. Please try again.",
});
})
.finally(() => setIsImageRemoving(false));
});
await updateWorkspace(currentWorkspace.slug, {
logo_url: "",
})
.then(() => {
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Success!",
message: "Workspace picture removed successfully.",
});
})
.catch(() => {
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "There was some error in deleting your profile picture. Please try again.",
});
});
};
const handleCopyUrl = () => {
@ -154,17 +142,15 @@ export const WorkspaceDetails: FC = observer(() => {
<>
<Controller
control={control}
name="logo"
name="logo_url"
render={({ field: { onChange, value } }) => (
<WorkspaceImageUploadModal
isOpen={isImageUploadModalOpen}
onClose={() => setIsImageUploadModalOpen(false)}
isRemoving={isImageRemoving}
handleRemove={handleRemoveLogo}
onSuccess={(imageUrl) => {
onChange(imageUrl);
setIsImageUploadModalOpen(false);
handleSubmit(onSubmit)();
}}
value={value}
/>
@ -174,10 +160,10 @@ export const WorkspaceDetails: FC = observer(() => {
<div className="flex gap-5 border-b border-custom-border-100 pb-7 items-start">
<div className="flex flex-col gap-1">
<button type="button" onClick={() => setIsImageUploadModalOpen(true)} disabled={!isAdmin}>
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
{workspaceLogo && workspaceLogo !== "" ? (
<div className="relative mx-auto flex h-14 w-14">
<img
src={watch("logo")!}
src={getFileURL(workspaceLogo)}
className="absolute left-0 top-0 h-full w-full rounded-md object-cover"
alt="Workspace Logo"
/>
@ -199,7 +185,7 @@ export const WorkspaceDetails: FC = observer(() => {
className="flex items-center gap-1.5 text-left text-xs font-medium text-custom-primary-100"
onClick={() => setIsImageUploadModalOpen(true)}
>
{watch("logo") && watch("logo") !== null && watch("logo") !== "" ? (
{workspaceLogo && workspaceLogo !== "" ? (
<>
<Pencil className="h-3 w-3" />
Edit logo

View file

@ -14,8 +14,11 @@ import { IWorkspace } from "@plane/types";
// plane ui
import { Avatar, Loader, TOAST_TYPE, setToast } from "@plane/ui";
import { GOD_MODE_URL, cn } from "@/helpers/common.helper";
// helpers
import { getFileURL } from "@/helpers/file.helper";
// hooks
import { useAppTheme, useUser, useUserPermissions, useUserProfile, useWorkspace } from "@/hooks/store";
// plane web constants
import { EUserPermissions, EUserPermissionsLevel } from "@/plane-web/constants/user-permissions";
import { WorkspaceLogo } from "../logo";
@ -110,7 +113,7 @@ export const SidebarDropdown = observer(() => {
)}
>
<div className="flex-grow flex items-center gap-2 truncate">
<WorkspaceLogo logo={activeWorkspace?.logo} name={activeWorkspace?.name} />
<WorkspaceLogo logo={activeWorkspace?.logo_url} name={activeWorkspace?.name} />
{!sidebarCollapsed && (
<h4 className="truncate text-base font-medium text-custom-text-100">
{activeWorkspace?.name ?? "Loading..."}
@ -162,17 +165,17 @@ export const SidebarDropdown = observer(() => {
<div className="flex items-center justify-start gap-2.5 truncate">
<span
className={`relative flex h-6 w-6 flex-shrink-0 items-center justify-center p-2 text-xs uppercase ${
!workspace?.logo && "rounded bg-custom-primary-500 text-white"
!workspace?.logo_url && "rounded bg-custom-primary-500 text-white"
}`}
>
{workspace?.logo && workspace.logo !== "" ? (
{workspace?.logo_url && workspace.logo_url !== "" ? (
<img
src={workspace.logo}
src={getFileURL(workspace.logo_url)}
className="absolute left-0 top-0 h-full w-full rounded object-cover"
alt="Workspace Logo"
/>
) : (
(workspace?.name?.charAt(0) ?? "...")
(workspace?.name?.[0] ?? "...")
)}
</span>
<h5
@ -255,7 +258,7 @@ export const SidebarDropdown = observer(() => {
<Menu.Button className="grid place-items-center outline-none" ref={setReferenceElement}>
<Avatar
name={currentUser?.display_name}
src={currentUser?.avatar || undefined}
src={getFileURL(currentUser?.avatar_url ?? "")}
size={24}
shape="square"
className="!text-base"