dev: profile page, issue details page design

This commit is contained in:
Aaryan Khandelwal 2022-11-24 19:18:18 +05:30
parent 2a57b111f0
commit dbf2a138b3
40 changed files with 2301 additions and 2034 deletions

View File

@ -36,6 +36,7 @@ type Props = {
>; >;
bgColor?: string; bgColor?: string;
stateId?: string; stateId?: string;
createdBy?: string;
}; };
const SingleBoard: React.FC<Props> = ({ const SingleBoard: React.FC<Props> = ({
@ -48,6 +49,7 @@ const SingleBoard: React.FC<Props> = ({
setPreloadedData, setPreloadedData,
bgColor = "#0f2b16", bgColor = "#0f2b16",
stateId, stateId,
createdBy,
}) => { }) => {
// Collapse/Expand // Collapse/Expand
const [show, setState] = useState<any>(true); const [show, setState] = useState<any>(true);
@ -118,6 +120,8 @@ const SingleBoard: React.FC<Props> = ({
> >
{groupTitle === null || groupTitle === "null" {groupTitle === null || groupTitle === "null"
? "None" ? "None"
: createdBy
? createdBy
: addSpaceIfCamelCase(groupTitle)} : addSpaceIfCamelCase(groupTitle)}
</h2> </h2>
<span className="text-gray-500 text-sm ml-0.5"> <span className="text-gray-500 text-sm ml-0.5">
@ -280,7 +284,7 @@ const SingleBoard: React.FC<Props> = ({
</div> </div>
) : ( ) : (
<div <div
className={`h-5 w-5 bg-gray-500 text-white border-2 border-white grid place-items-center rounded-full`} className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
> >
{assignee.first_name.charAt(0)} {assignee.first_name.charAt(0)}
</div> </div>

View File

@ -18,9 +18,9 @@ import SingleBoard from "components/project/issues/BoardView/SingleBoard";
import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import StrictModeDroppable from "components/dnd/StrictModeDroppable";
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
// ui // ui
import { Spinner, Button } from "ui"; import { Spinner } from "ui";
// types // types
import type { IState, IIssue, Properties, NestedKeyOf } from "types"; import type { IState, IIssue, Properties, NestedKeyOf, ProjectMember } from "types";
type Props = { type Props = {
properties: Properties; properties: Properties;
@ -28,9 +28,10 @@ type Props = {
groupedByIssues: { groupedByIssues: {
[key: string]: IIssue[]; [key: string]: IIssue[];
}; };
members: ProjectMember[] | undefined;
}; };
const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues }) => { const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues, members }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isIssueOpen, setIsIssueOpen] = useState(false); const [isIssueOpen, setIsIssueOpen] = useState(false);
@ -164,7 +165,7 @@ const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues
/> />
{groupedByIssues ? ( {groupedByIssues ? (
groupedByIssues ? ( groupedByIssues ? (
<div className="h-full w-full"> <div className="w-full" style={{ height: "calc(82vh - 1.5rem)" }}>
<DragDropContext onDragEnd={handleOnDragEnd}> <DragDropContext onDragEnd={handleOnDragEnd}>
<div className="h-full w-full overflow-hidden"> <div className="h-full w-full overflow-hidden">
<StrictModeDroppable droppableId="state" type="state" direction="horizontal"> <StrictModeDroppable droppableId="state" type="state" direction="horizontal">
@ -180,6 +181,12 @@ const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues
key={singleGroup} key={singleGroup}
selectedGroup={selectedGroup} selectedGroup={selectedGroup}
groupTitle={singleGroup} groupTitle={singleGroup}
createdBy={
members
? members?.find((m) => m.member.id === singleGroup)?.member
.first_name
: undefined
}
groupedByIssues={groupedByIssues} groupedByIssues={groupedByIssues}
index={index} index={index}
setIsIssueOpen={setIsIssueOpen} setIsIssueOpen={setIsIssueOpen}

View File

@ -72,7 +72,7 @@ const SelectAssignee: React.FC<Props> = ({ control }) => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="absolute mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none"> <Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<div className="p-1"> <div className="p-1">
{people?.map((person) => ( {people?.map((person) => (
<Listbox.Option <Listbox.Option

View File

@ -46,7 +46,7 @@ const SelectSprint: React.FC<Props> = ({ control, setIsOpen }) => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="absolute mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none"> <Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<div className="p-1"> <div className="p-1">
{sprints?.map((sprint) => ( {sprints?.map((sprint) => (
<Listbox.Option <Listbox.Option
@ -63,16 +63,6 @@ const SelectSprint: React.FC<Props> = ({ control, setIsOpen }) => {
<span className={`block ${selected && "font-semibold"}`}> <span className={`block ${selected && "font-semibold"}`}>
{sprint.name} {sprint.name}
</span> </span>
{selected && (
<span
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
active ? "text-white" : "text-indigo-600"
}`}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
)}
</> </>
)} )}
</Listbox.Option> </Listbox.Option>

View File

@ -98,7 +98,7 @@ const SelectLabels: React.FC<Props> = ({ control }) => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="absolute mt-1 bg-white shadow-lg max-h-28 rounded-md text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none"> <Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<div className="p-1"> <div className="p-1">
{issueLabels?.map((label) => ( {issueLabels?.map((label) => (
<Listbox.Option <Listbox.Option
@ -121,18 +121,6 @@ const SelectLabels: React.FC<Props> = ({ control }) => {
> >
{label.name} {label.name}
</span> </span>
{selected ? (
<span
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
active || (value ?? []).some((i) => i === label.id)
? "text-white"
: "text-indigo-600"
}`}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</> </>
)} )}
</Listbox.Option> </Listbox.Option>

View File

@ -52,7 +52,7 @@ const SelectParent: React.FC<Props> = ({ control }) => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="absolute mt-1 bg-white shadow-lg max-h-28 max-w-[15rem] rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none"> <Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 max-w-[15rem] rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<div className="p-1"> <div className="p-1">
{projectIssues?.results?.map((issue) => ( {projectIssues?.results?.map((issue) => (
<Listbox.Option <Listbox.Option

View File

@ -39,7 +39,7 @@ const SelectPriority: React.FC<Props> = ({ control }) => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="absolute mt-1 w-full bg-white shadow-lg max-h-28 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-xs"> <Listbox.Options className="absolute z-10 mt-1 w-full w-[5rem] bg-white shadow-lg max-h-28 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-xs">
<div className="p-1"> <div className="p-1">
{PRIORITIES.map((priority) => ( {PRIORITIES.map((priority) => (
<Listbox.Option <Listbox.Option
@ -55,21 +55,11 @@ const SelectPriority: React.FC<Props> = ({ control }) => {
<> <>
<span <span
className={`block capitalize ${ className={`block capitalize ${
selected ? "font-semibold" : "font-normal" selected ? "font-medium" : "font-normal"
}`} }`}
> >
{priority} {priority}
</span> </span>
{selected ? (
<span
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
active ? "text-white" : "text-indigo-600"
}`}
>
<CheckIcon className="h-5 w-5" aria-hidden="true" />
</span>
) : null}
</> </>
)} )}
</Listbox.Option> </Listbox.Option>

View File

@ -51,7 +51,7 @@ const SelectProject: React.FC<Props> = ({ control }) => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="absolute mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none"> <Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<div className="p-1"> <div className="p-1">
{projects ? ( {projects ? (
projects.length > 0 ? ( projects.length > 0 ? (

View File

@ -49,7 +49,7 @@ const SelectState: React.FC<Props> = ({ control, data, setIsOpen }) => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="absolute mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none"> <Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<div className="p-1"> <div className="p-1">
{states ? ( {states ? (
states.filter((i) => i.id !== data?.id).length > 0 ? ( states.filter((i) => i.id !== data?.id).length > 0 ? (

View File

@ -135,7 +135,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
setToastAlert({ setToastAlert({
title: "Success", title: "Success",
type: "success", type: "success",
message: "Issue added to sprint successfully", message: "Issue added to cycle successfully",
}); });
}) })
.catch((err) => { .catch((err) => {
@ -325,7 +325,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
register={register} register={register}
/> />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center flex-wrap gap-2">
<SelectState control={control} setIsOpen={setIsStateModalOpen} /> <SelectState control={control} setIsOpen={setIsStateModalOpen} />
<SelectCycles control={control} setIsOpen={setIsCycleModalOpen} /> <SelectCycles control={control} setIsOpen={setIsCycleModalOpen} />
<SelectPriority control={control} /> <SelectPriority control={control} />

View File

@ -9,7 +9,15 @@ import { Listbox, Transition } from "@headlessui/react";
// icons // icons
import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline"; import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
// types // types
import { IIssue, IssueResponse, IState, NestedKeyOf, Properties, WorkspaceMember } from "types"; import {
IIssue,
IssueResponse,
IState,
NestedKeyOf,
ProjectMember,
Properties,
WorkspaceMember,
} from "types";
// hooks // hooks
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
// fetch keys // fetch keys
@ -32,6 +40,7 @@ type Props = {
selectedGroup: NestedKeyOf<IIssue> | null; selectedGroup: NestedKeyOf<IIssue> | null;
setSelectedIssue: any; setSelectedIssue: any;
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>; handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
members: ProjectMember[] | undefined;
}; };
const PRIORITIES = ["high", "medium", "low"]; const PRIORITIES = ["high", "medium", "low"];
@ -42,6 +51,7 @@ const ListView: React.FC<Props> = ({
selectedGroup, selectedGroup,
setSelectedIssue, setSelectedIssue,
handleDeleteIssue, handleDeleteIssue,
members,
}) => { }) => {
const { activeWorkspace, activeProject, states } = useUser(); const { activeWorkspace, activeProject, states } = useUser();
@ -71,7 +81,6 @@ const ListView: React.FC<Props> = ({
); );
return ( return (
<div className="mt-4 flex flex-col">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<div className="inline-block min-w-full p-0.5 align-middle"> <div className="inline-block min-w-full p-0.5 align-middle">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg"> <div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
@ -123,8 +132,7 @@ const ListView: React.FC<Props> = ({
...(issue?.assignees_list ?? []), ...(issue?.assignees_list ?? []),
...(issue?.assignees ?? []), ...(issue?.assignees ?? []),
]?.map( ]?.map(
(assignee) => (assignee) => people?.find((p) => p.member.id === assignee)?.member.email
people?.find((p) => p.member.id === assignee)?.member.email
); );
return ( return (
@ -275,9 +283,7 @@ const ListView: React.FC<Props> = ({
> >
<div <div
className={`flex items-center ${ className={`flex items-center ${
assignees.includes( assignees.includes(person.member.email)
person.member.email
)
? "font-medium" ? "font-medium"
: "font-normal" : "font-normal"
}`} }`}
@ -404,7 +410,6 @@ const ListView: React.FC<Props> = ({
</div> </div>
</div> </div>
</div> </div>
</div>
); );
}; };

View File

@ -19,11 +19,20 @@ import {
PROJECT_ISSUE_LABELS, PROJECT_ISSUE_LABELS,
} from "constants/fetch-keys"; } from "constants/fetch-keys";
// commons // commons
import { classNames } from "constants/common"; import { classNames, copyTextToClipboard } from "constants/common";
// ui // ui
import { Input, Button } from "ui"; import { Input, Button } from "ui";
// icons // icons
import { Bars3BottomRightIcon, PlusIcon, UserIcon, TagIcon } from "@heroicons/react/24/outline"; import {
UserIcon,
TagIcon,
UserGroupIcon,
ChevronDownIcon,
Squares2X2Icon,
ChartBarIcon,
ClipboardDocumentIcon,
LinkIcon,
} from "@heroicons/react/24/outline";
// types // types
import type { Control } from "react-hook-form"; import type { Control } from "react-hook-form";
import type { IIssue, IIssueLabels, IssueResponse, IState, WorkspaceMember } from "types"; import type { IIssue, IIssueLabels, IssueResponse, IState, WorkspaceMember } from "types";
@ -31,6 +40,7 @@ import type { IIssue, IIssueLabels, IssueResponse, IState, WorkspaceMember } fro
type Props = { type Props = {
control: Control<IIssue, any>; control: Control<IIssue, any>;
submitChanges: (formData: Partial<IIssue>) => void; submitChanges: (formData: Partial<IIssue>) => void;
issueDetail: IIssue | undefined;
}; };
const PRIORITIES = ["high", "medium", "low"]; const PRIORITIES = ["high", "medium", "low"];
@ -39,7 +49,7 @@ const defaultValues: Partial<IIssueLabels> = {
name: "", name: "",
}; };
const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => { const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDetail }) => {
const { activeWorkspace, activeProject } = useUser(); const { activeWorkspace, activeProject } = useUser();
const { data: states } = useSWR<IState[]>( const { data: states } = useSWR<IState[]>(
@ -90,16 +100,12 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
}); });
}; };
return ( const sidebarOptions = [
<div className="w-full h-full">
<div className="space-y-3">
<div className="flex flex-col gap-y-4">
{[
{ {
label: "Priority", label: "Priority",
name: "priority", name: "priority",
canSelectMultipleOptions: false, canSelectMultipleOptions: false,
icon: Bars3BottomRightIcon, icon: ChartBarIcon,
options: PRIORITIES.map((property) => ({ options: PRIORITIES.map((property) => ({
label: property, label: property,
value: property, value: property,
@ -109,7 +115,7 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
label: "Status", label: "Status",
name: "state", name: "state",
canSelectMultipleOptions: false, canSelectMultipleOptions: false,
icon: Bars3BottomRightIcon, icon: Squares2X2Icon,
options: states?.map((state) => ({ options: states?.map((state) => ({
label: state.name, label: state.name,
value: state.id, value: state.id,
@ -119,7 +125,7 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
label: "Assignees", label: "Assignees",
name: "assignees_list", name: "assignees_list",
canSelectMultipleOptions: true, canSelectMultipleOptions: true,
icon: UserIcon, icon: UserGroupIcon,
options: people?.map((person) => ({ options: people?.map((person) => ({
label: person.member.first_name, label: person.member.first_name,
value: person.member.id, value: person.member.id,
@ -145,10 +151,37 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
value: issue.id, value: issue.id,
})), })),
}, },
].map((item) => ( ];
<div className="flex items-center gap-x-2" key={item.label}>
<div className="flex items-center gap-x-2"> return (
<item.icon className="w-5 h-5 text-gray-500" /> <div className="h-full w-full">
<div className="space-y-3">
<div className="flex flex-col gap-y-4">
<h3 className="text-lg font-medium leading-6 text-gray-900">Quick Actions</h3>
<div className="flex items-center gap-2 flex-wrap">
<button
type="button"
className="p-2 hover:bg-gray-100 border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
onClick={() =>
copyTextToClipboard(
`https://app.plane.so/projects/${activeProject?.id}/issues/${issueDetail?.id}`
)
}
>
<LinkIcon className="h-3.5 w-3.5" />
</button>
<button
type="button"
className="p-2 hover:bg-gray-100 border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
onClick={() => copyTextToClipboard(`${issueDetail?.id}`)}
>
<ClipboardDocumentIcon className="h-3.5 w-3.5" />
</button>
</div>
{sidebarOptions.map((item) => (
<div className="flex items-center justify-between gap-x-2" key={item.label}>
<div className="flex items-center gap-x-2 text-sm">
<item.icon className="h-4 w-4" />
<p>{item.label}</p> <p>{item.label}</p>
</div> </div>
<div> <div>
@ -160,22 +193,17 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
as="div" as="div"
value={value} value={value}
multiple={item.canSelectMultipleOptions} multiple={item.canSelectMultipleOptions}
onChange={(value) => submitChanges({ [item.name]: value })} onChange={(value: any) => submitChanges({ [item.name]: value })}
className="flex-shrink-0" className="flex-shrink-0"
> >
{({ open }) => ( {({ open }) => (
<>
<Listbox.Label className="sr-only">{item.label}</Listbox.Label>
<div className="relative"> <div className="relative">
<Listbox.Button className="relative inline-flex items-center whitespace-nowrap rounded-full bg-gray-50 py-2 px-2 text-sm font-medium text-gray-500 hover:bg-gray-100 sm:px-3 border border-dashed"> <Listbox.Button className="relative flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-sm duration-300">
<PlusIcon
className="h-5 w-5 flex-shrink-0 text-gray-300 sm:-ml-1"
aria-hidden="true"
/>
<span <span
className={classNames( className={classNames(
value ? "" : "text-gray-900", value ? "" : "text-gray-900",
"hidden truncate capitalize sm:ml-2 sm:block w-16" "hidden truncate sm:block w-16 text-left",
item.label === "Priority" ? "capitalize" : ""
)} )}
> >
{value {value
@ -183,13 +211,13 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
? value ? value
.map( .map(
(i: any) => (i: any) =>
item.options?.find((option) => option.value === i) item.options?.find((option) => option.value === i)?.label
?.label
) )
.join(", ") || `Select ${item.label}` .join(", ") || item.label
: item.options?.find((option) => option.value === value)?.label : item.options?.find((option) => option.value === value)?.label
: `Select ${item.label}`} : "None"}
</span> </span>
<ChevronDownIcon className="h-3 w-3" />
</Listbox.Button> </Listbox.Button>
<Transition <Transition
@ -199,29 +227,27 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-56 w-52 overflow-auto rounded-lg bg-white py-3 text-base shadow ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"> <Listbox.Options className="absolute z-10 right-0 mt-1 w-40 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<div className="p-1">
{item.options?.map((option) => ( {item.options?.map((option) => (
<Listbox.Option <Listbox.Option
key={option.value} key={option.value}
className={({ active, selected }) => className={({ active, selected }) =>
classNames( `${
active || selected ? "bg-indigo-50" : "bg-white", active || selected ? "text-white bg-theme" : "text-gray-900"
"relative cursor-default select-none py-2 px-3" } ${
) item.label === "Priority" && "capitalize"
} cursor-pointer select-none relative p-2 rounded-md truncate`
} }
value={option.value} value={option.value}
> >
<div className="flex items-center">
<span className="ml-3 block capitalize font-medium">
{option.label} {option.label}
</span>
</div>
</Listbox.Option> </Listbox.Option>
))} ))}
</div>
</Listbox.Options> </Listbox.Options>
</Transition> </Transition>
</div> </div>
</>
)} )}
</Listbox> </Listbox>
)} )}
@ -230,11 +256,11 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
</div> </div>
))} ))}
<div> <div>
<form className="flex" onSubmit={handleSubmit(onSubmit)}> <form className="flex items-center gap-x-2" onSubmit={handleSubmit(onSubmit)}>
<Input <Input
id="name" id="name"
name="name" name="name"
placeholder="Add label" placeholder="Add new label"
register={register} register={register}
validations={{ validations={{
required: false, required: false,
@ -246,9 +272,9 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
</Button> </Button>
</form> </form>
</div> </div>
<div className="flex items-center gap-x-2"> <div className="flex justify-between items-center gap-x-2">
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2 text-sm">
<TagIcon className="w-5 h-5 text-gray-500" /> <TagIcon className="w-4 h-4" />
<p>Label</p> <p>Label</p>
</div> </div>
<div> <div>
@ -267,15 +293,11 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
<> <>
<Listbox.Label className="sr-only">Label</Listbox.Label> <Listbox.Label className="sr-only">Label</Listbox.Label>
<div className="relative"> <div className="relative">
<Listbox.Button className="relative inline-flex items-center whitespace-nowrap rounded-full bg-gray-50 py-2 px-2 text-sm font-medium text-gray-500 hover:bg-gray-100 sm:px-3 border border-dashed"> <Listbox.Button className="relative flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-sm duration-300">
<PlusIcon
className="h-5 w-5 flex-shrink-0 text-gray-300 sm:-ml-1"
aria-hidden="true"
/>
<span <span
className={classNames( className={classNames(
value ? "" : "text-gray-900", value ? "" : "text-gray-900",
"hidden truncate capitalize sm:ml-2 sm:block w-16" "hidden truncate capitalize sm:block w-16 text-left"
)} )}
> >
{value && value.length > 0 {value && value.length > 0
@ -285,8 +307,9 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
issueLabels?.find((option) => option.id === i)?.name issueLabels?.find((option) => option.id === i)?.name
) )
.join(", ") .join(", ")
: `Select label`} : "None"}
</span> </span>
<ChevronDownIcon className="h-3 w-3" />
</Listbox.Button> </Listbox.Button>
<Transition <Transition
@ -296,25 +319,22 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
leaveFrom="opacity-100" leaveFrom="opacity-100"
leaveTo="opacity-0" leaveTo="opacity-0"
> >
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-56 w-52 overflow-auto rounded-lg bg-white py-3 text-base shadow ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm"> <Listbox.Options className="absolute z-10 right-0 mt-1 w-40 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
<div className="p-1">
{issueLabels?.map((label: any) => ( {issueLabels?.map((label: any) => (
<Listbox.Option <Listbox.Option
key={label.id} key={label.id}
className={({ active, selected }) => className={({ active, selected }) =>
classNames( `${
active || selected ? "bg-indigo-50" : "bg-white", active || selected ? "text-white bg-theme" : "text-gray-900"
"relative cursor-default select-none py-2 px-3" } cursor-pointer select-none relative p-2 rounded-md truncate`
)
} }
value={label.id} value={label.id}
> >
<div className="flex items-center">
<span className="ml-3 block capitalize font-medium">
{label.name} {label.name}
</span>
</div>
</Listbox.Option> </Listbox.Option>
))} ))}
</div>
</Listbox.Options> </Listbox.Options>
</Transition> </Transition>
</div> </div>

View File

@ -0,0 +1,122 @@
// next
import Image from "next/image";
import {
ChartBarIcon,
ChatBubbleBottomCenterTextIcon,
Squares2X2Icon,
} from "@heroicons/react/24/outline";
import { addSpaceIfCamelCase, timeAgo } from "constants/common";
import { IState } from "types";
import { Spinner } from "ui";
type Props = {
issueActivities: any[] | undefined;
states: IState[] | undefined;
};
const activityIcons = {
state: <Squares2X2Icon className="h-4 w-4" />,
priority: <ChartBarIcon className="h-4 w-4" />,
name: <ChatBubbleBottomCenterTextIcon className="h-4 w-4" />,
description: <ChatBubbleBottomCenterTextIcon className="h-4 w-4" />,
};
const IssueActivitySection: React.FC<Props> = ({ issueActivities, states }) => {
return (
<>
{issueActivities ? (
<div className="space-y-3">
{issueActivities.map((activity) => {
if (activity.field !== "updated_by")
return (
<div key={activity.id} className="relative flex gap-x-2 w-full">
{issueActivities.length > 1 ? (
<span
className="absolute top-5 left-2.5 h-full w-0.5 bg-gray-200"
aria-hidden="true"
/>
) : null}
{activity.field ? (
<div className="relative z-10 flex-shrink-0 -ml-1">
<div
className={`h-7 w-7 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
>
{activityIcons[activity.field as keyof typeof activityIcons]}
</div>
</div>
) : (
<div className="relative z-10 flex-shrink-0 border-2 border-white -ml-1.5">
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
<Image
src={activity.actor_detail.avatar}
alt={activity.actor_detail.name}
height={30}
width={30}
className="rounded-full"
/>
) : (
<div
className={`h-8 w-8 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
>
{activity.actor_detail.first_name.charAt(0)}
</div>
)}
</div>
)}
<div className="w-full">
<p>
{activity.actor_detail.first_name} {activity.actor_detail.last_name}{" "}
<span>{activity.verb}</span>{" "}
{activity.verb !== "created" ? (
<span>{activity.field ?? "commented"}</span>
) : (
" this issue"
)}
</p>
<p className="text-xs text-gray-500">{timeAgo(activity.created_at)}</p>
<div className="w-full mt-2">
{activity.verb !== "created" && (
<div className="text-sm">
<div>
From:{" "}
<span className="text-gray-500">
{activity.field === "state"
? activity.old_value
? addSpaceIfCamelCase(
states?.find((s) => s.id === activity.old_value)?.name ?? ""
)
: "None"
: activity.old_value}
</span>
</div>
<div>
To:{" "}
<span className="text-gray-500">
{activity.field === "state"
? activity.new_value
? addSpaceIfCamelCase(
states?.find((s) => s.id === activity.new_value)?.name ?? ""
)
: "None"
: activity.new_value}
</span>
</div>
</div>
)}
</div>
</div>
</div>
);
})}
</div>
) : (
<div className="w-full h-full flex justify-center items-center">
<Spinner />
</div>
)}
</>
);
};
export default IssueActivitySection;

View File

@ -8,11 +8,14 @@ import issuesServices from "lib/services/issues.services";
// fetch keys // fetch keys
import { PROJECT_ISSUES_COMMENTS } from "constants/fetch-keys"; import { PROJECT_ISSUES_COMMENTS } from "constants/fetch-keys";
// components // components
import CommentCard from "components/project/issues/comment/IssueCommentCard"; import CommentCard from "components/project/issues/issue-detail/comment/IssueCommentCard";
// ui // ui
import { TextArea, Button, Spinner } from "ui"; import { TextArea, Button, Spinner } from "ui";
// types // types
import type { IIssueComment } from "types"; import type { IIssueComment } from "types";
// icons
import UploadingIcon from "public/animated-icons/uploading.json";
type Props = { type Props = {
comments?: IIssueComment[]; comments?: IIssueComment[];
workspaceSlug: string; workspaceSlug: string;
@ -67,9 +70,9 @@ const IssueCommentSection: React.FC<Props> = ({ comments, issueId, projectId, wo
}; };
return ( return (
<div className="space-y-3 px-2"> <div className="space-y-5">
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<div className="p-2 bg-indigo-50 rounded-md mb-6"> <div className="p-2 bg-indigo-50 rounded-md">
<div className="w-full"> <div className="w-full">
<TextArea <TextArea
id="comment" id="comment"
@ -99,6 +102,7 @@ const IssueCommentSection: React.FC<Props> = ({ comments, issueId, projectId, wo
<div className="w-full flex justify-end"> <div className="w-full flex justify-end">
<Button type="submit" disabled={isSubmitting}> <Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Adding comment..." : "Add comment"} {isSubmitting ? "Adding comment..." : "Add comment"}
{/* <UploadingIcon /> */}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -107,3 +107,36 @@ export const addSpaceIfCamelCase = (str: string) => {
export const replaceUnderscoreIfSnakeCase = (str: string) => { export const replaceUnderscoreIfSnakeCase = (str: string) => {
return str.replace(/_/g, " "); return str.replace(/_/g, " ");
}; };
const fallbackCopyTextToClipboard = (text: string) => {
var textArea = document.createElement("textarea");
textArea.value = text;
// Avoid scrolling to bottom
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
// FIXME: Even though we are using this as a fallback, execCommand is deprecated 👎. We should find a better way to do this.
// https://developer.mozilla.org/en-US/docs/Web/API/Document/execCommand
var successful = document.execCommand("copy");
var msg = successful ? "successful" : "unsuccessful";
console.log("Fallback: Copying text command was " + msg);
} catch (err) {
console.error("Fallback: Oops, unable to copy", err);
}
document.body.removeChild(textArea);
};
export const copyTextToClipboard = async (text: string) => {
if (!navigator.clipboard) {
fallbackCopyTextToClipboard(text);
return;
}
await navigator.clipboard.writeText(text);
};

View File

@ -43,7 +43,7 @@ type ReducerFunctionType = (state: StateType, action: ReducerActionType) => Stat
export const initialState: StateType = { export const initialState: StateType = {
collapsed: false, collapsed: false,
issueView: null, issueView: "list",
groupByProperty: null, groupByProperty: null,
}; };

View File

@ -1,16 +1,22 @@
import React from "react"; // react
import React, { useState } from "react";
// layouts // layouts
import Container from "layouts/Container"; import Container from "layouts/Container";
import Sidebar from "layouts/Navbar/Sidebar";
// components
import CreateProjectModal from "components/project/CreateProjectModal";
// types // types
import type { Props } from "./types"; import type { Props } from "./types";
const AdminLayout: React.FC<Props> = ({ meta, children }) => { const AdminLayout: React.FC<Props> = ({ meta, children }) => {
const [isOpen, setIsOpen] = useState(false);
return ( return (
<Container meta={meta}> <Container meta={meta}>
<div className="w-full h-screen overflow-auto"> <CreateProjectModal isOpen={isOpen} setIsOpen={setIsOpen} />
<>{children}</> <div className="h-screen w-full flex overflow-x-hidden">
<Sidebar />
<main className="h-full w-full min-w-0 p-5 bg-primary overflow-y-auto">{children}</main>
</div> </div>
</Container> </Container>
); );

View File

@ -1,6 +1,9 @@
// next
import Head from "next/head"; import Head from "next/head";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// types
import type { Props } from "./types";
// constants
import { import {
SITE_NAME, SITE_NAME,
SITE_DESCRIPTION, SITE_DESCRIPTION,
@ -10,9 +13,6 @@ import {
SITE_TITLE, SITE_TITLE,
} from "constants/seo/seo-variables"; } from "constants/seo/seo-variables";
// types
import type { Props } from "./types";
const Container = ({ meta, children }: Props) => { const Container = ({ meta, children }: Props) => {
const router = useRouter(); const router = useRouter();
const image = meta?.image || "/site-image.png"; const image = meta?.image || "/site-image.png";
@ -31,35 +31,16 @@ const Container = ({ meta, children }: Props) => {
<meta property="og:description" content={description} /> <meta property="og:description" content={description} />
<meta name="keywords" content={SITE_KEYWORDS} /> <meta name="keywords" content={SITE_KEYWORDS} />
<meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} /> <meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} />
<meta <meta name="twitter:card" content={image ? "summary_large_image" : "summary"} />
name="twitter:card" <link rel="apple-touch-icon" sizes="180x180" href="/favicon/apple-touch-icon.png" />
content={image ? "summary_large_image" : "summary"} <link rel="icon" type="image/png" sizes="32x32" href="/favicon/favicon-32x32.png" />
/> <link rel="icon" type="image/png" sizes="16x16" href="/favicon/favicon-16x16.png" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/favicon/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/favicon/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/favicon/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest.json" /> <link rel="manifest" href="/site.webmanifest.json" />
<link rel="shortcut icon" href="/favicon/favicon.ico" /> <link rel="shortcut icon" href="/favicon/favicon.ico" />
{image && ( {image && (
<meta <meta
property="og:image" property="og:image"
content={ content={image.startsWith("https://") ? image : `${SITE_URL}${image}`}
image.startsWith("https://") ? image : `${SITE_URL}${image}`
}
/> />
)} )}
</Head> </Head>

View File

@ -27,6 +27,7 @@ import {
XMarkIcon, XMarkIcon,
InboxIcon, InboxIcon,
ArrowLongLeftIcon, ArrowLongLeftIcon,
QuestionMarkCircleIcon,
} from "@heroicons/react/24/outline"; } from "@heroicons/react/24/outline";
// constants // constants
import { classNames } from "constants/common"; import { classNames } from "constants/common";
@ -57,7 +58,7 @@ const navigation = (projectId: string) => [
}, },
]; ];
const navLinks = [ const workspaceLinks = [
{ {
icon: HomeIcon, icon: HomeIcon,
name: "Home", name: "Home",
@ -116,7 +117,7 @@ const Sidebar: React.FC = () => {
const { collapsed: sidebarCollapse, toggleCollapsed } = useTheme(); const { collapsed: sidebarCollapse, toggleCollapsed } = useTheme();
return ( return (
<nav className="h-screen"> <nav className="h-full">
<CreateProjectModal isOpen={isCreateProjectModal} setIsOpen={setCreateProjectModal} /> <CreateProjectModal isOpen={isCreateProjectModal} setIsOpen={setCreateProjectModal} />
<Transition.Root show={sidebarOpen} as={React.Fragment}> <Transition.Root show={sidebarOpen} as={React.Fragment}>
<Dialog as="div" className="relative z-40 md:hidden" onClose={setSidebarOpen}> <Dialog as="div" className="relative z-40 md:hidden" onClose={setSidebarOpen}>
@ -202,7 +203,7 @@ const Sidebar: React.FC = () => {
sidebarCollapse ? "" : "w-auto md:w-64" sidebarCollapse ? "" : "w-auto md:w-64"
} hidden md:inset-y-0 md:flex md:flex-col h-full`} } hidden md:inset-y-0 md:flex md:flex-col h-full`}
> >
<div className="h-full flex flex-1 flex-col border-r border-gray-200"> <div className="flex flex-1 flex-col border-r border-gray-200">
<div className="h-full flex flex-1 flex-col pt-5"> <div className="h-full flex flex-1 flex-col pt-5">
<div className="px-2"> <div className="px-2">
<div className={`relative ${sidebarCollapse ? "flex" : "grid grid-cols-5 gap-1"}`}> <div className={`relative ${sidebarCollapse ? "flex" : "grid grid-cols-5 gap-1"}`}>
@ -210,7 +211,9 @@ const Sidebar: React.FC = () => {
<div className="w-full"> <div className="w-full">
<Menu.Button <Menu.Button
className={`inline-flex justify-between items-center w-full rounded-md px-2 py-2 text-sm font-semibold text-gray-700 focus:outline-none ${ className={`inline-flex justify-between items-center w-full rounded-md px-2 py-2 text-sm font-semibold text-gray-700 focus:outline-none ${
!sidebarCollapse ? "hover:bg-gray-50 border border-gray-300 shadow-sm" : "" !sidebarCollapse
? "hover:bg-gray-50 focus:bg-gray-50 border border-gray-300 shadow-sm"
: ""
}`} }`}
> >
<span className="flex gap-x-1 items-center"> <span className="flex gap-x-1 items-center">
@ -302,7 +305,7 @@ const Sidebar: React.FC = () => {
{!sidebarCollapse && ( {!sidebarCollapse && (
<Menu as="div" className="inline-block text-left w-full"> <Menu as="div" className="inline-block text-left w-full">
<div className="h-full w-full"> <div className="h-full w-full">
<Menu.Button className="grid place-items-center h-full w-full rounded-md shadow-sm px-2 py-2 bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 focus:outline-none"> <Menu.Button className="grid place-items-center h-full w-full rounded-md shadow-sm px-2 py-2 bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 focus:bg-gray-50 focus:outline-none">
<UserIcon className="h-5 w-5" /> <UserIcon className="h-5 w-5" />
</Menu.Button> </Menu.Button>
</div> </div>
@ -361,12 +364,14 @@ const Sidebar: React.FC = () => {
)} )}
</div> </div>
<div className="mt-3 flex-1 space-y-1 bg-white"> <div className="mt-3 flex-1 space-y-1 bg-white">
{navLinks.map((link, index) => ( {workspaceLinks.map((link, index) => (
<Link key={index} href={link.href}> <Link key={index} href={link.href}>
<a <a
className={`${ className={`${
link.href === router.asPath ? "bg-theme text-white" : "hover:bg-indigo-100" link.href === router.asPath
} group flex items-center gap-3 px-2 py-2 text-xs font-medium rounded-md ${ ? "bg-theme text-white"
: "hover:bg-indigo-100 focus:bg-indigo-100"
} flex items-center gap-3 p-2 text-xs font-medium rounded-md outline-none ${
sidebarCollapse ? "justify-center" : "" sidebarCollapse ? "justify-center" : ""
}`} }`}
> >
@ -380,6 +385,17 @@ const Sidebar: React.FC = () => {
</a> </a>
</Link> </Link>
))} ))}
<button
type="button"
className="w-full flex items-center gap-3 p-2 hover:bg-indigo-100 text-xs font-medium rounded-md outline-none"
onClick={() => {
const e = new KeyboardEvent("keydown", { key: "h", ctrlKey: true });
document.dispatchEvent(e);
}}
>
<QuestionMarkCircleIcon className="flex-shrink-0 h-4 w-4" />
{!sidebarCollapse && "Help Centre"}
</button>
</div> </div>
</div> </div>
<div <div
@ -421,8 +437,8 @@ const Sidebar: React.FC = () => {
className={classNames( className={classNames(
item.href === router.asPath item.href === router.asPath
? "bg-gray-200 text-gray-900" ? "bg-gray-200 text-gray-900"
: "text-gray-500 hover:bg-gray-100 hover:text-gray-900", : "text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900",
"group flex items-center px-2 py-2 text-xs font-medium rounded-md", "group flex items-center px-2 py-2 text-xs font-medium rounded-md outline-none",
sidebarCollapse ? "justify-center" : "" sidebarCollapse ? "justify-center" : ""
)} )}
> >
@ -467,8 +483,8 @@ const Sidebar: React.FC = () => {
<div className="px-2 py-2 bg-gray-50 w-full self-baseline"> <div className="px-2 py-2 bg-gray-50 w-full self-baseline">
<button <button
type="button" type="button"
className={`flex items-center gap-3 px-2 py-2 text-xs font-medium rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-900 w-full ${ className={`flex items-center gap-3 px-2 py-2 text-xs font-medium rounded-md text-gray-500 hover:bg-gray-100 hover:text-gray-900 focus:bg-gray-100 focus:text-gray-900 outline-none ${
sidebarCollapse ? "justify-center" : "" sidebarCollapse ? "justify-center w-full" : ""
}`} }`}
onClick={() => toggleCollapsed()} onClick={() => toggleCollapsed()}
> >
@ -477,7 +493,6 @@ const Sidebar: React.FC = () => {
sidebarCollapse ? "rotate-180" : "" sidebarCollapse ? "rotate-180" : ""
}`} }`}
/> />
{!sidebarCollapse && "Collapse"}
</button> </button>
</div> </div>
</div> </div>

View File

@ -1,27 +0,0 @@
import React, { useState } from "react";
// components
import CreateProjectModal from "components/project/CreateProjectModal";
// layouts
import AdminLayout from "layouts/AdminLayout";
// types
import type { Props } from "./types";
// components
import Sidebar from "./Navbar/Sidebar";
const ProjectLayouts: React.FC<Props> = ({ children, meta }) => {
const [isOpen, setIsOpen] = useState(false);
return (
<AdminLayout meta={meta}>
<CreateProjectModal isOpen={isOpen} setIsOpen={setIsOpen} />
<div className="h-full w-full overflow-x-hidden relative flex">
<Sidebar />
<main className="h-full w-full mx-auto min-w-0 pb-6 overflow-y-hidden">
<div className="h-full w-full px-8 py-6 overflow-auto">{children}</div>
</main>
</div>
</AdminLayout>
);
};
export default ProjectLayouts;

View File

@ -1,33 +1,14 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
// next // next
import type { NextPage } from "next"; import type { NextPage } from "next";
import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
// swr
import useSWR from "swr";
// hooks // hooks
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
// fetch keys
import { USER_ISSUE } from "constants/fetch-keys";
// services
import userService from "lib/services/user.service";
// ui
import { Spinner } from "ui";
// icons
import { ArrowRightIcon } from "@heroicons/react/24/outline";
// types
import type { IIssue } from "types";
import ProjectLayout from "layouts/ProjectLayout";
const Home: NextPage = () => { const Home: NextPage = () => {
const router = useRouter(); const router = useRouter();
const { user, isUserLoading, activeWorkspace, projects, workspaces } = useUser(); const { user, isUserLoading, activeWorkspace, workspaces } = useUser();
const { data: myIssues } = useSWR<IIssue[]>(
user ? USER_ISSUE : null,
user ? () => userService.userIssues() : null
);
if (!isUserLoading && !user) router.push("/signin"); if (!isUserLoading && !user) router.push("/signin");

View File

@ -1,59 +1,192 @@
// react
import React, { useState } from "react";
// next // next
import type { NextPage } from "next"; import type { NextPage } from "next";
import Link from "next/link";
// react
import React, { useState } from "react";
// swr // swr
import useSWR from "swr"; import useSWR from "swr";
// layouts // layouts
import ProjectLayout from "layouts/ProjectLayout"; import AdminLayout from "layouts/AdminLayout";
// hooks // hooks
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
import useIssuesProperties from "lib/hooks/useIssuesProperties";
// components // components
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
import ChangeStateDropdown from "components/project/issues/my-issues/ChangeStateDropdown";
// ui // ui
import { Spinner } from "ui"; import { Spinner } from "ui";
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs";
import { EmptySpace, EmptySpaceItem } from "ui/EmptySpace"; import { EmptySpace, EmptySpaceItem } from "ui/EmptySpace";
import HeaderButton from "ui/HeaderButton"; import HeaderButton from "ui/HeaderButton";
import { Menu, Popover, Transition } from "@headlessui/react";
// icons // icons
import { PlusIcon, RectangleStackIcon } from "@heroicons/react/24/outline"; import { ChevronDownIcon, PlusIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
// services // services
import userService from "lib/services/user.service"; import userService from "lib/services/user.service";
// types // types
import { IIssue } from "types"; import { IIssue, NestedKeyOf, Properties, WorkspaceMember } from "types";
// constants // constants
import ChangeStateDropdown from "components/project/issues/my-issues/ChangeStateDropdown"; import { USER_ISSUE, WORKSPACE_MEMBERS } from "constants/fetch-keys";
import { USER_ISSUE } from "constants/fetch-keys"; import {
import { classNames } from "constants/common"; classNames,
groupBy,
renderShortNumericDateFormat,
replaceUnderscoreIfSnakeCase,
} from "constants/common";
import { EyeIcon, EyeSlashIcon } from "@heroicons/react/20/solid";
import workspaceService from "lib/services/workspace.service";
import useTheme from "lib/hooks/useTheme";
const MyIssues: NextPage = () => { const MyIssues: NextPage = () => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { user } = useUser(); const { user, activeWorkspace, activeProject } = useUser();
const { issueView, setIssueView, groupByProperty, setGroupByProperty } = useTheme();
const { data: myIssues } = useSWR<IIssue[]>( const { data: myIssues } = useSWR<IIssue[]>(
user ? USER_ISSUE : null, user ? USER_ISSUE : null,
user ? () => userService.userIssues() : null user ? () => userService.userIssues() : null
); );
const [properties, setProperties] = useIssuesProperties(
activeWorkspace?.slug,
activeProject?.id as string
);
const { data: people } = useSWR<WorkspaceMember[]>(
activeWorkspace ? WORKSPACE_MEMBERS : null,
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
);
const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> }> = [
{ name: "State", key: "state_detail.name" },
{ name: "Priority", key: "priority" },
{ name: "Created By", key: "created_by" },
];
const groupedByIssues: {
[key: string]: IIssue[];
} = groupBy(myIssues ?? [], groupByProperty ?? "");
return ( return (
<ProjectLayout> <AdminLayout>
<CreateUpdateIssuesModal isOpen={isOpen} setIsOpen={setIsOpen} /> <CreateUpdateIssuesModal isOpen={isOpen} setIsOpen={setIsOpen} />
<div className="w-full h-full flex flex-col space-y-5">
{myIssues ? ( {myIssues ? (
<> <>
{myIssues.length > 0 ? ( {myIssues.length > 0 ? (
<> <div className="space-y-5">
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title="My Issues" /> <BreadcrumbItem title="My Issues" />
</Breadcrumbs> </Breadcrumbs>
<div className="flex items-center justify-between cursor-pointer w-full"> <div className="w-full flex items-center justify-between">
<h2 className="text-2xl font-medium">My Issues</h2> <h2 className="text-2xl font-medium">My Issues</h2>
<div className="flex items-center gap-x-3"> <div className="flex items-center gap-x-3">
<Menu as="div" className="relative inline-block w-40">
<div className="w-full">
<Menu.Button className="inline-flex justify-between items-center w-full rounded-md shadow-sm p-2 bg-white border border-gray-300 text-xs font-semibold text-gray-700 hover:bg-gray-50 focus:outline-none">
<span className="flex gap-x-1 items-center">
{groupByOptions.find((option) => option.key === groupByProperty)?.name ??
"No Grouping"}
</span>
<div className="flex-grow flex justify-end">
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
</div>
</Menu.Button>
</div>
<Transition
as={React.Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="origin-top-left absolute left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
<div className="p-1">
{groupByOptions.map((option) => (
<Menu.Item key={option.key}>
{({ active }) => (
<button
type="button"
className={`${
active ? "bg-theme text-white" : "text-gray-900"
} group flex w-full items-center rounded-md p-2 text-xs`}
onClick={() => setGroupByProperty(option.key)}
>
{option.name}
</button>
)}
</Menu.Item>
))}
{issueView === "list" ? (
<Menu.Item>
{({ active }) => (
<button
type="button"
className={`hover:bg-theme hover:text-white ${
active ? "bg-theme text-white" : "text-gray-900"
} group flex w-full items-center rounded-md p-2 text-xs`}
onClick={() => setGroupByProperty(null)}
>
No grouping
</button>
)}
</Menu.Item>
) : null}
</div>
</Menu.Items>
</Transition>
</Menu>
<Popover className="relative">
{({ open }) => (
<>
<Popover.Button className="inline-flex justify-between items-center rounded-md shadow-sm p-2 bg-white border border-gray-300 text-xs font-semibold text-gray-700 hover:bg-gray-50 focus:outline-none w-40">
<span>Properties</span>
<ChevronDownIcon className="h-4 w-4" />
</Popover.Button>
<Transition
as={React.Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<Popover.Panel className="absolute left-1/2 z-10 mt-1 -translate-x-1/2 transform px-2 sm:px-0 w-full">
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
<div className="relative grid bg-white p-1">
{Object.keys(properties).map((key) => (
<button
key={key}
className={`text-gray-900 hover:bg-theme hover:text-white flex justify-between w-full items-center rounded-md p-2 text-xs`}
onClick={() => setProperties(key as keyof Properties)}
>
<p className="capitalize">{key.replace("_", " ")}</p>
<span className="self-end">
{properties[key as keyof Properties] ? (
<EyeIcon width="18" height="18" />
) : (
<EyeSlashIcon width="18" height="18" />
)}
</span>
</button>
))}
</div>
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<HeaderButton <HeaderButton
Icon={PlusIcon} Icon={PlusIcon}
label="Add Issue" label="Add Issue"
action={() => { onClick={() => {
const e = new KeyboardEvent("keydown", { const e = new KeyboardEvent("keydown", {
key: "i", key: "i",
ctrlKey: true, ctrlKey: true,
@ -63,43 +196,24 @@ const MyIssues: NextPage = () => {
/> />
</div> </div>
</div> </div>
<div className="mt-4 flex flex-col">
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<div className="inline-block min-w-full align-middle px-0.5 py-2"> <div className="inline-block min-w-full align-middle p-0.5">
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg"> <div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
<table className="min-w-full"> <table className="min-w-full">
<thead className="bg-gray-100"> <thead className="bg-gray-100">
<tr className="text-left"> <tr>
{Object.keys(properties).map(
(key) =>
properties[key as keyof Properties] && (
<th <th
key={key}
scope="col" scope="col"
className="px-3 py-3.5 text-sm font-semibold text-gray-900" className="px-3 py-3.5 text-left uppercase text-sm font-semibold text-gray-900"
> >
NAME {replaceUnderscoreIfSnakeCase(key)}
</th>
<th
scope="col"
className="px-3 py-3.5 text-sm font-semibold text-gray-900"
>
DESCRIPTION
</th>
<th
scope="col"
className="px-3 py-3.5 text-sm font-semibold text-gray-900"
>
PROJECT
</th>
<th
scope="col"
className="px-3 py-3.5 text-sm font-semibold text-gray-900"
>
PRIORITY
</th>
<th
scope="col"
className="px-3 py-3.5 text-sm font-semibold text-gray-900"
>
STATUS
</th> </th>
)
)}
</tr> </tr>
</thead> </thead>
<tbody className="bg-white"> <tbody className="bg-white">
@ -111,19 +225,62 @@ const MyIssues: NextPage = () => {
"border-t text-sm text-gray-900" "border-t text-sm text-gray-900"
)} )}
> >
<td className="px-3 py-4 text-sm font-medium text-gray-900 max-w-[15rem]"> {Object.keys(properties).map(
(key) =>
properties[key as keyof Properties] && (
<td
key={key}
className="px-3 py-4 text-sm font-medium text-gray-900 relative"
>
{(key as keyof Properties) === "name" ? (
<p className="w-[15rem]">
<Link
href={`/projects/${myIssue.project}/issues/${myIssue.id}`}
>
<a className="hover:text-theme duration-300">
{myIssue.name} {myIssue.name}
</td> </a>
<td className="px-3 py-4 max-w-[15rem]">{myIssue.description}</td> </Link>
<td className="px-3 py-4"> </p>
{myIssue.project_detail.name} ) : (key as keyof Properties) === "key" ? (
<br /> <p className="text-xs whitespace-nowrap">
<span className="text-xs">{`(${myIssue.project_detail.identifier}-${myIssue.sequence_id})`}</span> {activeProject?.identifier}-{myIssue.sequence_id}
</td> </p>
<td className="px-3 py-4 capitalize">{myIssue.priority}</td> ) : (key as keyof Properties) === "description" ? (
<td className="relative px-3 py-4"> <p className="truncate text-xs max-w-[15rem]">
{myIssue.description}
</p>
) : (key as keyof Properties) === "state" ? (
<ChangeStateDropdown issue={myIssue} /> <ChangeStateDropdown issue={myIssue} />
) : (key as keyof Properties) === "assignee" ? (
<div className="max-w-xs text-xs">
{myIssue.assignees && myIssue.assignees.length > 0
? myIssue.assignees.map((assignee, index) => (
<p key={index}>
{
people?.find((p) => p.member.id === assignee)
?.member.email
}
</p>
))
: "None"}
</div>
) : (key as keyof Properties) === "target_date" ? (
<p className="whitespace-nowrap">
{myIssue.target_date
? renderShortNumericDateFormat(myIssue.target_date)
: "-"}
</p>
) : (
<p className="capitalize text-sm">
{myIssue[key as keyof IIssue] ??
(myIssue[key as keyof IIssue] as any)?.name ??
"None"}
</p>
)}
</td> </td>
)
)}
</tr> </tr>
))} ))}
</tbody> </tbody>
@ -132,9 +289,8 @@ const MyIssues: NextPage = () => {
</div> </div>
</div> </div>
</div> </div>
</>
) : ( ) : (
<div className="w-full h-full flex flex-col justify-center items-center px-4"> <div className="h-full w-full grid place-items-center px-4 sm:px-0">
<EmptySpace <EmptySpace
title="You don't have any issue assigned to you yet." title="You don't have any issue assigned to you yet."
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done." description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
@ -157,12 +313,11 @@ const MyIssues: NextPage = () => {
)} )}
</> </>
) : ( ) : (
<div className="w-full h-full flex justify-center items-center"> <div className="h-full w-full grid place-items-center px-4 sm:px-0">
<Spinner /> <Spinner />
</div> </div>
)} )}
</div> </AdminLayout>
</ProjectLayout>
); );
}; };

View File

@ -5,19 +5,32 @@ import type { NextPage } from "next";
// react hook form // react hook form
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
// react dropzone // react dropzone
import Dropzone from "react-dropzone"; import Dropzone, { useDropzone } from "react-dropzone";
// hooks // hooks
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
// layouts // layouts
import ProjectLayout from "layouts/ProjectLayout"; import AdminLayout from "layouts/AdminLayout";
// services // services
import userService from "lib/services/user.service"; import userService from "lib/services/user.service";
import fileServices from "lib/services/file.services"; import fileServices from "lib/services/file.services";
// ui // ui
import { Button, Input, Spinner } from "ui"; import { BreadcrumbItem, Breadcrumbs, Button, Input, Spinner } from "ui";
// types // types
import type { IUser } from "types"; import type { IIssue, IUser, IWorkspaceInvitation } from "types";
import { UserIcon } from "@heroicons/react/24/outline"; import {
ChevronRightIcon,
ClipboardDocumentListIcon,
PencilIcon,
RectangleStackIcon,
UserIcon,
UserPlusIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
import useSWR from "swr";
import { USER_ISSUE, USER_WORKSPACE_INVITATIONS } from "constants/fetch-keys";
import useToast from "lib/hooks/useToast";
import Link from "next/link";
import workspaceService from "lib/services/workspace.service";
const defaultValues: Partial<IUser> = { const defaultValues: Partial<IUser> = {
avatar: "", avatar: "",
@ -29,15 +42,23 @@ const defaultValues: Partial<IUser> = {
const Profile: NextPage = () => { const Profile: NextPage = () => {
const [image, setImage] = useState<File | null>(null); const [image, setImage] = useState<File | null>(null);
const [isImageUploading, setIsImageUploading] = useState(false); const [isImageUploading, setIsImageUploading] = useState(false);
const [isEditing, setIsEditing] = useState(false);
const { user: myProfile, mutateUser } = useUser(); const { user: myProfile, mutateUser, projects } = useUser();
const { setToastAlert } = useToast();
const onSubmit = (formData: IUser) => { const onSubmit = (formData: IUser) => {
userService userService
.updateUser(formData) .updateUser(formData)
.then((response) => { .then((response) => {
console.log(response);
mutateUser(response, false); mutateUser(response, false);
setIsEditing(false);
setToastAlert({
title: "Success",
type: "success",
message: "Profile updated successfully",
});
}) })
.catch((error) => { .catch((error) => {
console.log(error); console.log(error);
@ -53,20 +74,68 @@ const Profile: NextPage = () => {
formState: { errors, isSubmitting }, formState: { errors, isSubmitting },
} = useForm<IUser>({ defaultValues }); } = useForm<IUser>({ defaultValues });
const { data: myIssues } = useSWR<IIssue[]>(
myProfile ? USER_ISSUE : null,
myProfile ? () => userService.userIssues() : null
);
const { data: invitations } = useSWR<IWorkspaceInvitation[]>(USER_WORKSPACE_INVITATIONS, () =>
workspaceService.userWorkspaceInvitations()
);
useEffect(() => { useEffect(() => {
reset({ ...defaultValues, ...myProfile }); reset({ ...defaultValues, ...myProfile });
}, [myProfile, reset]); }, [myProfile, reset]);
const quickLinks = [
{
icon: RectangleStackIcon,
title: "My Issues",
number: myIssues?.length ?? 0,
description: "View the list of issues assigned to you across the workspace.",
href: "/me/my-issues",
},
{
icon: ClipboardDocumentListIcon,
title: "My Projects",
number: projects?.length ?? 0,
description: "View the list of projects of the workspace.",
href: "/projects",
},
{
icon: UserPlusIcon,
title: "Workspace Invitations",
number: invitations?.length ?? 0,
description: "View your workspace invitations.",
href: "/invitations",
},
];
return ( return (
<ProjectLayout <AdminLayout
meta={{ meta={{
title: "Plane - My Profile", title: "Plane - My Profile",
}} }}
> >
<div className="w-full h-full md:px-20 p-8 flex flex-wrap overflow-auto gap-y-10 justify-center items-center"> <div className="w-full space-y-5">
<Breadcrumbs>
<BreadcrumbItem title="My Profile" />
</Breadcrumbs>
{myProfile ? ( {myProfile ? (
<> <>
<div className="w-2/5"> <div className="space-y-5">
<section className="relative p-5 rounded-xl flex gap-10 bg-secondary">
<div
className="absolute top-4 right-4 bg-indigo-100 hover:bg-theme hover:text-white rounded p-1 cursor-pointer duration-300"
onClick={() => setIsEditing((prevData) => !prevData)}
>
{isEditing ? (
<XMarkIcon className="h-4 w-4" />
) : (
<PencilIcon className="h-4 w-4" />
)}
</div>
<div className="flex-shrink-0">
<Dropzone <Dropzone
multiple={false} multiple={false}
accept={{ accept={{
@ -76,20 +145,19 @@ const Profile: NextPage = () => {
setImage(files[0]); setImage(files[0]);
}} }}
> >
{({ getRootProps, getInputProps }) => ( {({ getRootProps, getInputProps, open }) => (
<div className="space-y-4"> <div className="space-y-4">
<input {...getInputProps()} /> <input {...getInputProps()} />
<h2 className="font-semibold text-xl">Profile Picture</h2>
<div className="relative"> <div className="relative">
<span <span
className="inline-block h-24 w-24 rounded-full overflow-hidden bg-gray-100" className="inline-block h-40 w-40 rounded overflow-hidden bg-gray-100"
{...getRootProps()} {...getRootProps()}
> >
{(!watch("avatar") || watch("avatar") === "") && {(!watch("avatar") || watch("avatar") === "") &&
(!image || image === null) ? ( (!image || image === null) ? (
<UserIcon className="h-full w-full text-gray-300" /> <UserIcon className="h-full w-full text-gray-300" />
) : ( ) : (
<div className="relative h-24 w-24 overflow-hidden"> <div className="relative h-40 w-40 overflow-hidden">
<Image <Image
src={image ? URL.createObjectURL(image) : watch("avatar")} src={image ? URL.createObjectURL(image) : watch("avatar")}
alt={myProfile.first_name} alt={myProfile.first_name}
@ -102,16 +170,16 @@ const Profile: NextPage = () => {
</span> </span>
</div> </div>
<p className="text-gray-500 text-sm"> <p className="text-gray-500 text-sm">
Max file size is 500kb. Supported file types are .jpg and .png. Max file size is 500kb.
<br />
Supported file types are .jpg and .png.
</p> </p>
</div>
)}
</Dropzone>
<Button <Button
type="button" type="button"
className="mt-4" className="mt-4"
onClick={() => { onClick={() => {
if (image === null) return; if (image === null) open();
else {
setIsImageUploading(true); setIsImageUploading(true);
const formData = new FormData(); const formData = new FormData();
formData.append("asset", image); formData.append("asset", image);
@ -127,43 +195,53 @@ const Profile: NextPage = () => {
.catch((err) => { .catch((err) => {
setIsImageUploading(false); setIsImageUploading(false);
}); });
}
}} }}
> >
{isImageUploading ? "Uploading..." : "Upload"} {isImageUploading ? "Uploading..." : "Upload"}
</Button> </Button>
</div> </div>
<div className="mt-5 w-3/5"> )}
<form onSubmit={handleSubmit(onSubmit)}> </Dropzone>
<div className="space-y-4"> </div>
<h2 className="font-semibold text-xl">Details</h2> <form className="space-y-5" onSubmit={handleSubmit(onSubmit)}>
<div className="flex gap-x-4"> <div className="grid grid-cols-2 gap-x-10 gap-y-5 mt-2">
<div className="flex-grow"> <div>
<h4 className="text-sm text-gray-500">First Name</h4>
{isEditing ? (
<Input <Input
name="first_name" name="first_name"
id="first_name" id="first_name"
register={register} register={register}
error={errors.first_name} error={errors.first_name}
label="First Name"
placeholder="Enter your first name" placeholder="Enter your first name"
autoComplete="off" autoComplete="off"
validations={{ validations={{
required: "This field is required.", required: "This field is required.",
}} }}
/> />
) : (
<h2>{myProfile.first_name}</h2>
)}
</div> </div>
<div className="flex-grow"> <div>
<h4 className="text-sm text-gray-500">Last Name</h4>
{isEditing ? (
<Input <Input
name="last_name" name="last_name"
register={register} register={register}
error={errors.last_name} error={errors.last_name}
id="last_name" id="last_name"
label="Last Name"
placeholder="Enter your last name" placeholder="Enter your last name"
autoComplete="off" autoComplete="off"
/> />
</div> ) : (
<h2>{myProfile.last_name}</h2>
)}
</div> </div>
<div> <div>
<h4 className="text-sm text-gray-500">Email ID</h4>
{isEditing ? (
<Input <Input
id="email" id="email"
type="email" type="email"
@ -173,44 +251,50 @@ const Profile: NextPage = () => {
validations={{ validations={{
required: "Email is required", required: "Email is required",
}} }}
label="Email"
placeholder="Enter email" placeholder="Enter email"
/> />
) : (
<h2>{myProfile.email}</h2>
)}
</div> </div>
</div>
{isEditing && (
<div> <div>
<Button disabled={isSubmitting} type="submit"> <Button type="submit" disabled={isSubmitting}>
{isSubmitting ? "Updating Profile..." : "Update Profile"} {isSubmitting ? "Updating Profile..." : "Update Profile"}
</Button> </Button>
</div> </div>
{/* <div>
<Button type="submit" onClick={handleSubmit(onSubmit)} disabled={isSubmitting}>
{isSubmitting ? "Submitting..." : "Update"}
</Button>
{myProfile.is_email_verified || (
<button
type="button"
className="ml-2 text-indigo-600"
onClick={() => {
requestEmailVerification()
.then(() => {
setToastAlert({
type: "success",
title: "Verification email sent.",
message: "Please check your email.",
});
})
.catch((err) => {
console.error(err);
});
}}
>
Verify Your Email
</button>
)} )}
</div> */}
</div>
</form> </form>
</section>
<section>
<h2 className="text-xl font-medium mb-3">Quick Links</h2>
<div className="grid grid-cols-3 gap-5">
{quickLinks.map((item, index) => (
<Link key={index} href={item.href}>
<a className="group p-5 rounded-lg bg-secondary hover:bg-theme duration-300">
<h4 className="group-hover:text-white flex items-center gap-2 duration-300">
{item.title}
<ChevronRightIcon className="h-3 w-3" />
</h4>
<div className="flex justify-between items-center gap-3">
<div>
<h2 className="mt-3 mb-2 text-3xl font-bold group-hover:text-white duration-300">
{item.number}
</h2>
<p className="text-gray-500 group-hover:text-white text-sm duration-300">
{item.description}
</p>
</div>
<div>
<item.icon className="h-12 w-12 group-hover:text-white duration-300" />
</div>
</div>
</a>
</Link>
))}
</div>
</section>
</div> </div>
</> </>
) : ( ) : (
@ -219,7 +303,7 @@ const Profile: NextPage = () => {
</div> </div>
)} )}
</div> </div>
</ProjectLayout> </AdminLayout>
); );
}; };

View File

@ -12,7 +12,7 @@ import useUser from "lib/hooks/useUser";
// fetching keys // fetching keys
import { CYCLE_ISSUES, CYCLE_LIST } from "constants/fetch-keys"; import { CYCLE_ISSUES, CYCLE_LIST } from "constants/fetch-keys";
// layouts // layouts
import ProjectLayout from "layouts/ProjectLayout"; import AdminLayout from "layouts/AdminLayout";
// components // components
import SprintView from "components/project/cycles/CycleView"; import SprintView from "components/project/cycles/CycleView";
import ConfirmIssueDeletion from "components/project/issues/ConfirmIssueDeletion"; import ConfirmIssueDeletion from "components/project/issues/ConfirmIssueDeletion";
@ -98,7 +98,7 @@ const ProjectSprints: NextPage = () => {
}, [selectedIssues]); }, [selectedIssues]);
return ( return (
<ProjectLayout <AdminLayout
meta={{ meta={{
title: "Plane - Cycles", title: "Plane - Cycles",
}} }}
@ -134,20 +134,18 @@ const ProjectSprints: NextPage = () => {
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
projectId={projectId as string} projectId={projectId as string}
/> />
<div className="w-full h-full flex flex-col space-y-5">
{sprints ? ( {sprints ? (
sprints.length > 0 ? ( sprints.length > 0 ? (
<div className="flex flex-col items-center justify-center w-full h-full px-2"> <div className="h-full w-full space-y-5">
<div className="w-full h-full flex flex-col space-y-5">
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title="Projects" link="/projects" /> <BreadcrumbItem title="Projects" link="/projects" />
<BreadcrumbItem title={`${activeProject?.name} Cycles`} /> <BreadcrumbItem title={`${activeProject?.name ?? "Project"} Cycles`} />
</Breadcrumbs> </Breadcrumbs>
<div className="flex items-center justify-between cursor-pointer w-full"> <div className="flex items-center justify-between cursor-pointer w-full">
<h2 className="text-2xl font-medium">Project Cycle</h2> <h2 className="text-2xl font-medium">Project Cycle</h2>
<HeaderButton Icon={PlusIcon} label="Add Cycle" action={() => setIsOpen(true)} /> <HeaderButton Icon={PlusIcon} label="Add Cycle" onClick={() => setIsOpen(true)} />
</div> </div>
<div className="w-full h-full pr-2 overflow-auto"> <div className="h-full w-full">
{sprints.map((sprint) => ( {sprints.map((sprint) => (
<SprintView <SprintView
sprint={sprint} sprint={sprint}
@ -161,9 +159,7 @@ const ProjectSprints: NextPage = () => {
))} ))}
</div> </div>
</div> </div>
</div>
) : ( ) : (
<>
<div className="w-full h-full flex flex-col justify-center items-center px-4"> <div className="w-full h-full flex flex-col justify-center items-center px-4">
<EmptySpace <EmptySpace
title="You don't have any cycle yet." title="You don't have any cycle yet."
@ -174,8 +170,7 @@ const ProjectSprints: NextPage = () => {
title="Create a new cycle" title="Create a new cycle"
description={ description={
<span> <span>
Use{" "} Use <pre className="inline bg-gray-100 px-2 py-1 rounded">Ctrl/Command + Q</pre>{" "}
<pre className="inline bg-gray-100 px-2 py-1 rounded">Ctrl/Command + Q</pre>{" "}
shortcut to create a new cycle shortcut to create a new cycle
</span> </span>
} }
@ -184,15 +179,13 @@ const ProjectSprints: NextPage = () => {
/> />
</EmptySpace> </EmptySpace>
</div> </div>
</>
) )
) : ( ) : (
<div className="w-full h-full flex justify-center items-center"> <div className="w-full h-full flex justify-center items-center">
<Spinner /> <Spinner />
</div> </div>
)} )}
</div> </AdminLayout>
</ProjectLayout>
); );
}; };

View File

@ -1,8 +1,6 @@
// next // next
import Link from "next/link";
import type { NextPage } from "next"; import type { NextPage } from "next";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import Image from "next/image";
// react // react
import React, { useCallback, useEffect, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
// swr // swr
@ -13,25 +11,30 @@ import { useForm } from "react-hook-form";
import { Tab } from "@headlessui/react"; import { Tab } from "@headlessui/react";
// services // services
import issuesServices from "lib/services/issues.services"; import issuesServices from "lib/services/issues.services";
import stateServices from "lib/services/state.services";
// fetch keys // fetch keys
import { PROJECT_ISSUES_ACTIVITY, PROJECT_ISSUES_COMMENTS, STATE_LIST } from "constants/fetch-keys"; import { PROJECT_ISSUES_ACTIVITY, PROJECT_ISSUES_COMMENTS, STATE_LIST } from "constants/fetch-keys";
// hooks // hooks
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
// layouts // layouts
import ProjectLayout from "layouts/ProjectLayout"; import AdminLayout from "layouts/AdminLayout";
// components // components
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal"; import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
import IssueCommentSection from "components/project/issues/comment/IssueCommentSection"; import IssueCommentSection from "components/project/issues/issue-detail/comment/IssueCommentSection";
// common // common
import { timeAgo, debounce, addSpaceIfCamelCase } from "constants/common"; import { debounce } from "constants/common";
// components // components
import IssueDetailSidebar from "components/project/issues/issue-detail/IssueDetailSidebar"; import IssueDetailSidebar from "components/project/issues/issue-detail/IssueDetailSidebar";
// activites
import IssueActivitySection from "components/project/issues/issue-detail/activity";
// ui // ui
import { Spinner, TextArea } from "ui"; import { Spinner, TextArea } from "ui";
import HeaderButton from "ui/HeaderButton";
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs";
// types // types
import { IIssue, IIssueComment, IssueResponse, IState } from "types"; import { IIssue, IIssueComment, IssueResponse, IState } from "types";
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs"; // icons
import stateServices from "lib/services/state.services"; import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/24/outline";
const IssueDetail: NextPage = () => { const IssueDetail: NextPage = () => {
const router = useRouter(); const router = useRouter();
@ -137,7 +140,7 @@ const IssueDetail: NextPage = () => {
const nextIssue = issues?.results[issues?.results.findIndex((issue) => issue.id === issueId) + 1]; const nextIssue = issues?.results[issues?.results.findIndex((issue) => issue.id === issueId) + 1];
return ( return (
<ProjectLayout> <AdminLayout>
<CreateUpdateIssuesModal <CreateUpdateIssuesModal
isOpen={isOpen} isOpen={isOpen}
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
@ -149,54 +152,43 @@ const IssueDetail: NextPage = () => {
<div className="space-y-5"> <div className="space-y-5">
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem <BreadcrumbItem
title={`${activeProject?.name} Issues`} title={`${activeProject?.name ?? "Project"} Issues`}
link={`/projects/${activeProject?.id}/issues`} link={`/projects/${activeProject?.id}/issues`}
/> />
<BreadcrumbItem <BreadcrumbItem
title={`Issue ${activeProject?.identifier}-${issueDetail?.sequence_id} Details`} title={`Issue ${activeProject?.identifier ?? "Project"}-${
issueDetail?.sequence_id ?? "..."
} Details`}
/> />
</Breadcrumbs> </Breadcrumbs>
<div className="bg-gray-50 rounded-xl overflow-hidden"> <div className="flex items-center justify-between w-full">
{issueDetail && activeProject ? ( <h2 className="text-2xl font-medium">{`${activeProject?.name}/${activeProject?.identifier}-${issueDetail?.sequence_id}`}</h2>
<> <div className="flex items-center gap-x-3">
<div className="w-full py-4 px-10 bg-gray-200 flex justify-between items-center"> <HeaderButton
<p className="text-gray-500"> Icon={ChevronLeftIcon}
<Link href={`/projects/${activeProject.id}/issues`}>{activeProject.name}</Link>/ disabled={!prevIssue}
{activeProject.identifier}-{issueDetail.sequence_id} label="Previous"
</p>
<div className="flex gap-x-2">
<button
type="button"
className={`px-4 py-1.5 bg-white rounded-lg ${
prevIssue ? "hover:bg-gray-100" : "bg-gray-100"
}`}
disabled={prevIssue ? false : true}
onClick={() => { onClick={() => {
if (!prevIssue) return; if (!prevIssue) return;
router.push(`/projects/${prevIssue.project}/issues/${prevIssue.id}`); router.push(`/projects/${prevIssue.project}/issues/${prevIssue.id}`);
}} }}
> />
Previous <HeaderButton
</button> Icon={ChevronRightIcon}
<button disabled={!nextIssue}
type="button" label="Next"
className={`px-4 py-1.5 bg-white rounded-lg ${
nextIssue ? "hover:bg-gray-100" : "bg-gray-100"
}`}
disabled={nextIssue ? false : true}
onClick={() => { onClick={() => {
if (!nextIssue) return; if (!nextIssue) return;
router.push(`/projects/${nextIssue.project}/issues/${nextIssue?.id}`); router.push(`/projects/${nextIssue.project}/issues/${nextIssue?.id}`);
}} }}
> position="reverse"
Next />
</button>
</div> </div>
</div> </div>
<div> {issueDetail && activeProject ? (
<div className="flex flex-wrap"> <div className="grid grid-cols-4 gap-5">
<div className="w-full lg:w-3/4 h-full px-2 md:px-10 py-10 overflow-auto"> <div className="col-span-3 space-y-5">
<div className="w-full h-full space-y-5"> <div className="bg-secondary rounded-lg p-5">
<TextArea <TextArea
id="name" id="name"
placeholder="Enter issue name" placeholder="Enter issue name"
@ -224,25 +216,25 @@ const IssueDetail: NextPage = () => {
mode="transparent" mode="transparent"
register={register} register={register}
/> />
</div>
<div className="bg-secondary rounded-lg p-5">
<div className="relative"> <div className="relative">
<div className="absolute inset-0 flex items-center" aria-hidden="true"> <div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-300" /> <div className="w-full border-t border-gray-300" />
</div> </div>
<div className="relative flex justify-center"> <div className="relative flex justify-center">
<span className="bg-gray-50 px-2 text-sm text-gray-500"> <span className="bg-white px-2 text-sm text-gray-500">Activity/Comments</span>
Activity/Comments
</span>
</div> </div>
</div> </div>
<div className="w-full"> <div className="w-full space-y-5 mt-3">
<Tab.Group> <Tab.Group>
<Tab.List className="flex gap-x-3"> <Tab.List className="flex gap-x-3">
{["Comments", "Activity"].map((item) => ( {["Comments", "Activity"].map((item) => (
<Tab <Tab
key={item} key={item}
className={({ selected }) => className={({ selected }) =>
`px-3 py-1 text-sm rounded-md ${ `px-3 py-1 text-sm rounded-md border border-gray-700 ${
selected ? "bg-gray-800 text-white" : "" selected ? "bg-gray-700 text-white" : ""
}` }`
} }
> >
@ -250,7 +242,7 @@ const IssueDetail: NextPage = () => {
</Tab> </Tab>
))} ))}
</Tab.List> </Tab.List>
<Tab.Panels className="mt-5"> <Tab.Panels>
<Tab.Panel> <Tab.Panel>
<IssueCommentSection <IssueCommentSection
comments={issueComments} comments={issueComments}
@ -260,115 +252,28 @@ const IssueDetail: NextPage = () => {
/> />
</Tab.Panel> </Tab.Panel>
<Tab.Panel> <Tab.Panel>
{issueActivities ? ( <IssueActivitySection issueActivities={issueActivities} states={states} />
<div className="space-y-3">
{issueActivities.map((activity) => {
if (activity.field !== "updated_by")
return (
<div
key={activity.id}
className="relative flex gap-x-2 w-full"
>
{/* <span
className="absolute top-5 left-5 -ml-1 h-full w-0.5 bg-gray-200"
aria-hidden="true"
/> */}
<div className="flex-shrink-0 -ml-1.5">
{activity.actor_detail.avatar &&
activity.actor_detail.avatar !== "" ? (
<Image
src={activity.actor_detail.avatar}
alt={activity.actor_detail.name}
height={30}
width={30}
className="rounded-full"
/>
) : (
<div
className={`h-8 w-8 bg-gray-500 text-white border-2 border-white grid place-items-center rounded-full`}
>
{activity.actor_detail.first_name.charAt(0)}
</div>
)}
</div>
<div className="w-full">
<p>
{activity.actor_detail.first_name}{" "}
{activity.actor_detail.last_name}{" "}
<span>{activity.verb}</span>{" "}
{activity.verb !== "created" ? (
<span>{activity.field ?? "commented"}</span>
) : (
" this issue"
)}
</p>
<p className="text-xs text-gray-500">
{timeAgo(activity.created_at)}
</p>
<div className="w-full mt-2">
{activity.verb !== "created" && (
<div className="text-sm">
<div>
From:{" "}
<span className="text-gray-500">
{activity.field === "state"
? activity.old_value
? addSpaceIfCamelCase(
states?.find(
(s) => s.id === activity.old_value
)?.name ?? ""
)
: "None"
: activity.old_value}
</span>
</div>
<div>
To:{" "}
<span className="text-gray-500">
{activity.field === "state"
? activity.new_value
? addSpaceIfCamelCase(
states?.find(
(s) => s.id === activity.new_value
)?.name ?? ""
)
: "None"
: activity.new_value}
</span>
</div>
</div>
)}
</div>
</div>
</div>
);
})}
</div>
) : (
<div className="w-full h-full flex justify-center items-center">
<Spinner />
</div>
)}
</Tab.Panel> </Tab.Panel>
</Tab.Panels> </Tab.Panels>
</Tab.Group> </Tab.Group>
</div> </div>
</div> </div>
</div> </div>
<div className="w-full lg:w-1/4 h-full border-l px-2 md:px-10 py-10"> <div className="sticky top-0 h-min bg-secondary p-4 rounded-lg">
<IssueDetailSidebar control={control} submitChanges={submitChanges} /> <IssueDetailSidebar
control={control}
issueDetail={issueDetail}
submitChanges={submitChanges}
/>
</div> </div>
</div> </div>
</div>
</>
) : ( ) : (
<div className="w-full h-full flex items-center justify-center"> <div className="h-full w-full grid place-items-center px-4 sm:px-0">
<Spinner /> <Spinner />
</div> </div>
)} )}
</div> </div>
</div> </AdminLayout>
</ProjectLayout>
); );
}; };

View File

@ -19,7 +19,7 @@ import { PROJECT_ISSUES_LIST, STATE_LIST } from "constants/fetch-keys";
// commons // commons
import { groupBy } from "constants/common"; import { groupBy } from "constants/common";
// layouts // layouts
import ProjectLayout from "layouts/ProjectLayout"; import AdminLayout from "layouts/AdminLayout";
// components // components
import ListView from "components/project/issues/ListView"; import ListView from "components/project/issues/ListView";
import BoardView from "components/project/issues/BoardView"; import BoardView from "components/project/issues/BoardView";
@ -29,12 +29,14 @@ import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssue
import { Spinner } from "ui"; import { Spinner } from "ui";
import { EmptySpace, EmptySpaceItem } from "ui/EmptySpace"; import { EmptySpace, EmptySpaceItem } from "ui/EmptySpace";
import HeaderButton from "ui/HeaderButton"; import HeaderButton from "ui/HeaderButton";
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "ui";
// icons // icons
import { ChevronDownIcon, ListBulletIcon, RectangleStackIcon } from "@heroicons/react/24/outline"; import { ChevronDownIcon, ListBulletIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
import { PlusIcon, EyeIcon, EyeSlashIcon, Squares2X2Icon } from "@heroicons/react/20/solid"; import { PlusIcon, EyeIcon, EyeSlashIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
// types // types
import type { IIssue, IssueResponse, Properties, IState, NestedKeyOf } from "types"; import type { IIssue, IssueResponse, Properties, IState, NestedKeyOf, ProjectMember } from "types";
import { PROJECT_MEMBERS } from "constants/api-routes";
import projectService from "lib/services/project.service";
const PRIORITIES = ["high", "medium", "low"]; const PRIORITIES = ["high", "medium", "low"];
@ -76,6 +78,13 @@ const ProjectIssues: NextPage = () => {
: null : null
); );
const { data: members } = useSWR<ProjectMember[]>(
activeWorkspace && activeProject ? PROJECT_MEMBERS : null,
activeWorkspace && activeProject
? () => projectService.projectMembers(activeWorkspace.slug, activeProject.id)
: null
);
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
const timer = setTimeout(() => { const timer = setTimeout(() => {
@ -111,10 +120,11 @@ const ProjectIssues: NextPage = () => {
const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> }> = [ const groupByOptions: 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" },
]; ];
return ( return (
<ProjectLayout> <AdminLayout>
<CreateUpdateIssuesModal <CreateUpdateIssuesModal
isOpen={isOpen && selectedIssue?.actionType !== "delete"} isOpen={isOpen && selectedIssue?.actionType !== "delete"}
setIsOpen={setIsOpen} setIsOpen={setIsOpen}
@ -126,19 +136,18 @@ const ProjectIssues: NextPage = () => {
isOpen={!!deleteIssue} isOpen={!!deleteIssue}
data={projectIssues?.results.find((issue) => issue.id === deleteIssue)} data={projectIssues?.results.find((issue) => issue.id === deleteIssue)}
/> />
<div className="w-full h-full flex flex-col space-y-5 pb-6 mb-10"> <div className="w-full">
{!projectIssues ? ( {!projectIssues ? (
<div className="w-full h-full flex justify-center items-center"> <div className="h-full w-full flex justify-center items-center">
<Spinner /> <Spinner />
</div> </div>
) : projectIssues.count > 0 ? ( ) : projectIssues.count > 0 ? (
<div className="flex flex-col items-center justify-center w-full h-full px-2 pb-8"> <div className="w-full space-y-5">
<div className="w-full h-full flex flex-col space-y-5">
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title="Projects" link="/projects" /> <BreadcrumbItem title="Projects" link="/projects" />
<BreadcrumbItem title={`${activeProject?.name} Issues`} /> <BreadcrumbItem title={`${activeProject?.name ?? "Project"} Issues`} />
</Breadcrumbs> </Breadcrumbs>
<div className="flex items-center justify-between cursor-pointer w-full"> <div className="flex items-center justify-between w-full">
<h2 className="text-2xl font-medium">Project Issues</h2> <h2 className="text-2xl font-medium">Project Issues</h2>
<div className="flex items-center gap-x-3"> <div className="flex items-center gap-x-3">
<div className="flex items-center gap-x-1"> <div className="flex items-center gap-x-1">
@ -271,7 +280,7 @@ const ProjectIssues: NextPage = () => {
<HeaderButton <HeaderButton
Icon={PlusIcon} Icon={PlusIcon}
label="Add Issue" label="Add Issue"
action={() => { onClick={() => {
const e = new KeyboardEvent("keydown", { const e = new KeyboardEvent("keydown", {
key: "i", key: "i",
ctrlKey: true, ctrlKey: true,
@ -281,7 +290,6 @@ const ProjectIssues: NextPage = () => {
/> />
</div> </div>
</div> </div>
<div className="h-full">
{issueView === "list" ? ( {issueView === "list" ? (
<ListView <ListView
properties={properties} properties={properties}
@ -289,19 +297,19 @@ const ProjectIssues: NextPage = () => {
selectedGroup={groupByProperty} selectedGroup={groupByProperty}
setSelectedIssue={setSelectedIssue} setSelectedIssue={setSelectedIssue}
handleDeleteIssue={setDeleteIssue} handleDeleteIssue={setDeleteIssue}
members={members}
/> />
) : ( ) : (
<BoardView <BoardView
properties={properties} properties={properties}
selectedGroup={groupByProperty} selectedGroup={groupByProperty}
groupedByIssues={groupedByIssues} groupedByIssues={groupedByIssues}
members={members}
/> />
)} )}
</div> </div>
</div>
</div>
) : ( ) : (
<div className="w-full h-full flex flex-col justify-center items-center px-4"> <div className="h-full w-full grid place-items-center px-4 sm:px-0">
<EmptySpace <EmptySpace
title="You don't have any issue yet." title="You don't have any issue yet."
description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done." description="Issues help you track individual pieces of work. With Issues, keep track of what's going on, who is working on it, and what's done."
@ -322,7 +330,7 @@ const ProjectIssues: NextPage = () => {
</div> </div>
)} )}
</div> </div>
</ProjectLayout> </AdminLayout>
); );
}; };

View File

@ -13,7 +13,7 @@ import useUser from "lib/hooks/useUser";
// fetching keys // fetching keys
import { PROJECT_MEMBERS, PROJECT_INVITATIONS } from "constants/fetch-keys"; import { PROJECT_MEMBERS, PROJECT_INVITATIONS } from "constants/fetch-keys";
// layouts // layouts
import ProjectLayout from "layouts/ProjectLayout"; import AdminLayout from "layouts/AdminLayout";
// components // components
import SendProjectInvitationModal from "components/project/SendProjectInvitationModal"; import SendProjectInvitationModal from "components/project/SendProjectInvitationModal";
// ui // ui
@ -69,26 +69,23 @@ const ProjectMembers: NextPage = () => {
]; ];
return ( return (
<ProjectLayout> <AdminLayout>
<SendProjectInvitationModal isOpen={isOpen} setIsOpen={setIsOpen} members={members} /> <SendProjectInvitationModal isOpen={isOpen} setIsOpen={setIsOpen} members={members} />
<div className="w-full h-full flex flex-col space-y-5">
{!projectMembers || !projectInvitations ? ( {!projectMembers || !projectInvitations ? (
<div className="w-full h-full flex justify-center items-center"> <div className="h-full w-full grid place-items-center px-4 sm:px-0">
<Spinner /> <Spinner />
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center justify-center w-full h-full px-2"> <div className="h-full w-full space-y-5">
<div className="w-full h-full flex flex-col space-y-5 pb-10 overflow-auto">
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title="Projects" link="/projects" /> <BreadcrumbItem title="Projects" link="/projects" />
<BreadcrumbItem title={`${activeProject?.name} Members`} /> <BreadcrumbItem title={`${activeProject?.name ?? "Project"} Members`} />
</Breadcrumbs> </Breadcrumbs>
<div className="flex items-center justify-between cursor-pointer w-full"> <div className="flex items-center justify-between cursor-pointer w-full">
<h2 className="text-2xl font-medium">Invite Members</h2> <h2 className="text-2xl font-medium">Invite Members</h2>
<HeaderButton Icon={PlusIcon} label="Add Member" action={() => setIsOpen(true)} /> <HeaderButton Icon={PlusIcon} label="Add Member" onClick={() => setIsOpen(true)} />
</div> </div>
{members && members.length === 0 ? null : ( {members && members.length === 0 ? null : (
<>
<table className="min-w-full table-fixed border border-gray-300 md:rounded-lg divide-y divide-gray-300"> <table className="min-w-full table-fixed border border-gray-300 md:rounded-lg divide-y divide-gray-300">
<thead className="bg-gray-50"> <thead className="bg-gray-50">
<tr> <tr>
@ -190,13 +187,10 @@ const ProjectMembers: NextPage = () => {
))} ))}
</tbody> </tbody>
</table> </table>
</>
)} )}
</div> </div>
</div>
)} )}
</div> </AdminLayout>
</ProjectLayout>
); );
}; };

View File

@ -11,7 +11,7 @@ import { useForm, Controller } from "react-hook-form";
// headless ui // headless ui
import { Listbox, Transition } from "@headlessui/react"; import { Listbox, Transition } from "@headlessui/react";
// layouts // layouts
import ProjectLayout from "layouts/ProjectLayout"; import AdminLayout from "layouts/AdminLayout";
// service // service
import projectServices from "lib/services/project.service"; import projectServices from "lib/services/project.service";
import workspaceService from "lib/services/workspace.service"; import workspaceService from "lib/services/workspace.service";
@ -114,16 +114,14 @@ const ProjectSettings: NextPage = () => {
}; };
return ( return (
<ProjectLayout> <AdminLayout>
<div className="w-full h-full space-y-5"> <div className="space-y-5">
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title="Projects" link="/projects" /> <BreadcrumbItem title="Projects" link="/projects" />
<BreadcrumbItem title={`${activeProject?.name} Settings`} /> <BreadcrumbItem title={`${activeProject?.name ?? "Project"} Settings`} />
</Breadcrumbs> </Breadcrumbs>
<div className="w-full h-full flex flex-col space-y-3">
{projectDetails ? ( {projectDetails ? (
<div> <form onSubmit={handleSubmit(onSubmit)}>
<form onSubmit={handleSubmit(onSubmit)} className="mt-3">
<div className="space-y-8"> <div className="space-y-8">
<section className="space-y-5"> <section className="space-y-5">
<div> <div>
@ -209,8 +207,8 @@ const ProjectSettings: NextPage = () => {
<div className="relative"> <div className="relative">
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"> <Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
<span className="block truncate"> <span className="block truncate">
{people?.find((person) => person.member.id === value) {people?.find((person) => person.member.id === value)?.member
?.member.first_name ?? "Select Lead"} .first_name ?? "Select Lead"}
</span> </span>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none"> <span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
<ChevronDownIcon <ChevronDownIcon
@ -254,10 +252,7 @@ const ProjectSettings: NextPage = () => {
active ? "text-white" : "text-indigo-600" active ? "text-white" : "text-indigo-600"
}`} }`}
> >
<CheckIcon <CheckIcon className="h-5 w-5" aria-hidden="true" />
className="h-5 w-5"
aria-hidden="true"
/>
</span> </span>
) : null} ) : null}
</> </>
@ -332,10 +327,7 @@ const ProjectSettings: NextPage = () => {
active ? "text-white" : "text-indigo-600" active ? "text-white" : "text-indigo-600"
}`} }`}
> >
<CheckIcon <CheckIcon className="h-5 w-5" aria-hidden="true" />
className="h-5 w-5"
aria-hidden="true"
/>
</span> </span>
) : null} ) : null}
</> </>
@ -360,15 +352,13 @@ const ProjectSettings: NextPage = () => {
</section> </section>
</div> </div>
</form> </form>
</div>
) : ( ) : (
<div className="w-full h-full flex justify-center items-center"> <div className="h-full w-full grid place-items-center px-4 sm:px-0">
<Spinner /> <Spinner />
</div> </div>
)} )}
</div> </div>
</div> </AdminLayout>
</ProjectLayout>
); );
}; };

View File

@ -4,7 +4,7 @@ import type { NextPage } from "next";
// hooks // hooks
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
// layouts // layouts
import ProjectLayout from "layouts/ProjectLayout"; import AdminLayout from "layouts/AdminLayout";
// components // components
import CreateProjectModal from "components/project/CreateProjectModal"; import CreateProjectModal from "components/project/CreateProjectModal";
import ConfirmProjectDeletion from "components/project/ConfirmProjectDeletion"; import ConfirmProjectDeletion from "components/project/ConfirmProjectDeletion";
@ -61,7 +61,7 @@ const Projects: NextPage = () => {
}, [isOpen]); }, [isOpen]);
return ( return (
<ProjectLayout> <AdminLayout>
<CreateProjectModal isOpen={isOpen && !deleteProject} setIsOpen={setIsOpen} /> <CreateProjectModal isOpen={isOpen && !deleteProject} setIsOpen={setIsOpen} />
<ConfirmProjectDeletion <ConfirmProjectDeletion
isOpen={isOpen && !!deleteProject} isOpen={isOpen && !!deleteProject}
@ -70,10 +70,8 @@ const Projects: NextPage = () => {
/> />
{projects ? ( {projects ? (
<> <>
<div className="flex flex-col items-center justify-center w-full h-full px-2">
<div className="w-full h-full flex flex-col space-y-5 pb-10">
{projects.length === 0 ? ( {projects.length === 0 ? (
<div className="w-full h-full flex flex-col justify-center items-center px-4"> <div className="h-full w-full grid place-items-center px-4 sm:px-0">
<EmptySpace <EmptySpace
title="You don't have any project yet." title="You don't have any project yet."
description="Projects are a collection of issues. They can be used to represent the development work for a product, project, or service." description="Projects are a collection of issues. They can be used to represent the development work for a product, project, or service."
@ -84,9 +82,7 @@ const Projects: NextPage = () => {
description={ description={
<span> <span>
Use{" "} Use{" "}
<pre className="inline bg-gray-100 px-2 py-1 rounded"> <pre className="inline bg-gray-100 px-2 py-1 rounded">Ctrl/Command + P</pre>{" "}
Ctrl/Command + P
</pre>{" "}
shortcut to create a new project shortcut to create a new project
</span> </span>
} }
@ -96,19 +92,14 @@ const Projects: NextPage = () => {
</EmptySpace> </EmptySpace>
</div> </div>
) : ( ) : (
<> <div className="h-full w-full space-y-5">
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name} Projects`} /> <BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Projects`} />
</Breadcrumbs> </Breadcrumbs>
<div className="flex items-center justify-between cursor-pointer w-full"> <div className="flex items-center justify-between cursor-pointer w-full">
<h2 className="text-2xl font-medium">Projects</h2> <h2 className="text-2xl font-medium">Projects</h2>
<HeaderButton <HeaderButton Icon={PlusIcon} label="Add Project" onClick={() => setIsOpen(true)} />
Icon={PlusIcon}
label="Add Project"
action={() => setIsOpen(true)}
/>
</div> </div>
<>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{projects.map((item) => ( {projects.map((item) => (
<ProjectMemberInvitations <ProjectMemberInvitations
@ -126,18 +117,15 @@ const Projects: NextPage = () => {
<Button onClick={submitInvitations}>Submit</Button> <Button onClick={submitInvitations}>Submit</Button>
</div> </div>
)} )}
</> </div>
</>
)} )}
</div>
</div>
</> </>
) : ( ) : (
<div className="w-full h-full flex justify-center items-center"> <div className="w-full h-full flex justify-center items-center">
<Spinner /> <Spinner />
</div> </div>
)} )}
</ProjectLayout> </AdminLayout>
); );
}; };

View File

@ -4,7 +4,7 @@ import Link from "next/link";
// react // react
import React from "react"; import React from "react";
// layouts // layouts
import ProjectLayout from "layouts/ProjectLayout"; import AdminLayout from "layouts/AdminLayout";
// swr // swr
import useSWR from "swr"; import useSWR from "swr";
// hooks // hooks
@ -41,18 +41,26 @@ const Workspace: NextPage = () => {
}, },
]; ];
const hours = new Date().getHours();
return ( return (
<ProjectLayout> <AdminLayout>
<div className="h-full w-full px-2 space-y-5"> <div className="h-full w-full space-y-5">
<div>
{user ? ( {user ? (
<div className="font-medium text-2xl">Good Morning, {user.first_name}!!</div> <div className="font-medium text-2xl">
Good{" "}
{hours >= 4 && hours < 12
? "Morning"
: hours >= 12 && hours < 17
? "Afternoon"
: "Evening"}
, {user.first_name}!!
</div>
) : ( ) : (
<div className="animate-pulse" role="status"> <div className="animate-pulse" role="status">
<div className="font-semibold text-2xl h-8 bg-gray-200 rounded dark:bg-gray-700 w-60"></div> <div className="font-semibold text-2xl h-8 bg-gray-200 rounded dark:bg-gray-700 w-60"></div>
</div> </div>
)} )}
</div>
{/* dashboard */} {/* dashboard */}
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
@ -155,7 +163,7 @@ const Workspace: NextPage = () => {
</div> </div>
</div> </div>
</div> </div>
</ProjectLayout> </AdminLayout>
); );
}; };

View File

@ -15,7 +15,6 @@ import { WORKSPACE_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
import withAuthWrapper from "lib/hoc/withAuthWrapper"; import withAuthWrapper from "lib/hoc/withAuthWrapper";
// layouts // layouts
import AdminLayout from "layouts/AdminLayout"; import AdminLayout from "layouts/AdminLayout";
import ProjectLayout from "layouts/ProjectLayout";
// components // components
import SendWorkspaceInvitationModal from "components/workspace/SendWorkspaceInvitationModal"; import SendWorkspaceInvitationModal from "components/workspace/SendWorkspaceInvitationModal";
// ui // ui
@ -65,7 +64,7 @@ const WorkspaceInvite: NextPage = () => {
]; ];
return ( return (
<ProjectLayout <AdminLayout
meta={{ meta={{
title: "Plane - Workspace Invite", title: "Plane - Workspace Invite",
}} }}
@ -77,18 +76,17 @@ const WorkspaceInvite: NextPage = () => {
members={members} members={members}
/> />
{!workspaceMembers || !workspaceInvitations ? ( {!workspaceMembers || !workspaceInvitations ? (
<div className="w-full h-full flex justify-center items-center"> <div className="h-full w-full grid place-items-center px-4 sm:px-0">
<Spinner /> <Spinner />
</div> </div>
) : ( ) : (
<div className="flex flex-col items-center justify-center w-full h-full px-2"> <div className="w-full space-y-5">
<div className="w-full h-full flex flex-col space-y-5 pb-10">
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name} Members`} /> <BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Members`} />
</Breadcrumbs> </Breadcrumbs>
<div className="flex items-center justify-between cursor-pointer w-full"> <div className="flex items-center justify-between cursor-pointer w-full">
<h2 className="text-2xl font-medium">Invite Members</h2> <h2 className="text-2xl font-medium">Invite Members</h2>
<HeaderButton Icon={PlusIcon} label="Add Member" action={() => setIsOpen(true)} /> <HeaderButton Icon={PlusIcon} label="Add Member" onClick={() => setIsOpen(true)} />
</div> </div>
{members && members.length === 0 ? null : ( {members && members.length === 0 ? null : (
<> <>
@ -129,7 +127,9 @@ const WorkspaceInvite: NextPage = () => {
</td> </td>
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6"> <td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6">
{member?.member ? ( {member?.member ? (
"Accepted" <span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
Accepted
</span>
) : member.status ? ( ) : member.status ? (
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full"> <span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
Accepted Accepted
@ -202,9 +202,8 @@ const WorkspaceInvite: NextPage = () => {
</> </>
)} )}
</div> </div>
</div>
)} )}
</ProjectLayout> </AdminLayout>
); );
}; };

View File

@ -9,7 +9,7 @@ import Dropzone from "react-dropzone";
import workspaceService from "lib/services/workspace.service"; import workspaceService from "lib/services/workspace.service";
import fileServices from "lib/services/file.services"; import fileServices from "lib/services/file.services";
// layouts // layouts
import ProjectLayout from "layouts/ProjectLayout"; import AdminLayout from "layouts/AdminLayout";
// hooks // hooks
import useUser from "lib/hooks/useUser"; import useUser from "lib/hooks/useUser";
@ -21,6 +21,7 @@ import { Spinner, Button, Input, Select } from "ui";
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs"; import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs";
// types // types
import type { IWorkspace } from "types"; import type { IWorkspace } from "types";
import { Tab } from "@headlessui/react";
const defaultValues: Partial<IWorkspace> = { const defaultValues: Partial<IWorkspace> = {
name: "", name: "",
@ -79,27 +80,35 @@ const WorkspaceSettings = () => {
}; };
return ( return (
<ProjectLayout <AdminLayout
meta={{ meta={{
title: "Plane - Workspace Settings", title: "Plane - Workspace Settings",
}} }}
> >
<ConfirmWorkspaceDeletion isOpen={isOpen} setIsOpen={setIsOpen} /> <ConfirmWorkspaceDeletion isOpen={isOpen} setIsOpen={setIsOpen} />
<div className="space-y-5">
<div className="w-full h-full space-y-5">
<Breadcrumbs> <Breadcrumbs>
<BreadcrumbItem title={`${activeWorkspace?.name} Settings`} /> <BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Settings`} />
</Breadcrumbs> </Breadcrumbs>
<div className="w-full h-full flex flex-col space-y-3">
{activeWorkspace ? ( {activeWorkspace ? (
<div className="space-y-8"> <div className="space-y-8">
<section className="space-y-5"> <Tab.Group>
<div> <Tab.List className="flex items-center gap-3">
<h3 className="text-lg font-medium leading-6 text-gray-900">General</h3> {["General", "Actions"].map((tab, index) => (
<p className="mt-1 text-sm text-gray-500"> <Tab
This information will be displayed to every member of the workspace. key={index}
</p> className={({ selected }) =>
</div> `text-md leading-6 text-gray-900 px-4 py-1 rounded outline-none ${
selected ? "bg-gray-700 text-white" : "hover:bg-gray-200"
} duration-300`
}
>
{tab}
</Tab>
))}
</Tab.List>
<Tab.Panels>
<Tab.Panel>
<div className="grid grid-cols-2 gap-6"> <div className="grid grid-cols-2 gap-6">
<div className="w-full space-y-3"> <div className="w-full space-y-3">
<Dropzone <Dropzone
@ -117,7 +126,9 @@ const WorkspaceSettings = () => {
<div className="text-gray-500 mb-2">Logo</div> <div className="text-gray-500 mb-2">Logo</div>
<div> <div>
<div className="h-60 bg-blue-50" {...getRootProps()}> <div className="h-60 bg-blue-50" {...getRootProps()}>
{((watch("logo") && watch("logo") !== null && watch("logo") !== "") || {((watch("logo") &&
watch("logo") !== null &&
watch("logo") !== "") ||
(image && image !== null)) && ( (image && image !== null)) && (
<div className="relative flex mx-auto h-60"> <div className="relative flex mx-auto h-60">
<Image <Image
@ -197,29 +208,24 @@ const WorkspaceSettings = () => {
</div> </div>
</div> </div>
</div> </div>
</section> </Tab.Panel>
<section className="space-y-5"> <Tab.Panel>
<div>
<h3 className="text-lg font-medium leading-6 text-gray-900">Actions</h3>
<p className="mt-1 text-sm text-gray-500">
Once deleted, it will be gone forever. Please be certain.
</p>
</div>
<div> <div>
<Button theme="danger" onClick={() => setIsOpen(true)}> <Button theme="danger" onClick={() => setIsOpen(true)}>
Delete the workspace Delete the workspace
</Button> </Button>
</div> </div>
</section> </Tab.Panel>
</Tab.Panels>
</Tab.Group>
</div> </div>
) : ( ) : (
<div className="w-full h-full flex justify-center items-center"> <div className="h-full w-full grid place-items-center px-4 sm:px-0">
<Spinner /> <Spinner />
</div> </div>
)} )}
</div> </div>
</div> </AdminLayout>
</ProjectLayout>
); );
}; };

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,8 @@ module.exports = {
extend: { extend: {
colors: { colors: {
theme: "#4338ca", theme: "#4338ca",
primary: "#f9fafb", // gray-50
secondary: "white",
}, },
keyframes: { keyframes: {
leftToaster: { leftToaster: {

View File

@ -32,7 +32,7 @@ export interface ProjectMember {
email: string; email: string;
message: string; message: string;
role: 5 | 10 | 15 | 20; role: 5 | 10 | 15 | 20;
member: string; member: any;
member_id: string; member_id: string;
user_id: string; user_id: string;
} }

View File

@ -6,7 +6,7 @@ type BreadcrumbsProps = {
children: any; children: any;
}; };
const Breadcrumbs = (props: BreadcrumbsProps) => { const Breadcrumbs: React.FC<BreadcrumbsProps> = ({ children }: BreadcrumbsProps) => {
const router = useRouter(); const router = useRouter();
return ( return (
@ -20,7 +20,7 @@ const Breadcrumbs = (props: BreadcrumbsProps) => {
<ArrowLeftIcon className="h-3 w-3" /> <ArrowLeftIcon className="h-3 w-3" />
</p> </p>
</div> </div>
{props.children} {children}
</div> </div>
</> </>
); );
@ -32,23 +32,23 @@ type BreadcrumbItemProps = {
icon?: any; icon?: any;
}; };
const BreadcrumbItem = (props: BreadcrumbItemProps) => { const BreadcrumbItem: React.FC<BreadcrumbItemProps> = ({ title, link, icon }) => {
return ( return (
<> <>
{props.link ? ( {link ? (
<Link href={props.link}> <Link href={link}>
<a className="bg-indigo-50 hover:bg-indigo-100 duration-300 px-4 py-1 rounded-tl-lg rounded-tr-md rounded-br-lg rounded-bl-md skew-x-[-20deg] text-sm text-center"> <a className="bg-indigo-50 hover:bg-indigo-100 duration-300 px-4 py-1 rounded-tl-lg rounded-tr-md rounded-br-lg rounded-bl-md skew-x-[-20deg] text-sm text-center">
<p className={`skew-x-[20deg] ${props.icon ? "flex items-center gap-2" : ""}`}> <p className={`skew-x-[20deg] ${icon ? "flex items-center gap-2" : ""}`}>
{props?.icon} {icon ?? null}
{props.title} {title}
</p> </p>
</a> </a>
</Link> </Link>
) : ( ) : (
<div className="bg-indigo-50 px-4 py-1 rounded-tl-lg rounded-tr-md rounded-br-lg rounded-bl-md skew-x-[-20deg] text-sm text-center"> <div className="bg-indigo-50 px-4 py-1 rounded-tl-lg rounded-tr-md rounded-br-lg rounded-bl-md skew-x-[-20deg] text-sm text-center">
<p className={`skew-x-[20deg] ${props.icon ? "flex items-center gap-2" : ""}`}> <p className={`skew-x-[20deg] ${icon ? "flex items-center gap-2" : ""}`}>
{props?.icon} {icon}
{props.title} {title}
</p> </p>
</div> </div>
)} )}

View File

@ -18,7 +18,7 @@ type EmptySpaceProps = {
link?: { text: string; href: string }; link?: { text: string; href: string };
}; };
const EmptySpace = ({ title, description, children, Icon, link }: EmptySpaceProps) => { const EmptySpace: React.FC<EmptySpaceProps> = ({ title, description, children, Icon, link }) => {
return ( return (
<> <>
<div className="max-w-lg"> <div className="max-w-lg">
@ -61,13 +61,13 @@ type EmptySpaceItemProps = {
action: () => void; action: () => void;
}; };
const EmptySpaceItem = ({ const EmptySpaceItem: React.FC<EmptySpaceItemProps> = ({
title, title,
description, description,
bgColor = "blue", bgColor = "blue",
Icon, Icon,
action, action,
}: EmptySpaceItemProps) => { }) => {
return ( return (
<> <>
<li className="cursor-pointer" onClick={action}> <li className="cursor-pointer" onClick={action}>

View File

@ -6,16 +6,29 @@ type HeaderButtonProps = {
} }
) => JSX.Element; ) => JSX.Element;
label: string; label: string;
action: () => void; disabled?: boolean;
onClick: () => void;
className?: string;
position?: "normal" | "reverse";
}; };
const HeaderButton = ({ Icon, label, action }: HeaderButtonProps) => { const HeaderButton = ({
Icon,
label,
disabled = false,
onClick,
className = "",
position = "normal",
}: HeaderButtonProps) => {
return ( return (
<> <>
<button <button
type="button" type="button"
className="bg-theme text-white border border-indigo-600 text-xs flex items-center gap-x-1 p-2 rounded-md font-medium whitespace-nowrap outline-none" className={`bg-theme text-white border border-indigo-600 text-xs flex items-center gap-x-1 p-2 rounded-md font-medium whitespace-nowrap outline-none ${
onClick={action} position === "reverse" && "flex-row-reverse"
} ${className}`}
disabled={disabled}
onClick={onClick}
> >
<Icon className="h-4 w-4" /> <Icon className="h-4 w-4" />
{label} {label}

View File

@ -6,3 +6,5 @@ export { default as ListBox } from "./ListBox";
export { default as Spinner } from "./Spinner"; export { default as Spinner } from "./Spinner";
export { default as Tooltip } from "./Tooltip"; export { default as Tooltip } from "./Tooltip";
export { default as SearchListbox } from "./SearchListbox"; export { default as SearchListbox } from "./SearchListbox";
export * from "./Breadcrumbs";
export * from "./EmptySpace";