forked from github/plane
chore: update user file assets endpoint (#438)
* chore: new service for user assets * chore: update user file assets endpoint
This commit is contained in:
parent
dbd6de0988
commit
bfab4865cd
@ -19,11 +19,16 @@ type TImageUploadModalProps = {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onSuccess: (url: string) => void;
|
onSuccess: (url: string) => void;
|
||||||
|
userImage?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ImageUploadModal: React.FC<TImageUploadModalProps> = (props) => {
|
export const ImageUploadModal: React.FC<TImageUploadModalProps> = ({
|
||||||
const { value, onSuccess, isOpen, onClose } = props;
|
value,
|
||||||
|
onSuccess,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
userImage,
|
||||||
|
}) => {
|
||||||
const [image, setImage] = useState<File | null>(null);
|
const [image, setImage] = useState<File | null>(null);
|
||||||
const [isImageUploading, setIsImageUploading] = useState(false);
|
const [isImageUploading, setIsImageUploading] = useState(false);
|
||||||
|
|
||||||
@ -46,22 +51,34 @@ export const ImageUploadModal: React.FC<TImageUploadModalProps> = (props) => {
|
|||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setIsImageUploading(true);
|
setIsImageUploading(true);
|
||||||
|
|
||||||
if (image === null || !workspaceSlug) return;
|
if (!image || !workspaceSlug) return;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("asset", image);
|
formData.append("asset", image);
|
||||||
formData.append("attributes", JSON.stringify({}));
|
formData.append("attributes", JSON.stringify({}));
|
||||||
|
|
||||||
fileServices
|
if (userImage) {
|
||||||
.uploadFile(workspaceSlug as string, formData)
|
fileServices
|
||||||
.then((res) => {
|
.uploadUserFile(formData)
|
||||||
const imageUrl = res.asset;
|
.then((res) => {
|
||||||
onSuccess(imageUrl);
|
const imageUrl = res.asset;
|
||||||
setIsImageUploading(false);
|
onSuccess(imageUrl);
|
||||||
})
|
setIsImageUploading(false);
|
||||||
.catch((err) => {
|
})
|
||||||
console.error(err);
|
.catch((err) => {
|
||||||
});
|
console.error(err);
|
||||||
|
});
|
||||||
|
} else
|
||||||
|
fileServices
|
||||||
|
.uploadFile(workspaceSlug as string, formData)
|
||||||
|
.then((res) => {
|
||||||
|
const imageUrl = res.asset;
|
||||||
|
onSuccess(imageUrl);
|
||||||
|
setIsImageUploading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
@ -109,11 +126,10 @@ export const ImageUploadModal: React.FC<TImageUploadModalProps> = (props) => {
|
|||||||
: ""
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{image !== null || (value && value !== null && value !== "") ? (
|
{image !== null || (value && value !== "") ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={openFileDialog}
|
|
||||||
className="absolute top-0 right-0 z-40 translate-x-1/2 -translate-y-1/2 rounded bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600"
|
className="absolute top-0 right-0 z-40 translate-x-1/2 -translate-y-1/2 rounded bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
|
@ -2,7 +2,6 @@ import React, { useEffect, useState } from "react";
|
|||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
@ -11,15 +10,14 @@ import { useForm } from "react-hook-form";
|
|||||||
// lib
|
// lib
|
||||||
import { requiredAuth } from "lib/auth";
|
import { requiredAuth } from "lib/auth";
|
||||||
// services
|
// services
|
||||||
import projectService from "services/project.service";
|
import fileService from "services/file.service";
|
||||||
|
import userService from "services/user.service";
|
||||||
|
import workspaceService from "services/workspace.service";
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "hooks/use-user";
|
import useUser from "hooks/use-user";
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
// layouts
|
// layouts
|
||||||
import AppLayout from "layouts/app-layout";
|
import AppLayout from "layouts/app-layout";
|
||||||
// services
|
|
||||||
import userService from "services/user.service";
|
|
||||||
import workspaceService from "services/workspace.service";
|
|
||||||
// components
|
// components
|
||||||
import { ImageUploadModal } from "components/core";
|
import { ImageUploadModal } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
@ -28,18 +26,16 @@ import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
|
|||||||
// icons
|
// icons
|
||||||
import {
|
import {
|
||||||
ChevronRightIcon,
|
ChevronRightIcon,
|
||||||
ClipboardDocumentListIcon,
|
|
||||||
PencilIcon,
|
PencilIcon,
|
||||||
RectangleStackIcon,
|
|
||||||
UserIcon,
|
UserIcon,
|
||||||
UserPlusIcon,
|
UserPlusIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
} from "@heroicons/react/24/outline";
|
} from "@heroicons/react/24/outline";
|
||||||
// types
|
// types
|
||||||
import type { NextPage, GetServerSidePropsContext } from "next";
|
import type { NextPage, GetServerSidePropsContext } from "next";
|
||||||
import type { IIssue, IUser } from "types";
|
import type { IUser } from "types";
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { USER_ISSUE, USER_WORKSPACE_INVITATIONS, PROJECTS_LIST } from "constants/fetch-keys";
|
import { USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
|
||||||
|
|
||||||
const defaultValues: Partial<IUser> = {
|
const defaultValues: Partial<IUser> = {
|
||||||
avatar: "",
|
avatar: "",
|
||||||
@ -50,12 +46,9 @@ const defaultValues: Partial<IUser> = {
|
|||||||
|
|
||||||
const Profile: NextPage = () => {
|
const Profile: NextPage = () => {
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [isRemoving, setIsRemoving] = useState(false);
|
||||||
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
const [isImageUploadModalOpen, setIsImageUploadModalOpen] = useState(false);
|
||||||
|
|
||||||
const {
|
|
||||||
query: { workspaceSlug },
|
|
||||||
} = useRouter();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@ -68,65 +61,79 @@ const Profile: NextPage = () => {
|
|||||||
const { setToastAlert } = useToast();
|
const { setToastAlert } = useToast();
|
||||||
const { user: myProfile, mutateUser } = useUser();
|
const { user: myProfile, mutateUser } = useUser();
|
||||||
|
|
||||||
const { data: myIssues } = useSWR<IIssue[]>(
|
|
||||||
myProfile && workspaceSlug ? USER_ISSUE(workspaceSlug as string) : null,
|
|
||||||
myProfile && workspaceSlug ? () => userService.userIssues(workspaceSlug as string) : null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
|
const { data: invitations } = useSWR(USER_WORKSPACE_INVITATIONS, () =>
|
||||||
workspaceService.userWorkspaceInvitations()
|
workspaceService.userWorkspaceInvitations()
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: projects } = useSWR(
|
|
||||||
workspaceSlug ? PROJECTS_LIST(workspaceSlug as string) : null,
|
|
||||||
() => (workspaceSlug ? () => projectService.getProjects(workspaceSlug as string) : null)
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
reset({ ...defaultValues, ...myProfile });
|
reset({ ...defaultValues, ...myProfile });
|
||||||
}, [myProfile, reset]);
|
}, [myProfile, reset]);
|
||||||
|
|
||||||
const onSubmit = (formData: IUser) => {
|
const onSubmit = (formData: IUser) => {
|
||||||
const payload: Partial<IUser> = {
|
const payload: Partial<IUser> = {
|
||||||
id: formData.id,
|
|
||||||
first_name: formData.first_name,
|
first_name: formData.first_name,
|
||||||
last_name: formData.last_name,
|
last_name: formData.last_name,
|
||||||
avatar: formData.avatar,
|
avatar: formData.avatar,
|
||||||
};
|
};
|
||||||
userService
|
userService
|
||||||
.updateUser(payload)
|
.updateUser(payload)
|
||||||
.then((response) => {
|
.then((res) => {
|
||||||
mutateUser((prevData) => {
|
mutateUser((prevData) => {
|
||||||
if (!prevData) return prevData;
|
if (!prevData) return prevData;
|
||||||
return { ...prevData, user: { ...payload, ...response } };
|
return { ...prevData, user: { ...payload, ...res } };
|
||||||
});
|
}, false);
|
||||||
setIsEditing(false);
|
setIsEditing(false);
|
||||||
setToastAlert({
|
setToastAlert({
|
||||||
title: "Success",
|
|
||||||
type: "success",
|
type: "success",
|
||||||
message: "Profile updated successfully",
|
title: "Success!",
|
||||||
|
message: "Profile updated successfully.",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch(() => {
|
||||||
console.log(error);
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "There was some error in updating your profile. Please try again.",
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDelete = (url: string | null | undefined, updateUser: boolean = false) => {
|
||||||
|
if (!url) return;
|
||||||
|
|
||||||
|
setIsRemoving(true);
|
||||||
|
|
||||||
|
const index = url.indexOf(".com");
|
||||||
|
const asset = url.substring(index + 5);
|
||||||
|
|
||||||
|
fileService.deleteUserFile(asset).then(() => {
|
||||||
|
if (updateUser)
|
||||||
|
userService
|
||||||
|
.updateUser({ avatar: "" })
|
||||||
|
.then((res) => {
|
||||||
|
setIsRemoving(false);
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Success!",
|
||||||
|
message: "Profile picture removed successfully.",
|
||||||
|
});
|
||||||
|
mutateUser((prevData) => {
|
||||||
|
if (!prevData) return prevData;
|
||||||
|
return { ...prevData, user: res };
|
||||||
|
}, false);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setIsRemoving(false);
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: "There was some error in deleting your profile picture. Please try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const quickLinks = [
|
const quickLinks = [
|
||||||
{
|
|
||||||
icon: RectangleStackIcon,
|
|
||||||
title: "My Issues",
|
|
||||||
number: myIssues?.length ?? 0,
|
|
||||||
description: "View the list of issues assigned to you for this workspace.",
|
|
||||||
href: `/${workspaceSlug}/me/my-issues`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: ClipboardDocumentListIcon,
|
|
||||||
title: "My Projects",
|
|
||||||
number: projects?.length ?? 0,
|
|
||||||
description: "View the list of projects of the workspace.",
|
|
||||||
href: `/${workspaceSlug}/projects`,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: UserPlusIcon,
|
icon: UserPlusIcon,
|
||||||
title: "Workspace Invitations",
|
title: "Workspace Invitations",
|
||||||
@ -147,11 +154,13 @@ const Profile: NextPage = () => {
|
|||||||
isOpen={isImageUploadModalOpen}
|
isOpen={isImageUploadModalOpen}
|
||||||
onClose={() => setIsImageUploadModalOpen(false)}
|
onClose={() => setIsImageUploadModalOpen(false)}
|
||||||
onSuccess={(url) => {
|
onSuccess={(url) => {
|
||||||
|
handleDelete(myProfile?.avatar);
|
||||||
setValue("avatar", url);
|
setValue("avatar", url);
|
||||||
handleSubmit(onSubmit)();
|
handleSubmit(onSubmit)();
|
||||||
setIsImageUploadModalOpen(false);
|
setIsImageUploadModalOpen(false);
|
||||||
}}
|
}}
|
||||||
value={watch("avatar") !== "" ? watch("avatar") : undefined}
|
value={watch("avatar") !== "" ? watch("avatar") : undefined}
|
||||||
|
userImage
|
||||||
/>
|
/>
|
||||||
<div className="w-full space-y-5">
|
<div className="w-full space-y-5">
|
||||||
<Breadcrumbs>
|
<Breadcrumbs>
|
||||||
@ -197,15 +206,26 @@ const Profile: NextPage = () => {
|
|||||||
<br />
|
<br />
|
||||||
Supported file types are .jpg and .png.
|
Supported file types are .jpg and .png.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<div className="flex items-center gap-2">
|
||||||
type="button"
|
<Button
|
||||||
className="mt-4"
|
type="button"
|
||||||
onClick={() => {
|
className="mt-4"
|
||||||
setIsImageUploadModalOpen(true);
|
onClick={() => setIsImageUploadModalOpen(true)}
|
||||||
}}
|
>
|
||||||
>
|
Upload new
|
||||||
Upload
|
</Button>
|
||||||
</Button>
|
{myProfile.avatar && myProfile.avatar !== "" && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="mt-4"
|
||||||
|
theme="danger"
|
||||||
|
onClick={() => handleDelete(myProfile.avatar, true)}
|
||||||
|
disabled={isRemoving}
|
||||||
|
>
|
||||||
|
{isRemoving ? "Removing..." : "Remove"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form className="space-y-5" onSubmit={handleSubmit(onSubmit)}>
|
<form className="space-y-5" onSubmit={handleSubmit(onSubmit)}>
|
||||||
@ -259,7 +279,7 @@ const Profile: NextPage = () => {
|
|||||||
</section>
|
</section>
|
||||||
<section>
|
<section>
|
||||||
<h2 className="mb-3 text-xl font-medium">Quick Links</h2>
|
<h2 className="mb-3 text-xl font-medium">Quick Links</h2>
|
||||||
<div className="grid grid-cols-3 gap-5">
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
{quickLinks.map((item, index) => (
|
{quickLinks.map((item, index) => (
|
||||||
<Link key={index} href={item.href}>
|
<Link key={index} href={item.href}>
|
||||||
<a className="group rounded-lg bg-secondary p-5 duration-300 hover:bg-theme">
|
<a className="group rounded-lg bg-secondary p-5 duration-300 hover:bg-theme">
|
||||||
|
@ -81,7 +81,8 @@ const SingleCycle: React.FC<UserAuth> = (props) => {
|
|||||||
|
|
||||||
const cycleStatus =
|
const cycleStatus =
|
||||||
cycleDetails?.start_date && cycleDetails?.end_date
|
cycleDetails?.start_date && cycleDetails?.end_date
|
||||||
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date) : "";
|
? getDateRangeStatus(cycleDetails?.start_date, cycleDetails?.end_date)
|
||||||
|
: "";
|
||||||
|
|
||||||
const { data: cycleIssues } = useSWR<CycleIssueResponse[]>(
|
const { data: cycleIssues } = useSWR<CycleIssueResponse[]>(
|
||||||
workspaceSlug && projectId && cycleId ? CYCLE_ISSUES(cycleId as string) : null,
|
workspaceSlug && projectId && cycleId ? CYCLE_ISSUES(cycleId as string) : null,
|
||||||
|
@ -14,10 +14,11 @@ import { LinkIcon } from "@heroicons/react/24/outline";
|
|||||||
import { requiredWorkspaceAdmin } from "lib/auth";
|
import { requiredWorkspaceAdmin } from "lib/auth";
|
||||||
// services
|
// services
|
||||||
import workspaceService from "services/workspace.service";
|
import workspaceService from "services/workspace.service";
|
||||||
// layouts
|
import fileService from "services/file.service";
|
||||||
import AppLayout from "layouts/app-layout";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useToast from "hooks/use-toast";
|
import useToast from "hooks/use-toast";
|
||||||
|
// layouts
|
||||||
|
import AppLayout from "layouts/app-layout";
|
||||||
// components
|
// components
|
||||||
import { ImageUploadModal } from "components/core";
|
import { ImageUploadModal } from "components/core";
|
||||||
import { DeleteWorkspaceModal } from "components/workspace";
|
import { DeleteWorkspaceModal } from "components/workspace";
|
||||||
@ -96,6 +97,15 @@ const WorkspaceSettings: NextPage<UserAuth> = (props) => {
|
|||||||
.catch((err) => console.error(err));
|
.catch((err) => console.error(err));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDelete = (url: string | null | undefined) => {
|
||||||
|
if (!url) return;
|
||||||
|
|
||||||
|
const index = url.indexOf(".com");
|
||||||
|
const asset = url.substring(index + 5);
|
||||||
|
|
||||||
|
fileService.deleteFile(asset);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout
|
<AppLayout
|
||||||
memberType={props}
|
memberType={props}
|
||||||
@ -114,6 +124,7 @@ const WorkspaceSettings: NextPage<UserAuth> = (props) => {
|
|||||||
onClose={() => setIsImageUploadModalOpen(false)}
|
onClose={() => setIsImageUploadModalOpen(false)}
|
||||||
onSuccess={(imageUrl) => {
|
onSuccess={(imageUrl) => {
|
||||||
setIsImageUploading(true);
|
setIsImageUploading(true);
|
||||||
|
handleDelete(activeWorkspace?.logo);
|
||||||
setValue("logo", imageUrl);
|
setValue("logo", imageUrl);
|
||||||
setIsImageUploadModalOpen(false);
|
setIsImageUploadModalOpen(false);
|
||||||
handleSubmit(onSubmit)().then(() => setIsImageUploading(false));
|
handleSubmit(onSubmit)().then(() => setIsImageUploading(false));
|
||||||
|
@ -40,6 +40,30 @@ class FileServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async deleteFile(asset: string): Promise<any> {
|
||||||
|
return this.delete(`/api/workspaces/file-assets/${asset}/`)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadUserFile(file: FormData): Promise<any> {
|
||||||
|
return this.mediaUpload(`/api/users/file-assets/`, file)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteUserFile(asset: string): Promise<any> {
|
||||||
|
return this.delete(`/api/users/file-assets/${asset}`)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async getUnsplashImages(page: number = 1, query?: string): Promise<UnSplashImage[]> {
|
async getUnsplashImages(page: number = 1, query?: string): Promise<UnSplashImage[]> {
|
||||||
const clientId = process.env.NEXT_PUBLIC_UNSPLASH_ACCESS;
|
const clientId = process.env.NEXT_PUBLIC_UNSPLASH_ACCESS;
|
||||||
const url = query
|
const url = query
|
||||||
@ -50,9 +74,7 @@ class FileServices extends APIService {
|
|||||||
method: "get",
|
method: "get",
|
||||||
url,
|
url,
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => response?.data?.results ?? response?.data)
|
||||||
return response?.data?.results ?? response?.data;
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
"NEXT_PUBLIC_SENTRY_ENVIRONMENT",
|
||||||
"NEXT_PUBLIC_GITHUB_APP_NAME",
|
"NEXT_PUBLIC_GITHUB_APP_NAME",
|
||||||
"NEXT_PUBLIC_ENABLE_SENTRY",
|
"NEXT_PUBLIC_ENABLE_SENTRY",
|
||||||
"NEXT_PUBLIC_ENABLE_OAUTH"
|
"NEXT_PUBLIC_ENABLE_OAUTH",
|
||||||
|
"NEXT_PUBLIC_UNSPLASH_ACCESS"
|
||||||
],
|
],
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"build": {
|
"build": {
|
||||||
|
Loading…
Reference in New Issue
Block a user