chore: update user file assets endpoint (#438)

* chore: new service for user assets

* chore: update user file assets endpoint
This commit is contained in:
Aaryan Khandelwal 2023-03-15 11:00:42 +05:30 committed by GitHub
parent dbd6de0988
commit bfab4865cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 149 additions and 78 deletions

View File

@ -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,12 +51,24 @@ 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({}));
if (userImage) {
fileServices
.uploadUserFile(formData)
.then((res) => {
const imageUrl = res.asset;
onSuccess(imageUrl);
setIsImageUploading(false);
})
.catch((err) => {
console.error(err);
});
} else
fileServices fileServices
.uploadFile(workspaceSlug as string, formData) .uploadFile(workspaceSlug as string, formData)
.then((res) => { .then((res) => {
@ -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

View File

@ -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>
<div className="flex items-center gap-2">
<Button <Button
type="button" type="button"
className="mt-4" className="mt-4"
onClick={() => { onClick={() => setIsImageUploadModalOpen(true)}
setIsImageUploadModalOpen(true);
}}
> >
Upload Upload new
</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">

View File

@ -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,

View File

@ -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));

View File

@ -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;
}); });

View File

@ -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": {