mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
Merge pull request #3 from aaryan610/master
dev: profile page, issue details page design
This commit is contained in:
commit
60f0e3155b
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -50,7 +50,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 ? (
|
||||||
|
@ -147,7 +147,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) => {
|
||||||
@ -394,7 +394,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} />
|
||||||
|
@ -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,65 +100,88 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sidebarOptions = [
|
||||||
|
{
|
||||||
|
label: "Priority",
|
||||||
|
name: "priority",
|
||||||
|
canSelectMultipleOptions: false,
|
||||||
|
icon: ChartBarIcon,
|
||||||
|
options: PRIORITIES.map((property) => ({
|
||||||
|
label: property,
|
||||||
|
value: property,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Status",
|
||||||
|
name: "state",
|
||||||
|
canSelectMultipleOptions: false,
|
||||||
|
icon: Squares2X2Icon,
|
||||||
|
options: states?.map((state) => ({
|
||||||
|
label: state.name,
|
||||||
|
value: state.id,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Assignees",
|
||||||
|
name: "assignees_list",
|
||||||
|
canSelectMultipleOptions: true,
|
||||||
|
icon: UserGroupIcon,
|
||||||
|
options: people?.map((person) => ({
|
||||||
|
label: person.member.first_name,
|
||||||
|
value: person.member.id,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Blocker",
|
||||||
|
name: "blockers_list",
|
||||||
|
canSelectMultipleOptions: true,
|
||||||
|
icon: UserIcon,
|
||||||
|
options: projectIssues?.results?.map((issue) => ({
|
||||||
|
label: issue.name,
|
||||||
|
value: issue.id,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Blocked",
|
||||||
|
name: "blocked_list",
|
||||||
|
canSelectMultipleOptions: true,
|
||||||
|
icon: UserIcon,
|
||||||
|
options: projectIssues?.results?.map((issue) => ({
|
||||||
|
label: issue.name,
|
||||||
|
value: issue.id,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="h-full w-full">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex flex-col gap-y-4">
|
<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">
|
||||||
label: "Priority",
|
<button
|
||||||
name: "priority",
|
type="button"
|
||||||
canSelectMultipleOptions: false,
|
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"
|
||||||
icon: Bars3BottomRightIcon,
|
onClick={() =>
|
||||||
options: PRIORITIES.map((property) => ({
|
copyTextToClipboard(
|
||||||
label: property,
|
`https://app.plane.so/projects/${activeProject?.id}/issues/${issueDetail?.id}`
|
||||||
value: property,
|
)
|
||||||
})),
|
}
|
||||||
},
|
>
|
||||||
{
|
<LinkIcon className="h-3.5 w-3.5" />
|
||||||
label: "Status",
|
</button>
|
||||||
name: "state",
|
<button
|
||||||
canSelectMultipleOptions: false,
|
type="button"
|
||||||
icon: Bars3BottomRightIcon,
|
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"
|
||||||
options: states?.map((state) => ({
|
onClick={() => copyTextToClipboard(`${issueDetail?.id}`)}
|
||||||
label: state.name,
|
>
|
||||||
value: state.id,
|
<ClipboardDocumentIcon className="h-3.5 w-3.5" />
|
||||||
})),
|
</button>
|
||||||
},
|
</div>
|
||||||
{
|
{sidebarOptions.map((item) => (
|
||||||
label: "Assignees",
|
<div className="flex items-center justify-between gap-x-2" key={item.label}>
|
||||||
name: "assignees_list",
|
<div className="flex items-center gap-x-2 text-sm">
|
||||||
canSelectMultipleOptions: true,
|
<item.icon className="h-4 w-4" />
|
||||||
icon: UserIcon,
|
|
||||||
options: people?.map((person) => ({
|
|
||||||
label: person.member.first_name,
|
|
||||||
value: person.member.id,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Blocker",
|
|
||||||
name: "blockers_list",
|
|
||||||
canSelectMultipleOptions: true,
|
|
||||||
icon: UserIcon,
|
|
||||||
options: projectIssues?.results?.map((issue) => ({
|
|
||||||
label: issue.name,
|
|
||||||
value: issue.id,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Blocked",
|
|
||||||
name: "blocked_list",
|
|
||||||
canSelectMultipleOptions: true,
|
|
||||||
icon: UserIcon,
|
|
||||||
options: projectIssues?.results?.map((issue) => ({
|
|
||||||
label: issue.name,
|
|
||||||
value: issue.id,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
].map((item) => (
|
|
||||||
<div className="flex items-center gap-x-2" key={item.label}>
|
|
||||||
<div className="flex items-center gap-x-2">
|
|
||||||
<item.icon className="w-5 h-5 text-gray-500" />
|
|
||||||
<p>{item.label}</p>
|
<p>{item.label}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -160,68 +193,61 @@ 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 }) => (
|
||||||
<>
|
<div className="relative">
|
||||||
<Listbox.Label className="sr-only">{item.label}</Listbox.Label>
|
<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">
|
||||||
<div className="relative">
|
<span
|
||||||
<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">
|
className={classNames(
|
||||||
<PlusIcon
|
value ? "" : "text-gray-900",
|
||||||
className="h-5 w-5 flex-shrink-0 text-gray-300 sm:-ml-1"
|
"hidden truncate sm:block w-16 text-left",
|
||||||
aria-hidden="true"
|
item.label === "Priority" ? "capitalize" : ""
|
||||||
/>
|
)}
|
||||||
<span
|
|
||||||
className={classNames(
|
|
||||||
value ? "" : "text-gray-900",
|
|
||||||
"hidden truncate capitalize sm:ml-2 sm:block w-16"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{value
|
|
||||||
? Array.isArray(value)
|
|
||||||
? value
|
|
||||||
.map(
|
|
||||||
(i: any) =>
|
|
||||||
item.options?.find((option) => option.value === i)
|
|
||||||
?.label
|
|
||||||
)
|
|
||||||
.join(", ") || `Select ${item.label}`
|
|
||||||
: item.options?.find((option) => option.value === value)?.label
|
|
||||||
: `Select ${item.label}`}
|
|
||||||
</span>
|
|
||||||
</Listbox.Button>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
show={open}
|
|
||||||
as={React.Fragment}
|
|
||||||
leave="transition ease-in duration-100"
|
|
||||||
leaveFrom="opacity-100"
|
|
||||||
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">
|
{value
|
||||||
|
? Array.isArray(value)
|
||||||
|
? value
|
||||||
|
.map(
|
||||||
|
(i: any) =>
|
||||||
|
item.options?.find((option) => option.value === i)?.label
|
||||||
|
)
|
||||||
|
.join(", ") || item.label
|
||||||
|
: item.options?.find((option) => option.value === value)?.label
|
||||||
|
: "None"}
|
||||||
|
</span>
|
||||||
|
<ChevronDownIcon className="h-3 w-3" />
|
||||||
|
</Listbox.Button>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
show={open}
|
||||||
|
as={React.Fragment}
|
||||||
|
leave="transition ease-in duration-100"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<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">
|
{option.label}
|
||||||
<span className="ml-3 block capitalize font-medium">
|
|
||||||
{option.label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</Listbox.Option>
|
</Listbox.Option>
|
||||||
))}
|
))}
|
||||||
</Listbox.Options>
|
</div>
|
||||||
</Transition>
|
</Listbox.Options>
|
||||||
</div>
|
</Transition>
|
||||||
</>
|
</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">
|
||||||
{issueLabels?.map((label: any) => (
|
<div className="p-1">
|
||||||
<Listbox.Option
|
{issueLabels?.map((label: any) => (
|
||||||
key={label.id}
|
<Listbox.Option
|
||||||
className={({ active, selected }) =>
|
key={label.id}
|
||||||
classNames(
|
className={({ active, selected }) =>
|
||||||
active || selected ? "bg-indigo-50" : "bg-white",
|
`${
|
||||||
"relative cursor-default select-none py-2 px-3"
|
active || selected ? "text-white bg-theme" : "text-gray-900"
|
||||||
)
|
} cursor-pointer select-none relative p-2 rounded-md truncate`
|
||||||
}
|
}
|
||||||
value={label.id}
|
value={label.id}
|
||||||
>
|
>
|
||||||
<div className="flex items-center">
|
{label.name}
|
||||||
<span className="ml-3 block capitalize font-medium">
|
</Listbox.Option>
|
||||||
{label.name}
|
))}
|
||||||
</span>
|
</div>
|
||||||
</div>
|
|
||||||
</Listbox.Option>
|
|
||||||
))}
|
|
||||||
</Listbox.Options>
|
</Listbox.Options>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
122
components/project/issues/issue-detail/activity/index.tsx
Normal file
122
components/project/issues/issue-detail/activity/index.tsx
Normal 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;
|
@ -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>
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
|
@ -27,6 +27,7 @@ import {
|
|||||||
UserIcon,
|
UserIcon,
|
||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
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";
|
||||||
@ -58,7 +59,7 @@ const navigation = (projectId: string) => [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const navLinks = [
|
const workspaceLinks = [
|
||||||
{
|
{
|
||||||
icon: HomeIcon,
|
icon: HomeIcon,
|
||||||
name: "Home",
|
name: "Home",
|
||||||
@ -117,7 +118,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}>
|
||||||
@ -203,7 +204,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
|
<div
|
||||||
@ -215,7 +216,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"
|
||||||
|
: ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex gap-x-1 items-center">
|
<div className="flex gap-x-1 items-center">
|
||||||
@ -385,12 +388,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" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -404,6 +409,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
|
||||||
@ -445,8 +461,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" : ""
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -491,8 +507,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()}
|
||||||
>
|
>
|
||||||
|
@ -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;
|
|
@ -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");
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ import type { NextPage } from "next";
|
|||||||
// 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";
|
||||||
// ui
|
// ui
|
||||||
@ -66,7 +66,7 @@ const MyIssues: NextPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectLayout>
|
<AdminLayout>
|
||||||
<div className="w-full h-full flex flex-col space-y-5">
|
<div className="w-full h-full flex flex-col space-y-5">
|
||||||
{myIssues ? (
|
{myIssues ? (
|
||||||
<>
|
<>
|
||||||
@ -81,7 +81,7 @@ const MyIssues: 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,
|
||||||
@ -199,7 +199,7 @@ const MyIssues: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ProjectLayout>
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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,164 +74,227 @@ 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">
|
||||||
<Dropzone
|
<section className="relative p-5 rounded-xl flex gap-10 bg-secondary">
|
||||||
multiple={false}
|
<div
|
||||||
accept={{
|
className="absolute top-4 right-4 bg-indigo-100 hover:bg-theme hover:text-white rounded p-1 cursor-pointer duration-300"
|
||||||
"image/*": [],
|
onClick={() => setIsEditing((prevData) => !prevData)}
|
||||||
}}
|
>
|
||||||
onDrop={(files) => {
|
{isEditing ? (
|
||||||
setImage(files[0]);
|
<XMarkIcon className="h-4 w-4" />
|
||||||
}}
|
) : (
|
||||||
>
|
<PencilIcon className="h-4 w-4" />
|
||||||
{({ getRootProps, getInputProps }) => (
|
)}
|
||||||
<div className="space-y-4">
|
|
||||||
<input {...getInputProps()} />
|
|
||||||
<h2 className="font-semibold text-xl">Profile Picture</h2>
|
|
||||||
<div className="relative">
|
|
||||||
<span
|
|
||||||
className="inline-block h-24 w-24 rounded-full overflow-hidden bg-gray-100"
|
|
||||||
{...getRootProps()}
|
|
||||||
>
|
|
||||||
{(!watch("avatar") || watch("avatar") === "") &&
|
|
||||||
(!image || image === null) ? (
|
|
||||||
<UserIcon className="h-full w-full text-gray-300" />
|
|
||||||
) : (
|
|
||||||
<div className="relative h-24 w-24 overflow-hidden">
|
|
||||||
<Image
|
|
||||||
src={image ? URL.createObjectURL(image) : watch("avatar")}
|
|
||||||
alt={myProfile.first_name}
|
|
||||||
layout="fill"
|
|
||||||
objectFit="cover"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-500 text-sm">
|
|
||||||
Max file size is 500kb. Supported file types are .jpg and .png.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Dropzone>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className="mt-4"
|
|
||||||
onClick={() => {
|
|
||||||
if (image === null) return;
|
|
||||||
setIsImageUploading(true);
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("asset", image);
|
|
||||||
formData.append("attributes", JSON.stringify({}));
|
|
||||||
fileServices
|
|
||||||
.uploadFile(formData)
|
|
||||||
.then((response) => {
|
|
||||||
const imageUrl = response.asset;
|
|
||||||
setValue("avatar", imageUrl);
|
|
||||||
handleSubmit(onSubmit)();
|
|
||||||
setIsImageUploading(false);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setIsImageUploading(false);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isImageUploading ? "Uploading..." : "Upload"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="mt-5 w-3/5">
|
|
||||||
<form onSubmit={handleSubmit(onSubmit)}>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<h2 className="font-semibold text-xl">Details</h2>
|
|
||||||
<div className="flex gap-x-4">
|
|
||||||
<div className="flex-grow">
|
|
||||||
<Input
|
|
||||||
name="first_name"
|
|
||||||
id="first_name"
|
|
||||||
register={register}
|
|
||||||
error={errors.first_name}
|
|
||||||
label="First Name"
|
|
||||||
placeholder="Enter your first name"
|
|
||||||
autoComplete="off"
|
|
||||||
validations={{
|
|
||||||
required: "This field is required.",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex-grow">
|
|
||||||
<Input
|
|
||||||
name="last_name"
|
|
||||||
register={register}
|
|
||||||
error={errors.last_name}
|
|
||||||
id="last_name"
|
|
||||||
label="Last Name"
|
|
||||||
placeholder="Enter your last name"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
register={register}
|
|
||||||
error={errors.email}
|
|
||||||
name="email"
|
|
||||||
validations={{
|
|
||||||
required: "Email is required",
|
|
||||||
}}
|
|
||||||
label="Email"
|
|
||||||
placeholder="Enter email"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button disabled={isSubmitting} type="submit">
|
|
||||||
{isSubmitting ? "Updating Profile..." : "Update Profile"}
|
|
||||||
</Button>
|
|
||||||
</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>
|
</div>
|
||||||
</form>
|
<div className="flex-shrink-0">
|
||||||
|
<Dropzone
|
||||||
|
multiple={false}
|
||||||
|
accept={{
|
||||||
|
"image/*": [],
|
||||||
|
}}
|
||||||
|
onDrop={(files) => {
|
||||||
|
setImage(files[0]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{({ getRootProps, getInputProps, open }) => (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
<div className="relative">
|
||||||
|
<span
|
||||||
|
className="inline-block h-40 w-40 rounded overflow-hidden bg-gray-100"
|
||||||
|
{...getRootProps()}
|
||||||
|
>
|
||||||
|
{(!watch("avatar") || watch("avatar") === "") &&
|
||||||
|
(!image || image === null) ? (
|
||||||
|
<UserIcon className="h-full w-full text-gray-300" />
|
||||||
|
) : (
|
||||||
|
<div className="relative h-40 w-40 overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={image ? URL.createObjectURL(image) : watch("avatar")}
|
||||||
|
alt={myProfile.first_name}
|
||||||
|
layout="fill"
|
||||||
|
objectFit="cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-500 text-sm">
|
||||||
|
Max file size is 500kb.
|
||||||
|
<br />
|
||||||
|
Supported file types are .jpg and .png.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className="mt-4"
|
||||||
|
onClick={() => {
|
||||||
|
if (image === null) open();
|
||||||
|
else {
|
||||||
|
setIsImageUploading(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("asset", image);
|
||||||
|
formData.append("attributes", JSON.stringify({}));
|
||||||
|
fileServices
|
||||||
|
.uploadFile(formData)
|
||||||
|
.then((response) => {
|
||||||
|
const imageUrl = response.asset;
|
||||||
|
setValue("avatar", imageUrl);
|
||||||
|
handleSubmit(onSubmit)();
|
||||||
|
setIsImageUploading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setIsImageUploading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isImageUploading ? "Uploading..." : "Upload"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Dropzone>
|
||||||
|
</div>
|
||||||
|
<form className="space-y-5" onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<div className="grid grid-cols-2 gap-x-10 gap-y-5 mt-2">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm text-gray-500">First Name</h4>
|
||||||
|
{isEditing ? (
|
||||||
|
<Input
|
||||||
|
name="first_name"
|
||||||
|
id="first_name"
|
||||||
|
register={register}
|
||||||
|
error={errors.first_name}
|
||||||
|
placeholder="Enter your first name"
|
||||||
|
autoComplete="off"
|
||||||
|
validations={{
|
||||||
|
required: "This field is required.",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<h2>{myProfile.first_name}</h2>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm text-gray-500">Last Name</h4>
|
||||||
|
{isEditing ? (
|
||||||
|
<Input
|
||||||
|
name="last_name"
|
||||||
|
register={register}
|
||||||
|
error={errors.last_name}
|
||||||
|
id="last_name"
|
||||||
|
placeholder="Enter your last name"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<h2>{myProfile.last_name}</h2>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm text-gray-500">Email ID</h4>
|
||||||
|
{isEditing ? (
|
||||||
|
<Input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
register={register}
|
||||||
|
error={errors.email}
|
||||||
|
name="email"
|
||||||
|
validations={{
|
||||||
|
required: "Email is required",
|
||||||
|
}}
|
||||||
|
placeholder="Enter email"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<h2>{myProfile.email}</h2>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isEditing && (
|
||||||
|
<div>
|
||||||
|
<Button type="submit" disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? "Updating Profile..." : "Update Profile"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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,65 +134,58 @@ 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="h-full w-full space-y-5">
|
||||||
<div className="flex flex-col items-center justify-center w-full h-full px-2">
|
<Breadcrumbs>
|
||||||
<div className="w-full h-full flex flex-col space-y-5">
|
<BreadcrumbItem title="Projects" link="/projects" />
|
||||||
<Breadcrumbs>
|
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Cycles`} />
|
||||||
<BreadcrumbItem title="Projects" link="/projects" />
|
</Breadcrumbs>
|
||||||
<BreadcrumbItem title={`${activeProject?.name} Cycles`} />
|
<div className="flex items-center justify-between cursor-pointer w-full">
|
||||||
</Breadcrumbs>
|
<h2 className="text-2xl font-medium">Project Cycle</h2>
|
||||||
<div className="flex items-center justify-between cursor-pointer w-full">
|
<HeaderButton Icon={PlusIcon} label="Add Cycle" onClick={() => setIsOpen(true)} />
|
||||||
<h2 className="text-2xl font-medium">Project Cycle</h2>
|
</div>
|
||||||
<HeaderButton Icon={PlusIcon} label="Add Cycle" action={() => setIsOpen(true)} />
|
<div className="h-full w-full">
|
||||||
</div>
|
{sprints.map((sprint) => (
|
||||||
<div className="w-full h-full pr-2 overflow-auto">
|
<SprintView
|
||||||
{sprints.map((sprint) => (
|
sprint={sprint}
|
||||||
<SprintView
|
selectSprint={setSelectedSprint}
|
||||||
sprint={sprint}
|
projectId={projectId as string}
|
||||||
selectSprint={setSelectedSprint}
|
workspaceSlug={activeWorkspace?.slug as string}
|
||||||
projectId={projectId as string}
|
openIssueModal={openIssueModal}
|
||||||
workspaceSlug={activeWorkspace?.slug as string}
|
addIssueToSprint={addIssueToSprint}
|
||||||
openIssueModal={openIssueModal}
|
key={sprint.id}
|
||||||
addIssueToSprint={addIssueToSprint}
|
/>
|
||||||
key={sprint.id}
|
))}
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div className="w-full h-full flex flex-col justify-center items-center px-4">
|
|
||||||
<EmptySpace
|
|
||||||
title="You don't have any cycle yet."
|
|
||||||
description="A cycle is a fixed time period where a team commits to a set number of issues from their backlog. Cycles are usually one, two, or four weeks long."
|
|
||||||
Icon={ArrowPathIcon}
|
|
||||||
>
|
|
||||||
<EmptySpaceItem
|
|
||||||
title="Create a new cycle"
|
|
||||||
description={
|
|
||||||
<span>
|
|
||||||
Use{" "}
|
|
||||||
<pre className="inline bg-gray-100 px-2 py-1 rounded">Ctrl/Command + Q</pre>{" "}
|
|
||||||
shortcut to create a new cycle
|
|
||||||
</span>
|
|
||||||
}
|
|
||||||
Icon={PlusIcon}
|
|
||||||
action={() => setIsOpen(true)}
|
|
||||||
/>
|
|
||||||
</EmptySpace>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<div className="w-full h-full flex justify-center items-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : (
|
||||||
</div>
|
<div className="w-full h-full flex flex-col justify-center items-center px-4">
|
||||||
</ProjectLayout>
|
<EmptySpace
|
||||||
|
title="You don't have any cycle yet."
|
||||||
|
description="A cycle is a fixed time period where a team commits to a set number of issues from their backlog. Cycles are usually one, two, or four weeks long."
|
||||||
|
Icon={ArrowPathIcon}
|
||||||
|
>
|
||||||
|
<EmptySpaceItem
|
||||||
|
title="Create a new cycle"
|
||||||
|
description={
|
||||||
|
<span>
|
||||||
|
Use <pre className="inline bg-gray-100 px-2 py-1 rounded">Ctrl/Command + Q</pre>{" "}
|
||||||
|
shortcut to create a new cycle
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
Icon={PlusIcon}
|
||||||
|
action={() => setIsOpen(true)}
|
||||||
|
/>
|
||||||
|
</EmptySpace>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<div className="w-full h-full flex justify-center items-center">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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,226 +152,128 @@ 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>
|
onClick={() => {
|
||||||
<div className="flex gap-x-2">
|
if (!prevIssue) return;
|
||||||
<button
|
router.push(`/projects/${prevIssue.project}/issues/${prevIssue.id}`);
|
||||||
type="button"
|
}}
|
||||||
className={`px-4 py-1.5 bg-white rounded-lg ${
|
/>
|
||||||
prevIssue ? "hover:bg-gray-100" : "bg-gray-100"
|
<HeaderButton
|
||||||
}`}
|
Icon={ChevronRightIcon}
|
||||||
disabled={prevIssue ? false : true}
|
disabled={!nextIssue}
|
||||||
onClick={() => {
|
label="Next"
|
||||||
if (!prevIssue) return;
|
onClick={() => {
|
||||||
router.push(`/projects/${prevIssue.project}/issues/${prevIssue.id}`);
|
if (!nextIssue) return;
|
||||||
}}
|
router.push(`/projects/${nextIssue.project}/issues/${nextIssue?.id}`);
|
||||||
>
|
}}
|
||||||
Previous
|
position="reverse"
|
||||||
</button>
|
/>
|
||||||
<button
|
</div>
|
||||||
type="button"
|
|
||||||
className={`px-4 py-1.5 bg-white rounded-lg ${
|
|
||||||
nextIssue ? "hover:bg-gray-100" : "bg-gray-100"
|
|
||||||
}`}
|
|
||||||
disabled={nextIssue ? false : true}
|
|
||||||
onClick={() => {
|
|
||||||
if (!nextIssue) return;
|
|
||||||
router.push(`/projects/${nextIssue.project}/issues/${nextIssue?.id}`);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-wrap">
|
|
||||||
<div className="w-full lg:w-3/4 h-full px-2 md:px-10 py-10 overflow-auto">
|
|
||||||
<div className="w-full h-full space-y-5">
|
|
||||||
<TextArea
|
|
||||||
id="name"
|
|
||||||
placeholder="Enter issue name"
|
|
||||||
name="name"
|
|
||||||
autoComplete="off"
|
|
||||||
validations={{ required: true }}
|
|
||||||
register={register}
|
|
||||||
onChange={debounce(() => {
|
|
||||||
handleSubmit(submitChanges)();
|
|
||||||
}, 5000)}
|
|
||||||
mode="transparent"
|
|
||||||
className="text-3xl sm:text-3xl"
|
|
||||||
/>
|
|
||||||
<TextArea
|
|
||||||
id="description"
|
|
||||||
name="description"
|
|
||||||
error={errors.description}
|
|
||||||
validations={{
|
|
||||||
required: true,
|
|
||||||
}}
|
|
||||||
onChange={debounce(() => {
|
|
||||||
handleSubmit(submitChanges)();
|
|
||||||
}, 5000)}
|
|
||||||
placeholder="Enter issue description"
|
|
||||||
mode="transparent"
|
|
||||||
register={register}
|
|
||||||
/>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
|
||||||
<div className="w-full border-t border-gray-300" />
|
|
||||||
</div>
|
|
||||||
<div className="relative flex justify-center">
|
|
||||||
<span className="bg-gray-50 px-2 text-sm text-gray-500">
|
|
||||||
Activity/Comments
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<Tab.Group>
|
|
||||||
<Tab.List className="flex gap-x-3">
|
|
||||||
{["Comments", "Activity"].map((item) => (
|
|
||||||
<Tab
|
|
||||||
key={item}
|
|
||||||
className={({ selected }) =>
|
|
||||||
`px-3 py-1 text-sm rounded-md ${
|
|
||||||
selected ? "bg-gray-800 text-white" : ""
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{item}
|
|
||||||
</Tab>
|
|
||||||
))}
|
|
||||||
</Tab.List>
|
|
||||||
<Tab.Panels className="mt-5">
|
|
||||||
<Tab.Panel>
|
|
||||||
<IssueCommentSection
|
|
||||||
comments={issueComments}
|
|
||||||
workspaceSlug={activeWorkspace?.slug as string}
|
|
||||||
projectId={projectId as string}
|
|
||||||
issueId={issueId as string}
|
|
||||||
/>
|
|
||||||
</Tab.Panel>
|
|
||||||
<Tab.Panel>
|
|
||||||
{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"
|
|
||||||
>
|
|
||||||
{/* <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.Panels>
|
|
||||||
</Tab.Group>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full lg:w-1/4 h-full border-l px-2 md:px-10 py-10">
|
|
||||||
<IssueDetailSidebar control={control} submitChanges={submitChanges} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<div className="w-full h-full flex items-center justify-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{issueDetail && activeProject ? (
|
||||||
|
<div className="grid grid-cols-4 gap-5">
|
||||||
|
<div className="col-span-3 space-y-5">
|
||||||
|
<div className="bg-secondary rounded-lg p-5">
|
||||||
|
<TextArea
|
||||||
|
id="name"
|
||||||
|
placeholder="Enter issue name"
|
||||||
|
name="name"
|
||||||
|
autoComplete="off"
|
||||||
|
validations={{ required: true }}
|
||||||
|
register={register}
|
||||||
|
onChange={debounce(() => {
|
||||||
|
handleSubmit(submitChanges)();
|
||||||
|
}, 5000)}
|
||||||
|
mode="transparent"
|
||||||
|
className="text-3xl sm:text-3xl"
|
||||||
|
/>
|
||||||
|
<TextArea
|
||||||
|
id="description"
|
||||||
|
name="description"
|
||||||
|
error={errors.description}
|
||||||
|
validations={{
|
||||||
|
required: true,
|
||||||
|
}}
|
||||||
|
onChange={debounce(() => {
|
||||||
|
handleSubmit(submitChanges)();
|
||||||
|
}, 5000)}
|
||||||
|
placeholder="Enter issue description"
|
||||||
|
mode="transparent"
|
||||||
|
register={register}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="bg-secondary rounded-lg p-5">
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||||
|
<div className="w-full border-t border-gray-300" />
|
||||||
|
</div>
|
||||||
|
<div className="relative flex justify-center">
|
||||||
|
<span className="bg-white px-2 text-sm text-gray-500">Activity/Comments</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full space-y-5 mt-3">
|
||||||
|
<Tab.Group>
|
||||||
|
<Tab.List className="flex gap-x-3">
|
||||||
|
{["Comments", "Activity"].map((item) => (
|
||||||
|
<Tab
|
||||||
|
key={item}
|
||||||
|
className={({ selected }) =>
|
||||||
|
`px-3 py-1 text-sm rounded-md border border-gray-700 ${
|
||||||
|
selected ? "bg-gray-700 text-white" : ""
|
||||||
|
}`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</Tab.List>
|
||||||
|
<Tab.Panels>
|
||||||
|
<Tab.Panel>
|
||||||
|
<IssueCommentSection
|
||||||
|
comments={issueComments}
|
||||||
|
workspaceSlug={activeWorkspace?.slug as string}
|
||||||
|
projectId={projectId as string}
|
||||||
|
issueId={issueId as string}
|
||||||
|
/>
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel>
|
||||||
|
<IssueActivitySection issueActivities={issueActivities} states={states} />
|
||||||
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="sticky top-0 h-min bg-secondary p-4 rounded-lg">
|
||||||
|
<IssueDetailSidebar
|
||||||
|
control={control}
|
||||||
|
issueDetail={issueDetail}
|
||||||
|
submitChanges={submitChanges}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full grid place-items-center px-4 sm:px-0">
|
||||||
|
<Spinner />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ProjectLayout>
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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,182 +136,179 @@ 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 ?? "Project"} Issues`} />
|
||||||
<BreadcrumbItem title={`${activeProject?.name} Issues`} />
|
</Breadcrumbs>
|
||||||
</Breadcrumbs>
|
<div className="flex items-center justify-between w-full">
|
||||||
<div className="flex items-center justify-between cursor-pointer 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">
|
<button
|
||||||
<button
|
type="button"
|
||||||
type="button"
|
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none ${
|
||||||
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none ${
|
issueView === "list" ? "bg-gray-200" : ""
|
||||||
issueView === "list" ? "bg-gray-200" : ""
|
}`}
|
||||||
}`}
|
onClick={() => {
|
||||||
onClick={() => {
|
setIssueView("list");
|
||||||
setIssueView("list");
|
setGroupByProperty(null);
|
||||||
setGroupByProperty(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ListBulletIcon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none ${
|
|
||||||
issueView === "kanban" ? "bg-gray-200" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
setIssueView("kanban");
|
|
||||||
setGroupByProperty("state_detail.name");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Squares2X2Icon className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<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
|
|
||||||
Icon={PlusIcon}
|
|
||||||
label="Add Issue"
|
|
||||||
action={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", {
|
|
||||||
key: "i",
|
|
||||||
ctrlKey: true,
|
|
||||||
});
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
}}
|
||||||
/>
|
>
|
||||||
|
<ListBulletIcon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none ${
|
||||||
|
issueView === "kanban" ? "bg-gray-200" : ""
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
setIssueView("kanban");
|
||||||
|
setGroupByProperty("state_detail.name");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Squares2X2Icon className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<Menu as="div" className="relative inline-block w-40">
|
||||||
<div className="h-full">
|
<div className="w-full">
|
||||||
{issueView === "list" ? (
|
<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">
|
||||||
<ListView
|
<span className="flex gap-x-1 items-center">
|
||||||
properties={properties}
|
{groupByOptions.find((option) => option.key === groupByProperty)?.name ??
|
||||||
groupedByIssues={groupedByIssues}
|
"No Grouping"}
|
||||||
selectedGroup={groupByProperty}
|
</span>
|
||||||
setSelectedIssue={setSelectedIssue}
|
<div className="flex-grow flex justify-end">
|
||||||
handleDeleteIssue={setDeleteIssue}
|
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
||||||
/>
|
</div>
|
||||||
) : (
|
</Menu.Button>
|
||||||
<BoardView
|
</div>
|
||||||
properties={properties}
|
|
||||||
selectedGroup={groupByProperty}
|
<Transition
|
||||||
groupedByIssues={groupedByIssues}
|
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
|
||||||
|
Icon={PlusIcon}
|
||||||
|
label="Add Issue"
|
||||||
|
onClick={() => {
|
||||||
|
const e = new KeyboardEvent("keydown", {
|
||||||
|
key: "i",
|
||||||
|
ctrlKey: true,
|
||||||
|
});
|
||||||
|
document.dispatchEvent(e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{issueView === "list" ? (
|
||||||
|
<ListView
|
||||||
|
properties={properties}
|
||||||
|
groupedByIssues={groupedByIssues}
|
||||||
|
selectedGroup={groupByProperty}
|
||||||
|
setSelectedIssue={setSelectedIssue}
|
||||||
|
handleDeleteIssue={setDeleteIssue}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<BoardView
|
||||||
|
properties={properties}
|
||||||
|
selectedGroup={groupByProperty}
|
||||||
|
groupedByIssues={groupedByIssues}
|
||||||
|
members={members}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</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 +329,7 @@ const ProjectIssues: NextPage = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</ProjectLayout>
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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,134 +69,128 @@ 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="h-full w-full grid place-items-center px-4 sm:px-0">
|
||||||
<div className="w-full h-full flex justify-center items-center">
|
<Spinner />
|
||||||
<Spinner />
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full space-y-5">
|
||||||
|
<Breadcrumbs>
|
||||||
|
<BreadcrumbItem title="Projects" link="/projects" />
|
||||||
|
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Members`} />
|
||||||
|
</Breadcrumbs>
|
||||||
|
<div className="flex items-center justify-between cursor-pointer w-full">
|
||||||
|
<h2 className="text-2xl font-medium">Invite Members</h2>
|
||||||
|
<HeaderButton Icon={PlusIcon} label="Add Member" onClick={() => setIsOpen(true)} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
{members && members.length === 0 ? null : (
|
||||||
<div className="flex flex-col items-center justify-center w-full h-full px-2">
|
<table className="min-w-full table-fixed border border-gray-300 md:rounded-lg divide-y divide-gray-300">
|
||||||
<div className="w-full h-full flex flex-col space-y-5 pb-10 overflow-auto">
|
<thead className="bg-gray-50">
|
||||||
<Breadcrumbs>
|
<tr>
|
||||||
<BreadcrumbItem title="Projects" link="/projects" />
|
<th
|
||||||
<BreadcrumbItem title={`${activeProject?.name} Members`} />
|
scope="col"
|
||||||
</Breadcrumbs>
|
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"
|
||||||
<div className="flex items-center justify-between cursor-pointer w-full">
|
>
|
||||||
<h2 className="text-2xl font-medium">Invite Members</h2>
|
Name
|
||||||
<HeaderButton Icon={PlusIcon} label="Add Member" action={() => setIsOpen(true)} />
|
</th>
|
||||||
</div>
|
<th
|
||||||
{members && members.length === 0 ? null : (
|
scope="col"
|
||||||
<>
|
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"
|
||||||
<table className="min-w-full table-fixed border border-gray-300 md:rounded-lg divide-y divide-gray-300">
|
>
|
||||||
<thead className="bg-gray-50">
|
Role
|
||||||
<tr>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6"
|
||||||
>
|
>
|
||||||
Name
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6 w-10">
|
||||||
scope="col"
|
<span className="sr-only">Edit</span>
|
||||||
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"
|
</th>
|
||||||
>
|
</tr>
|
||||||
Role
|
</thead>
|
||||||
</th>
|
<tbody className="divide-y divide-gray-200 bg-white">
|
||||||
<th
|
{members?.map((member: any) => (
|
||||||
scope="col"
|
<tr key={member.id}>
|
||||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6"
|
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
||||||
>
|
{member.email ?? "No email has been added."}
|
||||||
Status
|
</td>
|
||||||
</th>
|
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
||||||
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6 w-10">
|
{ROLE[member.role as keyof typeof ROLE] ?? "None"}
|
||||||
<span className="sr-only">Edit</span>
|
</td>
|
||||||
</th>
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6">
|
||||||
</tr>
|
{member?.member ? (
|
||||||
</thead>
|
"Member"
|
||||||
<tbody className="divide-y divide-gray-200 bg-white">
|
) : member.status ? (
|
||||||
{members?.map((member: any) => (
|
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
||||||
<tr key={member.id}>
|
Accepted
|
||||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
</span>
|
||||||
{member.email ?? "No email has been added."}
|
) : (
|
||||||
</td>
|
<span className="p-0.5 px-2 text-sm bg-yellow-400 text-black rounded-full">
|
||||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
Pending
|
||||||
{ROLE[member.role as keyof typeof ROLE] ?? "None"}
|
</span>
|
||||||
</td>
|
)}
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6">
|
</td>
|
||||||
{member?.member ? (
|
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
||||||
"Member"
|
<Menu>
|
||||||
) : member.status ? (
|
<Menu.Button>
|
||||||
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
<EllipsisHorizontalIcon
|
||||||
Accepted
|
width="16"
|
||||||
</span>
|
height="16"
|
||||||
) : (
|
className="inline text-gray-500"
|
||||||
<span className="p-0.5 px-2 text-sm bg-yellow-400 text-black rounded-full">
|
/>
|
||||||
Pending
|
</Menu.Button>
|
||||||
</span>
|
<Menu.Items className="absolute z-50 w-28 bg-white rounded border cursor-pointer -left-20 top-9">
|
||||||
)}
|
<Menu.Item>
|
||||||
</td>
|
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||||
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
<button
|
||||||
<Menu>
|
className="w-full text-left py-2 pl-2"
|
||||||
<Menu.Button>
|
type="button"
|
||||||
<EllipsisHorizontalIcon
|
onClick={() => {}}
|
||||||
width="16"
|
>
|
||||||
height="16"
|
Edit
|
||||||
className="inline text-gray-500"
|
</button>
|
||||||
/>
|
</div>
|
||||||
</Menu.Button>
|
</Menu.Item>
|
||||||
<Menu.Items className="absolute z-50 w-28 bg-white rounded border cursor-pointer -left-20 top-9">
|
<Menu.Item>
|
||||||
<Menu.Item>
|
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
<button
|
||||||
<button
|
className="w-full text-left py-2 pl-2"
|
||||||
className="w-full text-left py-2 pl-2"
|
type="button"
|
||||||
type="button"
|
onClick={async () => {
|
||||||
onClick={() => {}}
|
member.member
|
||||||
>
|
? (await projectService.deleteProjectMember(
|
||||||
Edit
|
activeWorkspace?.slug as string,
|
||||||
</button>
|
projectId as any,
|
||||||
</div>
|
member.id
|
||||||
</Menu.Item>
|
),
|
||||||
<Menu.Item>
|
await mutateMembers())
|
||||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
: (await projectService.deleteProjectInvitation(
|
||||||
<button
|
activeWorkspace?.slug as string,
|
||||||
className="w-full text-left py-2 pl-2"
|
projectId as any,
|
||||||
type="button"
|
member.id
|
||||||
onClick={async () => {
|
),
|
||||||
member.member
|
await mutateInvitations());
|
||||||
? (await projectService.deleteProjectMember(
|
}}
|
||||||
activeWorkspace?.slug as string,
|
>
|
||||||
projectId as any,
|
Remove
|
||||||
member.id
|
</button>
|
||||||
),
|
</div>
|
||||||
await mutateMembers())
|
</Menu.Item>
|
||||||
: (await projectService.deleteProjectInvitation(
|
</Menu.Items>
|
||||||
activeWorkspace?.slug as string,
|
</Menu>
|
||||||
projectId as any,
|
</td>
|
||||||
member.id
|
</tr>
|
||||||
),
|
))}
|
||||||
await mutateInvitations());
|
</tbody>
|
||||||
}}
|
</table>
|
||||||
>
|
)}
|
||||||
Remove
|
</div>
|
||||||
</button>
|
)}
|
||||||
</div>
|
</AdminLayout>
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Items>
|
|
||||||
</Menu>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ProjectLayout>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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";
|
||||||
@ -132,8 +132,8 @@ const ProjectSettings: NextPage = () => {
|
|||||||
const checkIdentifierAvailability = useCallback(debounce(checkIdentifier, 1500), []);
|
const checkIdentifierAvailability = useCallback(debounce(checkIdentifier, 1500), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ProjectLayout>
|
<AdminLayout>
|
||||||
<div className="w-full h-full space-y-5">
|
<div className="space-y-5">
|
||||||
<CreateUpdateStateModal
|
<CreateUpdateStateModal
|
||||||
isOpen={isCreateStateModalOpen}
|
isOpen={isCreateStateModalOpen}
|
||||||
setIsOpen={setIsCreateStateModalOpen}
|
setIsOpen={setIsCreateStateModalOpen}
|
||||||
@ -143,7 +143,7 @@ const ProjectSettings: NextPage = () => {
|
|||||||
<BreadcrumbItem title="Projects" link="/projects" />
|
<BreadcrumbItem title="Projects" link="/projects" />
|
||||||
<BreadcrumbItem title={`${activeProject?.name} Settings`} />
|
<BreadcrumbItem title={`${activeProject?.name} Settings`} />
|
||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<div className="w-full h-full flex flex-col space-y-3">
|
<div className="space-y-3">
|
||||||
{projectDetails ? (
|
{projectDetails ? (
|
||||||
<div>
|
<div>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="mt-3">
|
<form onSubmit={handleSubmit(onSubmit)} className="mt-3">
|
||||||
@ -437,7 +437,7 @@ const ProjectSettings: NextPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ProjectLayout>
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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,74 +70,62 @@ const Projects: NextPage = () => {
|
|||||||
/>
|
/>
|
||||||
{projects ? (
|
{projects ? (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col items-center justify-center w-full h-full px-2">
|
{projects.length === 0 ? (
|
||||||
<div className="w-full h-full flex flex-col space-y-5 pb-10">
|
<div className="h-full w-full grid place-items-center px-4 sm:px-0">
|
||||||
{projects.length === 0 ? (
|
<EmptySpace
|
||||||
<div className="w-full h-full flex flex-col justify-center items-center px-4">
|
title="You don't have any project yet."
|
||||||
<EmptySpace
|
description="Projects are a collection of issues. They can be used to represent the development work for a product, project, or service."
|
||||||
title="You don't have any project yet."
|
Icon={ClipboardDocumentListIcon}
|
||||||
description="Projects are a collection of issues. They can be used to represent the development work for a product, project, or service."
|
>
|
||||||
Icon={ClipboardDocumentListIcon}
|
<EmptySpaceItem
|
||||||
>
|
title="Create a new project"
|
||||||
<EmptySpaceItem
|
description={
|
||||||
title="Create a new project"
|
<span>
|
||||||
description={
|
Use{" "}
|
||||||
<span>
|
<pre className="inline bg-gray-100 px-2 py-1 rounded">Ctrl/Command + P</pre>{" "}
|
||||||
Use{" "}
|
shortcut to create a new project
|
||||||
<pre className="inline bg-gray-100 px-2 py-1 rounded">
|
</span>
|
||||||
Ctrl/Command + P
|
}
|
||||||
</pre>{" "}
|
Icon={PlusIcon}
|
||||||
shortcut to create a new project
|
action={() => setIsOpen(true)}
|
||||||
</span>
|
/>
|
||||||
}
|
</EmptySpace>
|
||||||
Icon={PlusIcon}
|
</div>
|
||||||
action={() => setIsOpen(true)}
|
) : (
|
||||||
/>
|
<div className="h-full w-full space-y-5">
|
||||||
</EmptySpace>
|
<Breadcrumbs>
|
||||||
|
<BreadcrumbItem title={`${activeWorkspace?.name ?? "Workspace"} Projects`} />
|
||||||
|
</Breadcrumbs>
|
||||||
|
<div className="flex items-center justify-between cursor-pointer w-full">
|
||||||
|
<h2 className="text-2xl font-medium">Projects</h2>
|
||||||
|
<HeaderButton Icon={PlusIcon} label="Add Project" onClick={() => setIsOpen(true)} />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{projects.map((item) => (
|
||||||
|
<ProjectMemberInvitations
|
||||||
|
key={item.id}
|
||||||
|
project={item}
|
||||||
|
slug={(activeWorkspace as any).slug}
|
||||||
|
invitationsRespond={invitationsRespond}
|
||||||
|
handleInvitation={handleInvitation}
|
||||||
|
setDeleteProject={setDeleteProject}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{invitationsRespond.length > 0 && (
|
||||||
|
<div className="flex justify-between mt-4">
|
||||||
|
<Button onClick={submitInvitations}>Submit</Button>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Breadcrumbs>
|
|
||||||
<BreadcrumbItem title={`${activeWorkspace?.name} Projects`} />
|
|
||||||
</Breadcrumbs>
|
|
||||||
<div className="flex items-center justify-between cursor-pointer w-full">
|
|
||||||
<h2 className="text-2xl font-medium">Projects</h2>
|
|
||||||
<HeaderButton
|
|
||||||
Icon={PlusIcon}
|
|
||||||
label="Add Project"
|
|
||||||
action={() => setIsOpen(true)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{projects.map((item) => (
|
|
||||||
<ProjectMemberInvitations
|
|
||||||
key={item.id}
|
|
||||||
project={item}
|
|
||||||
slug={(activeWorkspace as any).slug}
|
|
||||||
invitationsRespond={invitationsRespond}
|
|
||||||
handleInvitation={handleInvitation}
|
|
||||||
setDeleteProject={setDeleteProject}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{invitationsRespond.length > 0 && (
|
|
||||||
<div className="flex justify-between mt-4">
|
|
||||||
<Button onClick={submitInvitations}>Submit</Button>
|
|
||||||
</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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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">
|
||||||
<div className="font-medium text-2xl">Good Morning, {user.first_name}!!</div>
|
Good{" "}
|
||||||
) : (
|
{hours >= 4 && hours < 12
|
||||||
<div className="animate-pulse" role="status">
|
? "Morning"
|
||||||
<div className="font-semibold text-2xl h-8 bg-gray-200 rounded dark:bg-gray-700 w-60"></div>
|
: hours >= 12 && hours < 17
|
||||||
</div>
|
? "Afternoon"
|
||||||
)}
|
: "Evening"}
|
||||||
</div>
|
, {user.first_name}!!
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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,134 +76,134 @@ 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 ?? "Workspace"} Members`} />
|
||||||
<BreadcrumbItem title={`${activeWorkspace?.name} 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" onClick={() => setIsOpen(true)} />
|
||||||
<HeaderButton Icon={PlusIcon} label="Add Member" action={() => setIsOpen(true)} />
|
|
||||||
</div>
|
|
||||||
{members && members.length === 0 ? null : (
|
|
||||||
<>
|
|
||||||
<table className="min-w-full table-fixed border border-gray-300 md:rounded-lg divide-y divide-gray-300">
|
|
||||||
<thead className="bg-gray-50">
|
|
||||||
<tr>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"
|
|
||||||
>
|
|
||||||
Role
|
|
||||||
</th>
|
|
||||||
<th
|
|
||||||
scope="col"
|
|
||||||
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6"
|
|
||||||
>
|
|
||||||
Status
|
|
||||||
</th>
|
|
||||||
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6 w-10">
|
|
||||||
<span className="sr-only">Edit</span>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody className="divide-y divide-gray-200 bg-white">
|
|
||||||
{members?.map((member: any) => (
|
|
||||||
<tr key={member.id}>
|
|
||||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
|
||||||
{member.email ?? "No email has been added."}
|
|
||||||
</td>
|
|
||||||
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
|
||||||
{ROLE[member.role as keyof typeof ROLE] ?? "None"}
|
|
||||||
</td>
|
|
||||||
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6">
|
|
||||||
{member?.member ? (
|
|
||||||
"Accepted"
|
|
||||||
) : member.status ? (
|
|
||||||
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
|
||||||
Accepted
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="p-0.5 px-2 text-sm bg-yellow-400 text-black rounded-full">
|
|
||||||
Pending
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
|
||||||
<Menu>
|
|
||||||
<Menu.Button>
|
|
||||||
<EllipsisHorizontalIcon
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
className="inline text-gray-500"
|
|
||||||
/>
|
|
||||||
</Menu.Button>
|
|
||||||
<Menu.Items className="absolute z-50 w-28 bg-white rounded border cursor-pointer -left-20 top-9">
|
|
||||||
<Menu.Item>
|
|
||||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
|
||||||
<button
|
|
||||||
className="w-full text-left py-2 pl-2"
|
|
||||||
type="button"
|
|
||||||
onClick={() => {}}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item>
|
|
||||||
<div className="hover:bg-gray-100 border-b last:border-0">
|
|
||||||
<button
|
|
||||||
className="w-full text-left py-2 pl-2"
|
|
||||||
type="button"
|
|
||||||
onClick={async () => {
|
|
||||||
member.member
|
|
||||||
? (await workspaceService.deleteWorkspaceMember(
|
|
||||||
activeWorkspace?.slug as string,
|
|
||||||
member.id
|
|
||||||
),
|
|
||||||
await mutateMembers((prevData) => [
|
|
||||||
...(prevData ?? [])?.filter(
|
|
||||||
(m: any) => m.id !== member.id
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
false)
|
|
||||||
: (await workspaceService.deleteWorkspaceInvitations(
|
|
||||||
activeWorkspace?.slug as string,
|
|
||||||
member.id
|
|
||||||
),
|
|
||||||
await mutateInvitations((prevData) => [
|
|
||||||
...(prevData ?? []).filter((m) => m.id !== member.id),
|
|
||||||
false,
|
|
||||||
]));
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.Items>
|
|
||||||
</Menu>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{members && members.length === 0 ? null : (
|
||||||
|
<>
|
||||||
|
<table className="min-w-full table-fixed border border-gray-300 md:rounded-lg divide-y divide-gray-300">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"
|
||||||
|
>
|
||||||
|
Name
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-gray-900 sm:pl-6"
|
||||||
|
>
|
||||||
|
Role
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
className="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 sm:pl-6"
|
||||||
|
>
|
||||||
|
Status
|
||||||
|
</th>
|
||||||
|
<th scope="col" className="relative py-3.5 pl-3 pr-4 sm:pr-6 w-10">
|
||||||
|
<span className="sr-only">Edit</span>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="divide-y divide-gray-200 bg-white">
|
||||||
|
{members?.map((member: any) => (
|
||||||
|
<tr key={member.id}>
|
||||||
|
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
||||||
|
{member.email ?? "No email has been added."}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-gray-900 sm:pl-6">
|
||||||
|
{ROLE[member.role as keyof typeof ROLE] ?? "None"}
|
||||||
|
</td>
|
||||||
|
<td className="whitespace-nowrap px-3 py-4 text-sm text-gray-500 sm:pl-6">
|
||||||
|
{member?.member ? (
|
||||||
|
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
||||||
|
Accepted
|
||||||
|
</span>
|
||||||
|
) : member.status ? (
|
||||||
|
<span className="p-0.5 px-2 text-sm bg-green-700 text-white rounded-full">
|
||||||
|
Accepted
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="p-0.5 px-2 text-sm bg-yellow-400 text-black rounded-full">
|
||||||
|
Pending
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="relative whitespace-nowrap py-4 pl-3 pr-4 text-right text-sm font-medium sm:pr-6">
|
||||||
|
<Menu>
|
||||||
|
<Menu.Button>
|
||||||
|
<EllipsisHorizontalIcon
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
className="inline text-gray-500"
|
||||||
|
/>
|
||||||
|
</Menu.Button>
|
||||||
|
<Menu.Items className="absolute z-50 w-28 bg-white rounded border cursor-pointer -left-20 top-9">
|
||||||
|
<Menu.Item>
|
||||||
|
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||||
|
<button
|
||||||
|
className="w-full text-left py-2 pl-2"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {}}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
<Menu.Item>
|
||||||
|
<div className="hover:bg-gray-100 border-b last:border-0">
|
||||||
|
<button
|
||||||
|
className="w-full text-left py-2 pl-2"
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
member.member
|
||||||
|
? (await workspaceService.deleteWorkspaceMember(
|
||||||
|
activeWorkspace?.slug as string,
|
||||||
|
member.id
|
||||||
|
),
|
||||||
|
await mutateMembers((prevData) => [
|
||||||
|
...(prevData ?? [])?.filter(
|
||||||
|
(m: any) => m.id !== member.id
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
false)
|
||||||
|
: (await workspaceService.deleteWorkspaceInvitations(
|
||||||
|
activeWorkspace?.slug as string,
|
||||||
|
member.id
|
||||||
|
),
|
||||||
|
await mutateInvitations((prevData) => [
|
||||||
|
...(prevData ?? []).filter((m) => m.id !== member.id),
|
||||||
|
false,
|
||||||
|
]));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
</Menu.Items>
|
||||||
|
</Menu>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</ProjectLayout>
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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: "",
|
||||||
@ -82,147 +83,152 @@ 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">
|
<Tab.Group>
|
||||||
<section className="space-y-5">
|
<Tab.List className="flex items-center gap-3">
|
||||||
<div>
|
{["General", "Actions"].map((tab, index) => (
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900">General</h3>
|
<Tab
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
key={index}
|
||||||
This information will be displayed to every member of the workspace.
|
className={({ selected }) =>
|
||||||
</p>
|
`text-md leading-6 text-gray-900 px-4 py-1 rounded outline-none ${
|
||||||
</div>
|
selected ? "bg-gray-700 text-white" : "hover:bg-gray-200"
|
||||||
<div className="grid grid-cols-2 gap-6">
|
} duration-300`
|
||||||
<div className="w-full space-y-3">
|
}
|
||||||
<Dropzone
|
>
|
||||||
multiple={false}
|
{tab}
|
||||||
accept={{
|
</Tab>
|
||||||
"image/*": [],
|
))}
|
||||||
}}
|
</Tab.List>
|
||||||
onDrop={(files) => {
|
<Tab.Panels>
|
||||||
setImage(files[0]);
|
<Tab.Panel>
|
||||||
}}
|
<div className="grid grid-cols-2 gap-6">
|
||||||
>
|
<div className="w-full space-y-3">
|
||||||
{({ getRootProps, getInputProps }) => (
|
<Dropzone
|
||||||
<div>
|
multiple={false}
|
||||||
<input {...getInputProps()} />
|
accept={{
|
||||||
<div className="text-gray-500 mb-2">Logo</div>
|
"image/*": [],
|
||||||
<div>
|
}}
|
||||||
<div className="h-60 bg-blue-50" {...getRootProps()}>
|
onDrop={(files) => {
|
||||||
{((watch("logo") && watch("logo") !== null && watch("logo") !== "") ||
|
setImage(files[0]);
|
||||||
(image && image !== null)) && (
|
|
||||||
<div className="relative flex mx-auto h-60">
|
|
||||||
<Image
|
|
||||||
src={image ? URL.createObjectURL(image) : watch("logo") ?? ""}
|
|
||||||
alt="Workspace Logo"
|
|
||||||
objectFit="cover"
|
|
||||||
layout="fill"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-sm text-gray-500 mt-2">
|
|
||||||
Max file size is 500kb. Supported file types are .jpg and .png.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Dropzone>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
if (image === null) return;
|
|
||||||
setIsImageUploading(true);
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("asset", image);
|
|
||||||
formData.append("attributes", JSON.stringify({}));
|
|
||||||
fileServices
|
|
||||||
.uploadFile(formData)
|
|
||||||
.then((response) => {
|
|
||||||
const imageUrl = response.asset;
|
|
||||||
setValue("logo", imageUrl);
|
|
||||||
handleSubmit(onSubmit)();
|
|
||||||
setIsImageUploading(false);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setIsImageUploading(false);
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isImageUploading ? "Uploading..." : "Upload"}
|
{({ getRootProps, getInputProps }) => (
|
||||||
</Button>
|
<div>
|
||||||
|
<input {...getInputProps()} />
|
||||||
|
<div className="text-gray-500 mb-2">Logo</div>
|
||||||
|
<div>
|
||||||
|
<div className="h-60 bg-blue-50" {...getRootProps()}>
|
||||||
|
{((watch("logo") &&
|
||||||
|
watch("logo") !== null &&
|
||||||
|
watch("logo") !== "") ||
|
||||||
|
(image && image !== null)) && (
|
||||||
|
<div className="relative flex mx-auto h-60">
|
||||||
|
<Image
|
||||||
|
src={image ? URL.createObjectURL(image) : watch("logo") ?? ""}
|
||||||
|
alt="Workspace Logo"
|
||||||
|
objectFit="cover"
|
||||||
|
layout="fill"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-500 mt-2">
|
||||||
|
Max file size is 500kb. Supported file types are .jpg and .png.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Dropzone>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (image === null) return;
|
||||||
|
setIsImageUploading(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("asset", image);
|
||||||
|
formData.append("attributes", JSON.stringify({}));
|
||||||
|
fileServices
|
||||||
|
.uploadFile(formData)
|
||||||
|
.then((response) => {
|
||||||
|
const imageUrl = response.asset;
|
||||||
|
setValue("logo", imageUrl);
|
||||||
|
handleSubmit(onSubmit)();
|
||||||
|
setIsImageUploading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setIsImageUploading(false);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isImageUploading ? "Uploading..." : "Upload"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<Input
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
label="Name"
|
||||||
|
placeholder="Name"
|
||||||
|
autoComplete="off"
|
||||||
|
register={register}
|
||||||
|
error={errors.name}
|
||||||
|
validations={{
|
||||||
|
required: "Name is required",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
id="company_size"
|
||||||
|
name="company_size"
|
||||||
|
label="How large is your company?"
|
||||||
|
options={[
|
||||||
|
{ value: 5, label: "5" },
|
||||||
|
{ value: 10, label: "10" },
|
||||||
|
{ value: 25, label: "25" },
|
||||||
|
{ value: 50, label: "50" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<Button onClick={handleSubmit(onSubmit)} disabled={isSubmitting}>
|
||||||
|
{isSubmitting ? "Updating..." : "Update"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-3">
|
</Tab.Panel>
|
||||||
<div>
|
<Tab.Panel>
|
||||||
<Input
|
<div>
|
||||||
id="name"
|
<Button theme="danger" onClick={() => setIsOpen(true)}>
|
||||||
name="name"
|
Delete the workspace
|
||||||
label="Name"
|
</Button>
|
||||||
placeholder="Name"
|
|
||||||
autoComplete="off"
|
|
||||||
register={register}
|
|
||||||
error={errors.name}
|
|
||||||
validations={{
|
|
||||||
required: "Name is required",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Select
|
|
||||||
id="company_size"
|
|
||||||
name="company_size"
|
|
||||||
label="How large is your company?"
|
|
||||||
options={[
|
|
||||||
{ value: 5, label: "5" },
|
|
||||||
{ value: 10, label: "10" },
|
|
||||||
{ value: 25, label: "25" },
|
|
||||||
{ value: 50, label: "50" },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="text-right">
|
|
||||||
<Button onClick={handleSubmit(onSubmit)} disabled={isSubmitting}>
|
|
||||||
{isSubmitting ? "Updating..." : "Update"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Tab.Panel>
|
||||||
</section>
|
</Tab.Panels>
|
||||||
<section className="space-y-5">
|
</Tab.Group>
|
||||||
<div>
|
</div>
|
||||||
<h3 className="text-lg font-medium leading-6 text-gray-900">Actions</h3>
|
) : (
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<div className="h-full w-full grid place-items-center px-4 sm:px-0">
|
||||||
Once deleted, it will be gone forever. Please be certain.
|
<Spinner />
|
||||||
</p>
|
</div>
|
||||||
</div>
|
)}
|
||||||
<div>
|
|
||||||
<Button theme="danger" onClick={() => setIsOpen(true)}>
|
|
||||||
Delete the workspace
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="w-full h-full flex justify-center items-center">
|
|
||||||
<Spinner />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</ProjectLayout>
|
</AdminLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
1
public/animated-icons/uploading.json
Normal file
1
public/animated-icons/uploading.json
Normal file
File diff suppressed because one or more lines are too long
@ -5,6 +5,8 @@ module.exports = {
|
|||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
theme: "#4338ca",
|
theme: "#4338ca",
|
||||||
|
primary: "#f9fafb", // gray-50
|
||||||
|
secondary: "white",
|
||||||
},
|
},
|
||||||
keyframes: {
|
keyframes: {
|
||||||
leftToaster: {
|
leftToaster: {
|
||||||
|
2
types/workspace.d.ts
vendored
2
types/workspace.d.ts
vendored
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
)}
|
)}
|
||||||
|
@ -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}>
|
||||||
|
@ -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}
|
||||||
|
@ -7,3 +7,5 @@ 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 { default as HeaderButton } from "./HeaderButton";
|
export { default as HeaderButton } from "./HeaderButton";
|
||||||
|
export * from "./Breadcrumbs";
|
||||||
|
export * from "./EmptySpace";
|
||||||
|
Loading…
Reference in New Issue
Block a user