feat: state description in settings (#275)

* chore: removed minor bugs

* feat: state description in settings

* feat: group by assignee
This commit is contained in:
Aaryan Khandelwal 2023-02-14 14:46:48 +05:30 committed by GitHub
parent 9c8c7f1dda
commit e53ff4c02e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 188 additions and 208 deletions

View File

@ -12,80 +12,97 @@ import {
// helpers // helpers
import { addSpaceIfCamelCase } from "helpers/string.helper"; import { addSpaceIfCamelCase } from "helpers/string.helper";
// types // types
import { IIssue } from "types"; import { IIssue, IProjectMember, NestedKeyOf } from "types";
type Props = { type Props = {
isCollapsed: boolean;
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
groupedByIssues: { groupedByIssues: {
[key: string]: IIssue[]; [key: string]: IIssue[];
}; };
selectedGroup: NestedKeyOf<IIssue> | null;
groupTitle: string; groupTitle: string;
createdBy: string | null;
bgColor?: string; bgColor?: string;
addIssueToState: () => void; addIssueToState: () => void;
members: IProjectMember[] | undefined;
isCollapsed: boolean;
setIsCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
}; };
export const BoardHeader: React.FC<Props> = ({ export const BoardHeader: React.FC<Props> = ({
isCollapsed,
setIsCollapsed,
groupedByIssues, groupedByIssues,
selectedGroup,
groupTitle, groupTitle,
createdBy,
bgColor, bgColor,
addIssueToState, addIssueToState,
}) => ( isCollapsed,
<div setIsCollapsed,
className={`flex justify-between p-3 pb-0 ${ members,
!isCollapsed ? "flex-col rounded-md border bg-gray-50" : "" }) => {
}`} const createdBy =
> selectedGroup === "created_by"
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}> ? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "loading..."
<div : null;
className={`flex cursor-pointer items-center gap-x-1 rounded-md bg-slate-900 px-2 ${
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : "" let assignees: any;
}`} if (selectedGroup === "assignees") {
style={{ assignees = groupTitle.split(",");
border: `2px solid ${bgColor}`, assignees = assignees
backgroundColor: `${bgColor}20`, .map((a: string) => members?.find((m) => m.member.id === a)?.member.first_name)
}} .join(", ");
> }
<h2
className={`text-[0.9rem] font-medium capitalize`} return (
<div
className={`flex justify-between p-3 pb-0 ${
!isCollapsed ? "flex-col rounded-md border bg-gray-50" : ""
}`}
>
<div className={`flex items-center ${!isCollapsed ? "flex-col gap-2" : "gap-1"}`}>
<div
className={`flex cursor-pointer items-center gap-x-1 rounded-md bg-slate-900 px-2 ${
!isCollapsed ? "mb-2 flex-col gap-y-2 py-2" : ""
}`}
style={{ style={{
writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb", border: `2px solid ${bgColor}`,
backgroundColor: `${bgColor}20`,
}} }}
> >
{groupTitle === null || groupTitle === "null" <h2
? "None" className={`text-[0.9rem] font-medium capitalize`}
: createdBy style={{
? createdBy writingMode: !isCollapsed ? "vertical-rl" : "horizontal-tb",
: addSpaceIfCamelCase(groupTitle)} }}
</h2> >
<span className="ml-0.5 text-sm text-gray-500">{groupedByIssues[groupTitle].length}</span> {selectedGroup === "created_by"
? createdBy
: selectedGroup === "assignees"
? assignees
: addSpaceIfCamelCase(groupTitle)}
</h2>
<span className="ml-0.5 text-sm text-gray-500">{groupedByIssues[groupTitle].length}</span>
</div>
</div>
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200"
onClick={() => {
setIsCollapsed((prevData) => !prevData);
}}
>
{isCollapsed ? (
<ArrowsPointingInIcon className="h-4 w-4" />
) : (
<ArrowsPointingOutIcon className="h-4 w-4" />
)}
</button>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200"
onClick={addIssueToState}
>
<PlusIcon className="h-4 w-4" />
</button>
</div> </div>
</div> </div>
);
<div className={`flex items-center ${!isCollapsed ? "flex-col pb-2" : ""}`}> };
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200"
onClick={() => {
setIsCollapsed((prevData) => !prevData);
}}
>
{isCollapsed ? (
<ArrowsPointingInIcon className="h-4 w-4" />
) : (
<ArrowsPointingOutIcon className="h-4 w-4" />
)}
</button>
<button
type="button"
className="grid h-7 w-7 place-items-center rounded p-1 outline-none duration-300 hover:bg-gray-200"
onClick={addIssueToState}
>
<PlusIcon className="h-4 w-4" />
</button>
</div>
</div>
);

View File

@ -55,11 +55,6 @@ export const SingleBoard: React.FC<Props> = ({
const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string); const [properties] = useIssuesProperties(workspaceSlug as string, projectId as string);
const createdBy =
selectedGroup === "created_by"
? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "loading..."
: null;
if (selectedGroup === "priority") if (selectedGroup === "priority")
groupTitle === "high" groupTitle === "high"
? (bgColor = "#dc2626") ? (bgColor = "#dc2626")
@ -77,11 +72,12 @@ export const SingleBoard: React.FC<Props> = ({
<BoardHeader <BoardHeader
addIssueToState={addIssueToState} addIssueToState={addIssueToState}
bgColor={bgColor} bgColor={bgColor}
createdBy={createdBy} selectedGroup={selectedGroup}
groupTitle={groupTitle} groupTitle={groupTitle}
groupedByIssues={groupedByIssues} groupedByIssues={groupedByIssues}
isCollapsed={isCollapsed} isCollapsed={isCollapsed}
setIsCollapsed={setIsCollapsed} setIsCollapsed={setIsCollapsed}
members={members}
/> />
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}> <StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
{(provided, snapshot) => ( {(provided, snapshot) => (
@ -97,7 +93,9 @@ export const SingleBoard: React.FC<Props> = ({
key={issue.id} key={issue.id}
draggableId={issue.id} draggableId={issue.id}
index={index} index={index}
isDragDisabled={isNotAllowed || selectedGroup === "created_by"} isDragDisabled={
isNotAllowed || selectedGroup === "created_by" || selectedGroup === "assignees"
}
> >
{(provided, snapshot) => ( {(provided, snapshot) => (
<SingleBoardIssue <SingleBoardIssue

View File

@ -50,9 +50,17 @@ export const SingleList: React.FC<Props> = ({
const createdBy = const createdBy =
selectedGroup === "created_by" selectedGroup === "created_by"
? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "loading..." ? members?.find((m) => m.member.id === groupTitle)?.member.first_name ?? "Loading..."
: null; : null;
let assignees: any;
if (selectedGroup === "assignees") {
assignees = groupTitle.split(",");
assignees = assignees
.map((a: string) => members?.find((m) => m.member.id === a)?.member.first_name)
.join(", ");
}
return ( return (
<Disclosure key={groupTitle} as="div" defaultOpen> <Disclosure key={groupTitle} as="div" defaultOpen>
{({ open }) => ( {({ open }) => (
@ -67,10 +75,10 @@ export const SingleList: React.FC<Props> = ({
</span> </span>
{selectedGroup !== null ? ( {selectedGroup !== null ? (
<h2 className="font-medium capitalize leading-5"> <h2 className="font-medium capitalize leading-5">
{groupTitle === null || groupTitle === "null" {selectedGroup === "created_by"
? "None"
: createdBy
? createdBy ? createdBy
: selectedGroup === "assignees"
? assignees
: addSpaceIfCamelCase(groupTitle)} : addSpaceIfCamelCase(groupTitle)}
</h2> </h2>
) : ( ) : (

View File

@ -82,14 +82,14 @@ export const SidebarCycleSelect: React.FC<Props> = ({
{cycles ? ( {cycles ? (
cycles.length > 0 ? ( cycles.length > 0 ? (
<> <>
<CustomSelect.Option value={null} className="capitalize">
None
</CustomSelect.Option>
{cycles.map((option) => ( {cycles.map((option) => (
<CustomSelect.Option key={option.id} value={option.id}> <CustomSelect.Option key={option.id} value={option.id}>
{option.name} {option.name}
</CustomSelect.Option> </CustomSelect.Option>
))} ))}
<CustomSelect.Option value={null} className="capitalize">
None
</CustomSelect.Option>
</> </>
) : ( ) : (
<div className="text-center">No cycles found</div> <div className="text-center">No cycles found</div>

View File

@ -81,14 +81,14 @@ export const SidebarModuleSelect: React.FC<Props> = ({
{modules ? ( {modules ? (
modules.length > 0 ? ( modules.length > 0 ? (
<> <>
<CustomSelect.Option value={null} className="capitalize">
None
</CustomSelect.Option>
{modules.map((option) => ( {modules.map((option) => (
<CustomSelect.Option key={option.id} value={option.id}> <CustomSelect.Option key={option.id} value={option.id}>
{option.name} {option.name}
</CustomSelect.Option> </CustomSelect.Option>
))} ))}
<CustomSelect.Option value={null} className="capitalize">
None
</CustomSelect.Option>
</> </>
) : ( ) : (
<div className="text-center">No modules found</div> <div className="text-center">No modules found</div>

View File

@ -1,4 +1,4 @@
import React, { useCallback, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -144,6 +144,12 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
[workspaceSlug, projectId, issueId, issueDetail] [workspaceSlug, projectId, issueId, issueDetail]
); );
useEffect(() => {
if (!createLabelForm) return;
reset();
}, [createLabelForm, reset]);
const isNotAllowed = userAuth.isGuest || userAuth.isViewer; const isNotAllowed = userAuth.isGuest || userAuth.isViewer;
return ( return (
@ -431,24 +437,25 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
</Listbox> </Listbox>
)} )}
/> />
<button {!isNotAllowed && (
type="button" <button
className={`flex ${ type="button"
isNotAllowed ? "cursor-not-allowed" : "cursor-pointer hover:bg-gray-100" className={`flex ${
} items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs`} isNotAllowed ? "cursor-not-allowed" : "cursor-pointer hover:bg-gray-100"
onClick={() => setCreateLabelForm((prevData) => !prevData)} } items-center gap-1 rounded-2xl border px-2 py-0.5 text-xs`}
disabled={isNotAllowed} onClick={() => setCreateLabelForm((prevData) => !prevData)}
> >
{createLabelForm ? ( {createLabelForm ? (
<> <>
<XMarkIcon className="h-3 w-3" /> Cancel <XMarkIcon className="h-3 w-3" /> Cancel
</> </>
) : ( ) : (
<> <>
<PlusIcon className="h-3 w-3" /> New <PlusIcon className="h-3 w-3" /> New
</> </>
)} )}
</button> </button>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,9 +6,10 @@ import { useRouter } from "next/router";
// components // components
import { DeleteModuleModal } from "components/modules"; import { DeleteModuleModal } from "components/modules";
// ui
import { AssigneesList, Avatar } from "components/ui";
// icons // icons
import { CalendarDaysIcon, TrashIcon } from "@heroicons/react/24/outline"; import { CalendarDaysIcon, TrashIcon } from "@heroicons/react/24/outline";
import User from "public/user.png";
// helpers // helpers
import { renderShortNumericDateFormat } from "helpers/date-time.helper"; import { renderShortNumericDateFormat } from "helpers/date-time.helper";
// types // types
@ -21,10 +22,12 @@ type Props = {
}; };
export const SingleModuleCard: React.FC<Props> = ({ module }) => { export const SingleModuleCard: React.FC<Props> = ({ module }) => {
const router = useRouter();
const { workspaceSlug } = router.query;
const [moduleDeleteModal, setModuleDeleteModal] = useState(false); const [moduleDeleteModal, setModuleDeleteModal] = useState(false);
const [selectedModuleForDelete, setSelectedModuleForDelete] = useState<SelectModuleType>(); const [selectedModuleForDelete, setSelectedModuleForDelete] = useState<SelectModuleType>();
const router = useRouter();
const { workspaceSlug } = router.query;
const handleDeleteModule = () => { const handleDeleteModule = () => {
if (!module) return; if (!module) return;
@ -33,16 +36,7 @@ export const SingleModuleCard: React.FC<Props> = ({ module }) => {
}; };
return ( return (
<div className="group/card h-full w-full relative select-none p-2"> <>
<div className="absolute top-4 right-4 z-50 bg-red-200 opacity-0 group-hover/card:opacity-100">
<button
type="button"
className="grid h-7 w-7 place-items-center bg-white p-1 text-red-500 outline-none duration-300 hover:bg-red-50"
onClick={() => handleDeleteModule()}
>
<TrashIcon className="h-4 w-4" />
</button>
</div>
<DeleteModuleModal <DeleteModuleModal
isOpen={ isOpen={
moduleDeleteModal && moduleDeleteModal &&
@ -52,101 +46,55 @@ export const SingleModuleCard: React.FC<Props> = ({ module }) => {
setIsOpen={setModuleDeleteModal} setIsOpen={setModuleDeleteModal}
data={selectedModuleForDelete} data={selectedModuleForDelete}
/> />
<Link href={`/${workspaceSlug}/projects/${module.project}/modules/${module.id}`}> <div className="group/card h-full w-full relative select-none p-2">
<a className="flex flex-col cursor-pointer rounded-md border bg-white p-3 "> <div className="absolute top-4 right-4 z-50 bg-red-200 opacity-0 group-hover/card:opacity-100">
<span className="w-3/4 text-ellipsis overflow-hidden">{module.name}</span> <button
<div className="mt-4 grid grid-cols-2 gap-2 text-xs md:grid-cols-4"> type="button"
<div className="space-y-2"> className="grid h-7 w-7 place-items-center bg-white p-1 text-red-500 outline-none duration-300 hover:bg-red-50"
<h6 className="text-gray-500">LEAD</h6> onClick={() => handleDeleteModule()}
<div> >
{module.lead ? ( <TrashIcon className="h-4 w-4" />
module.lead_detail?.avatar && module.lead_detail.avatar !== "" ? ( </button>
<div className="h-5 w-5 rounded-full border-2 border-white"> </div>
<Image <Link href={`/${workspaceSlug}/projects/${module.project}/modules/${module.id}`}>
src={module.lead_detail.avatar} <a className="flex flex-col cursor-pointer rounded-md border bg-white p-3 ">
height="100%" <span className="w-3/4 text-ellipsis overflow-hidden">{module.name}</span>
width="100%" <div className="mt-4 grid grid-cols-2 gap-2 text-xs md:grid-cols-4">
className="rounded-full" <div className="space-y-2">
alt={module.lead_detail.first_name} <h6 className="text-gray-500">LEAD</h6>
/> <div>
</div> <Avatar user={module.lead_detail} />
) : ( </div>
<div className="grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 capitalize text-white"> </div>
{module.lead_detail?.first_name && module.lead_detail.first_name !== "" <div className="space-y-2">
? module.lead_detail.first_name.charAt(0) <h6 className="text-gray-500">MEMBERS</h6>
: module.lead_detail?.email.charAt(0)} <div className="flex items-center gap-1 text-xs">
</div> <AssigneesList users={module.members_detail} />
) </div>
) : ( </div>
"N/A" <div className="space-y-2">
)} <h6 className="text-gray-500">END DATE</h6>
<div className="flex w-min cursor-pointer items-center gap-1 whitespace-nowrap rounded border px-1.5 py-0.5 text-xs shadow-sm">
<CalendarDaysIcon className="h-3 w-3" />
{renderShortNumericDateFormat(module.target_date ?? "")}
</div>
</div>
<div className="space-y-2">
<h6 className="text-gray-500">STATUS</h6>
<div className="flex items-center gap-2 capitalize">
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: MODULE_STATUS.find((s) => s.value === module.status)?.color,
}}
/>
{module.status}
</div>
</div> </div>
</div> </div>
<div className="space-y-2"> </a>
<h6 className="text-gray-500">MEMBERS</h6> </Link>
<div className="flex items-center gap-1 text-xs"> </div>
{module.members && module.members.length > 0 ? ( </>
module?.members_detail?.map((member, index: number) => (
<div
key={index}
className={`relative z-[1] h-5 w-5 rounded-full ${
index !== 0 ? "-ml-2.5" : ""
}`}
>
{member?.avatar && member.avatar !== "" ? (
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
<Image
src={member.avatar}
height="100%"
width="100%"
className="rounded-full"
alt={member?.first_name}
/>
</div>
) : (
<div className="grid h-5 w-5 place-items-center rounded-full border-2 border-white bg-gray-700 capitalize text-white">
{member?.first_name && member.first_name !== ""
? member.first_name.charAt(0)
: member?.email?.charAt(0)}
</div>
)}
</div>
))
) : (
<div className="h-5 w-5 rounded-full border-2 border-white bg-white">
<Image
src={User}
height="100%"
width="100%"
className="rounded-full"
alt="No user"
/>
</div>
)}
</div>
</div>
<div className="space-y-2">
<h6 className="text-gray-500">END DATE</h6>
<div className="flex w-min cursor-pointer items-center gap-1 whitespace-nowrap rounded border px-1.5 py-0.5 text-xs shadow-sm">
<CalendarDaysIcon className="h-3 w-3" />
{renderShortNumericDateFormat(module.target_date ?? "")}
</div>
</div>
<div className="space-y-2">
<h6 className="text-gray-500">STATUS</h6>
<div className="flex items-center gap-2 capitalize">
<span
className="h-2 w-2 flex-shrink-0 rounded-full"
style={{
backgroundColor: MODULE_STATUS.find((s) => s.value === module.status)?.color,
}}
/>
{module.status}
</div>
</div>
</div>
</a>
</Link>
</div>
); );
}; };

View File

@ -142,13 +142,16 @@ export const SingleState: React.FC<Props> = ({
}`} }`}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div <span
className="h-3 w-3 flex-shrink-0 rounded-full" className="h-3 w-3 flex-shrink-0 rounded-full"
style={{ style={{
backgroundColor: state.color, backgroundColor: state.color,
}} }}
/> />
<h6 className="text-sm">{addSpaceIfCamelCase(state.name)}</h6> <div>
<h6 className="text-sm">{addSpaceIfCamelCase(state.name)}</h6>
<p className="text-xs text-gray-400">{state.description}</p>
</div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{index !== 0 && ( {index !== 0 && (

View File

@ -13,7 +13,7 @@ import { IUser, IUserLite } from "types";
import { WORKSPACE_MEMBERS } from "constants/fetch-keys"; import { WORKSPACE_MEMBERS } from "constants/fetch-keys";
type AvatarProps = { type AvatarProps = {
user: Partial<IUser> | Partial<IUserLite> | undefined; user?: Partial<IUser> | Partial<IUserLite> | IUser | IUserLite | undefined | null;
index?: number; index?: number;
}; };

View File

@ -1 +0,0 @@
export * from "./avatar";

View File

@ -1,4 +1,3 @@
// components
export * from "./button"; export * from "./button";
export * from "./custom-listbox"; export * from "./custom-listbox";
export * from "./custom-menu"; export * from "./custom-menu";
@ -11,6 +10,6 @@ export * from "./outline-button";
export * from "./select"; export * from "./select";
export * from "./spinner"; export * from "./spinner";
export * from "./text-area"; export * from "./text-area";
export * from "./tooltip";
export * from "./avatar"; export * from "./avatar";
export * from "./datepicker"; export * from "./datepicker";
export * from "./tooltip";

View File

@ -5,6 +5,7 @@ export const GROUP_BY_OPTIONS: Array<{ name: string; key: NestedKeyOf<IIssue> |
{ name: "State", key: "state_detail.name" }, { name: "State", key: "state_detail.name" },
{ name: "Priority", key: "priority" }, { name: "Priority", key: "priority" },
{ name: "Created By", key: "created_by" }, { name: "Created By", key: "created_by" },
{ name: "Assignee", key: "assignees" },
{ name: "None", key: null }, { name: "None", key: null },
]; ];

View File

@ -80,7 +80,7 @@ const StatesSettings: NextPage<UserAuth> = (props) => {
<div className="space-y-8"> <div className="space-y-8">
<div> <div>
<h3 className="text-3xl font-bold leading-6 text-gray-900">States</h3> <h3 className="text-3xl font-bold leading-6 text-gray-900">States</h3>
<p className="mt-4 text-sm text-gray-500">Manage the state of this project.</p> <p className="mt-4 text-sm text-gray-500">Manage the states of this project.</p>
</div> </div>
<div className="flex flex-col justify-between gap-4"> <div className="flex flex-col justify-between gap-4">
{states && projectDetails ? ( {states && projectDetails ? (