style: project setting ui revamp (#2177)

* style: project settings navigation sidebar added

* chore: emoji and image picker close on outside click added

* style: project setting general page revamp

* style: project setting member page revamp

* style: project setting features page revamp

* style: project setting state page revamp

* style: project setting integrations page revamp

* style: project setting estimates page revamp

* style: project setting automation page revamp

* style: project setting label page revamp

* chore: member select improvement for member setting page

* chore: toggle switch component improvement

* style: project automation setting ui improvement

* style: module icon added

* style: toggle switch improvement

* style: ui and spacing consistency

* style: project label setting revamp

* style: project state setting ui improvement

* chore: integration setting repo select validation

* chore: code refactor

* fix: build fix
This commit is contained in:
Anmol Singh Bhatia 2023-09-13 23:09:55 +05:30 committed by GitHub
parent d0f6ca3bac
commit 87abf3ccb1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 1090 additions and 876 deletions

View file

@ -1,9 +1,9 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { useRouter } from "next/router";
import Link from "next/link";
import useSWR from "swr";
import useSWR, { mutate } from "swr";
// services
import projectService from "services/project.service";
@ -13,22 +13,35 @@ import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
import useProjectMembers from "hooks/use-project-members";
import useProjectDetails from "hooks/use-project-details";
import { Controller, useForm } from "react-hook-form";
// layouts
import { ProjectAuthorizationWrapper } from "layouts/auth-layout";
// components
import ConfirmProjectMemberRemove from "components/project/confirm-project-member-remove";
import SendProjectInvitationModal from "components/project/send-project-invitation-modal";
import { SettingsHeader } from "components/project";
import { MemberSelect, SettingsSidebar } from "components/project";
// ui
import { CustomMenu, CustomSelect, Loader } from "components/ui";
import {
CustomMenu,
CustomSearchSelect,
CustomSelect,
Icon,
Loader,
PrimaryButton,
SecondaryButton,
} from "components/ui";
import { BreadcrumbItem, Breadcrumbs } from "components/breadcrumbs";
// icons
import { PlusIcon, XMarkIcon } from "@heroicons/react/24/outline";
// types
import type { NextPage } from "next";
import { IProject, IUserLite, IWorkspace } from "types";
// fetch-keys
import {
PROJECTS_LIST,
PROJECT_DETAILS,
PROJECT_INVITATIONS_WITH_EMAIL,
PROJECT_MEMBERS,
PROJECT_MEMBERS_WITH_EMAIL,
WORKSPACE_DETAILS,
} from "constants/fetch-keys";
@ -37,6 +50,11 @@ import { ROLE } from "constants/workspace";
// helper
import { truncateText } from "helpers/string.helper";
const defaultValues: Partial<IProject> = {
project_lead: null,
default_assignee: null,
};
const MembersSettings: NextPage = () => {
const [inviteModal, setInviteModal] = useState(false);
const [selectedRemoveMember, setSelectedRemoveMember] = useState<string | null>(null);
@ -55,11 +73,25 @@ const MembersSettings: NextPage = () => {
Boolean(workspaceSlug && projectId)
);
const {
handleSubmit,
reset,
control,
formState: { isSubmitting },
} = useForm<IProject>({ defaultValues });
const { data: activeWorkspace } = useSWR(
workspaceSlug ? WORKSPACE_DETAILS(workspaceSlug as string) : null,
() => (workspaceSlug ? workspaceService.getWorkspace(workspaceSlug as string) : null)
);
const { data: people } = useSWR(
workspaceSlug && projectId ? PROJECT_MEMBERS(projectId as string) : null,
workspaceSlug && projectId
? () => projectService.projectMembers(workspaceSlug as string, projectId as string)
: null
);
const { data: projectMembers, mutate: mutateMembers } = useSWR(
workspaceSlug && projectId
? PROJECT_MEMBERS_WITH_EMAIL(workspaceSlug.toString(), projectId.toString())
@ -110,6 +142,76 @@ const MembersSettings: NextPage = () => {
const handleProjectInvitationSuccess = () => {};
const onSubmit = async (formData: IProject) => {
if (!workspaceSlug || !projectId || !projectDetails) return;
const payload: Partial<IProject> = {
default_assignee: formData.default_assignee,
project_lead: formData.project_lead === "none" ? null : formData.project_lead,
};
await projectService
.updateProject(workspaceSlug as string, projectId as string, payload, user)
.then((res) => {
mutate(PROJECT_DETAILS(projectId as string));
mutate(
PROJECTS_LIST(workspaceSlug as string, {
is_favorite: "all",
})
);
setToastAlert({
title: "Success",
type: "success",
message: "Project updated successfully",
});
})
.catch((err) => {
console.log(err);
});
};
useEffect(() => {
if (projectDetails)
reset({
...projectDetails,
default_assignee: projectDetails.default_assignee?.id ?? projectDetails.default_assignee,
project_lead: (projectDetails.project_lead as IUserLite)?.id ?? projectDetails.project_lead,
workspace: (projectDetails.workspace as IWorkspace).id,
});
}, [projectDetails, reset]);
const submitChanges = async (formData: Partial<IProject>) => {
if (!workspaceSlug || !projectId) return;
const payload: Partial<IProject> = {
default_assignee: formData.default_assignee === "none" ? null : formData.default_assignee,
project_lead: formData.project_lead === "none" ? null : formData.project_lead,
};
await projectService
.updateProject(workspaceSlug as string, projectId as string, payload, user)
.then((res) => {
mutate(PROJECT_DETAILS(projectId as string));
mutate(
PROJECTS_LIST(workspaceSlug as string, {
is_favorite: "all",
})
);
setToastAlert({
title: "Success",
type: "success",
message: "Project updated successfully",
});
})
.catch((err) => {
console.log(err);
});
};
return (
<ProjectAuthorizationWrapper
breadcrumbs={
@ -171,19 +273,69 @@ const MembersSettings: NextPage = () => {
user={user}
onSuccess={() => mutateMembers()}
/>
<div className="p-8">
<SettingsHeader />
<section className="space-y-5">
<div className="flex items-end justify-between gap-4">
<h3 className="text-2xl font-semibold">Members</h3>
<button
type="button"
className="flex items-center gap-2 text-custom-primary outline-none"
onClick={() => setInviteModal(true)}
>
<PlusIcon className="h-4 w-4" />
Add Member
</button>
<div className="flex flex-row gap-2">
<div className="w-80 py-8">
<SettingsSidebar />
</div>
<section className="pr-9 py-8 w-full">
<div className="flex items-center py-3.5 border-b border-custom-border-200">
<h3 className="text-xl font-medium">Defaults</h3>
</div>
<div className="flex flex-col gap-2 pb-4 w-full">
<div className="flex items-center py-8 gap-4 w-full">
<div className="flex flex-col gap-2 w-1/2">
<h4 className="text-sm">Project Lead</h4>
<div className="">
{projectDetails ? (
<Controller
control={control}
name="project_lead"
render={({ field: { value } }) => (
<MemberSelect
value={value}
onChange={(val: string) => {
submitChanges({ project_lead: val });
}}
/>
)}
/>
) : (
<Loader className="h-9 w-full">
<Loader.Item width="100%" height="100%" />
</Loader>
)}
</div>
</div>
<div className="flex flex-col gap-2 w-1/2">
<h4 className="text-sm">Default Assignee</h4>
<div className="">
{projectDetails ? (
<Controller
control={control}
name="default_assignee"
render={({ field: { value } }) => (
<MemberSelect
value={value}
onChange={(val: string) => {
submitChanges({ default_assignee: val });
}}
/>
)}
/>
) : (
<Loader className="h-9 w-full">
<Loader.Item width="100%" height="100%" />
</Loader>
)}
</div>
</div>
</div>
</div>
<div className="flex items-center justify-between gap-4 py-3.5 border-b border-custom-border-200">
<h4 className="text-xl font-medium border-b border-custom-border-100">Members</h4>
<PrimaryButton onClick={() => setInviteModal(true)}>Add Member</PrimaryButton>
</div>
{!projectMembers || !projectInvitations ? (
<Loader className="space-y-5">
@ -193,10 +345,13 @@ const MembersSettings: NextPage = () => {
<Loader.Item height="40px" />
</Loader>
) : (
<div className="divide-y divide-custom-border-200 rounded-[10px] border border-custom-border-200 bg-custom-background-100 px-6">
<div className="divide-y divide-custom-border-200">
{members.length > 0
? members.map((member) => (
<div key={member.id} className="flex items-center justify-between py-6">
<div
key={member.id}
className="flex items-center justify-between px-3.5 py-[18px]"
>
<div className="flex items-center gap-x-6 gap-y-2">
{member.avatar && member.avatar !== "" ? (
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg p-4 capitalize text-white">
@ -242,7 +397,20 @@ const MembersSettings: NextPage = () => {
</div>
)}
<CustomSelect
label={ROLE[member.role as keyof typeof ROLE]}
customButton={
<button className="flex item-center gap-1">
<span
className={`flex items-center text-sm font-medium ${
member.memberId !== user?.id ? "" : "text-custom-sidebar-text-400"
}`}
>
{ROLE[member.role as keyof typeof ROLE]}
</span>
{member.memberId !== user?.id && (
<Icon iconName="expand_more" className="text-lg font-medium" />
)}
</button>
}
value={member.role}
onChange={(value: 5 | 10 | 15 | 20 | undefined) => {
if (!activeWorkspace || !projectDetails) return;
@ -306,7 +474,11 @@ const MembersSettings: NextPage = () => {
>
<span className="flex items-center justify-start gap-2">
<XMarkIcon className="h-4 w-4" />
<span>Remove member</span>
<span>
{" "}
{member.memberId !== user?.id ? "Remove member" : "Leave project"}
</span>
</span>
</CustomMenu.MenuItem>
</CustomMenu>