forked from github/plane
style: issue details, list view, kanban card
This commit is contained in:
parent
1368fb9164
commit
b1aa389930
@ -278,15 +278,15 @@ const CommandPalette: React.FC = () => {
|
||||
value={issue.id}
|
||||
/>
|
||||
<span
|
||||
className={`h-1.5 w-1.5 block rounded-full`}
|
||||
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">
|
||||
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||
{activeProject?.identifier}-{issue.sequence_id}
|
||||
</span>
|
||||
{issue.name}
|
||||
<span>{issue.name}</span>
|
||||
</div>
|
||||
{active && (
|
||||
<button
|
||||
@ -297,10 +297,9 @@ const CommandPalette: React.FC = () => {
|
||||
);
|
||||
handleCommandPaletteClose();
|
||||
}}
|
||||
className="flex-shrink-0 text-gray-500"
|
||||
>
|
||||
<span className="justify-self-end flex-none text-gray-500">
|
||||
Jump to...
|
||||
</span>
|
||||
Jump to...
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
|
@ -44,7 +44,7 @@ const IsGuestCondition: React.FC<{
|
||||
type: "error",
|
||||
message: "You don't have permission to create project.",
|
||||
});
|
||||
}, []);
|
||||
}, [setIsOpen, setToastAlert]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
@ -18,11 +18,10 @@ import {
|
||||
ArrowsPointingOutIcon,
|
||||
CalendarDaysIcon,
|
||||
EllipsisHorizontalIcon,
|
||||
PencilIcon,
|
||||
PlusIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import Image from "next/image";
|
||||
import { divide } from "lodash";
|
||||
import { getPriorityIcon } from "constants/global";
|
||||
|
||||
type Props = {
|
||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||
@ -181,133 +180,116 @@ const SingleBoard: React.FC<Props> = ({
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
>
|
||||
<div
|
||||
className="px-2 py-3 space-y-1.5 select-none"
|
||||
{...provided.dragHandleProps}
|
||||
>
|
||||
<span className="group-hover:text-theme text-sm break-all">
|
||||
<div className="p-2 select-none" {...provided.dragHandleProps}>
|
||||
{properties.key && (
|
||||
<div className="text-xs font-medium text-gray-500 mb-2">
|
||||
{childIssue.project_detail?.identifier}-{childIssue.sequence_id}
|
||||
</div>
|
||||
)}
|
||||
<h5 className="group-hover:text-theme text-sm break-all mb-3">
|
||||
{childIssue.name}
|
||||
</span>
|
||||
{Object.keys(properties).map(
|
||||
(key) =>
|
||||
properties[key as keyof Properties] &&
|
||||
!Array.isArray(childIssue[key as keyof IIssue]) && (
|
||||
<div
|
||||
key={key}
|
||||
className={`${
|
||||
key === "name"
|
||||
? "text-sm mb-2"
|
||||
: key === "description"
|
||||
? "text-xs text-black"
|
||||
: key === "priority"
|
||||
? `text-xs bg-gray-200 px-2 py-1 mt-2 flex items-center gap-x-1 rounded w-min whitespace-nowrap capitalize font-medium ${
|
||||
childIssue.priority === "urgent"
|
||||
? "bg-red-100 text-red-600"
|
||||
: childIssue.priority === "high"
|
||||
? "bg-orange-100 text-orange-600"
|
||||
: childIssue.priority === "medium"
|
||||
? "bg-yellow-100 text-yellow-500"
|
||||
: childIssue.priority === "low"
|
||||
? "bg-green-100 text-green-500"
|
||||
: "hidden"
|
||||
}`
|
||||
: key === "target_date"
|
||||
? "text-xs bg-indigo-50 px-2 py-1 mt-2 flex items-center gap-x-1 rounded w-min whitespace-nowrap"
|
||||
: "text-sm text-gray-500"
|
||||
} gap-1 relative
|
||||
`}
|
||||
>
|
||||
{key === "start_date" && childIssue.start_date !== null && (
|
||||
<span className="text-sm">
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{renderShortNumericDateFormat(childIssue.start_date)} -
|
||||
{childIssue.target_date
|
||||
? renderShortNumericDateFormat(childIssue.target_date)
|
||||
: "None"}
|
||||
</span>
|
||||
)}
|
||||
{key === "due_date" && (
|
||||
<>
|
||||
<span
|
||||
className={`flex items-center gap-x-1 group ${
|
||||
childIssue.target_date === null
|
||||
? ""
|
||||
: childIssue.target_date < new Date().toISOString()
|
||||
? "text-red-600"
|
||||
: findHowManyDaysLeft(childIssue.target_date) <=
|
||||
3 && "text-orange-400"
|
||||
</h5>
|
||||
<div className="flex items-center gap-x-1 gap-y-2 text-xs flex-wrap">
|
||||
{properties.priority && (
|
||||
<div
|
||||
className={`rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 capitalize ${
|
||||
childIssue.priority === "high"
|
||||
? "bg-red-100 text-red-600"
|
||||
: childIssue.priority === "medium"
|
||||
? "bg-orange-100 text-orange-500"
|
||||
: childIssue.priority === "low"
|
||||
? "bg-green-100 text-green-500"
|
||||
: "hidden"
|
||||
}`}
|
||||
>
|
||||
{/* {getPriorityIcon(childIssue.priority ?? "")} */}
|
||||
{childIssue.priority}
|
||||
</div>
|
||||
)}
|
||||
{properties.state && (
|
||||
<div className="flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<span
|
||||
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||
style={{ backgroundColor: childIssue.state_detail.color }}
|
||||
></span>
|
||||
{addSpaceIfCamelCase(childIssue.state_detail.name)}
|
||||
</div>
|
||||
)}
|
||||
{properties.start_date && (
|
||||
<div className="flex-shrink-0 flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{childIssue.start_date
|
||||
? renderShortNumericDateFormat(childIssue.start_date)
|
||||
: "N/A"}
|
||||
</div>
|
||||
)}
|
||||
{properties.target_date && (
|
||||
<div
|
||||
className={`flex-shrink-0 group flex items-center gap-1 hover:bg-gray-100 border rounded shadow-sm px-2 py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300 ${
|
||||
childIssue.target_date === null
|
||||
? ""
|
||||
: childIssue.target_date < new Date().toISOString()
|
||||
? "text-red-600"
|
||||
: findHowManyDaysLeft(childIssue.target_date) <= 3 &&
|
||||
"text-orange-400"
|
||||
}`}
|
||||
>
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{childIssue.target_date
|
||||
? renderShortNumericDateFormat(childIssue.target_date)
|
||||
: "N/A"}
|
||||
{childIssue.target_date && (
|
||||
<span className="absolute -top-full mb-2 left-4 border transition-opacity opacity-0 group-hover:opacity-100 bg-white rounded px-2 py-1">
|
||||
{childIssue.target_date < new Date().toISOString()
|
||||
? `Target date has passed by ${findHowManyDaysLeft(
|
||||
childIssue.target_date
|
||||
)} days`
|
||||
: findHowManyDaysLeft(childIssue.target_date) <= 3
|
||||
? `Target date is in ${findHowManyDaysLeft(
|
||||
childIssue.target_date
|
||||
)} days`
|
||||
: "Target date"}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{properties.assignee && (
|
||||
<div className="justify-end w-full flex items-center gap-1 text-xs">
|
||||
{childIssue?.assignee_details?.length > 0 ? (
|
||||
childIssue?.assignee_details?.map(
|
||||
(assignee, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`relative z-[1] h-5 w-5 rounded-full ${
|
||||
index !== 0 ? "-ml-2.5" : ""
|
||||
}`}
|
||||
>
|
||||
<CalendarDaysIcon className="h-4 w-4" />
|
||||
{childIssue.target_date
|
||||
? renderShortNumericDateFormat(childIssue.target_date)
|
||||
: "N/A"}
|
||||
{childIssue.target_date && (
|
||||
<span className="absolute -top-full mb-2 left-4 border transition-opacity opacity-0 group-hover:opacity-100 bg-white rounded px-2 py-1">
|
||||
{childIssue.target_date < new Date().toISOString()
|
||||
? `Due date has passed by ${findHowManyDaysLeft(
|
||||
childIssue.target_date
|
||||
)} days`
|
||||
: findHowManyDaysLeft(childIssue.target_date) <= 3
|
||||
? `Due date is in ${findHowManyDaysLeft(
|
||||
childIssue.target_date
|
||||
)} days`
|
||||
: "Due date"}
|
||||
</span>
|
||||
{assignee.avatar && assignee.avatar !== "" ? (
|
||||
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||
<Image
|
||||
src={assignee.avatar}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt={assignee.name}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
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)}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
{key === "key" && (
|
||||
<span className="text-xs">
|
||||
{childIssue.project_detail?.identifier}-
|
||||
{childIssue.sequence_id}
|
||||
</span>
|
||||
)}
|
||||
{key === "state" && (
|
||||
<>{addSpaceIfCamelCase(childIssue["state_detail"].name)}</>
|
||||
)}
|
||||
{key === "priority" && <>{childIssue.priority}</>}
|
||||
{/* {key === "description" && <>{childIssue.description}</>} */}
|
||||
{key === "assignee" ? (
|
||||
<div className="flex items-center gap-1 text-xs">
|
||||
{childIssue?.assignee_details?.length > 0 ? (
|
||||
childIssue?.assignee_details?.map(
|
||||
(assignee, index: number) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`relative z-[1] h-5 w-5 rounded-full ${
|
||||
index !== 0 ? "-ml-2.5" : ""
|
||||
}`}
|
||||
>
|
||||
{assignee.avatar && assignee.avatar !== "" ? (
|
||||
<div className="h-5 w-5 border-2 bg-white border-white rounded-full">
|
||||
<Image
|
||||
src={assignee.avatar}
|
||||
height="100%"
|
||||
width="100%"
|
||||
className="rounded-full"
|
||||
alt={assignee.name}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
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)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<span>No assignee.</span>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<span>No assignee.</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</Link>
|
||||
|
@ -14,7 +14,7 @@ type Props = {
|
||||
control: Control<IIssue, any>;
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
issues: IssueResponse | undefined;
|
||||
issues: IIssue[];
|
||||
};
|
||||
|
||||
const SelectParent: React.FC<Props> = ({ control, isOpen, setIsOpen, issues }) => {
|
||||
|
@ -392,16 +392,16 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
||||
/> */}
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
{/* <Input
|
||||
id="target_date"
|
||||
label="Due Date"
|
||||
label="Target Date"
|
||||
name="target_date"
|
||||
type="date"
|
||||
placeholder="Enter name"
|
||||
autoComplete="off"
|
||||
error={errors.target_date}
|
||||
register={register}
|
||||
/>
|
||||
/> */}
|
||||
</div>
|
||||
<div className="flex items-center flex-wrap gap-2">
|
||||
<SelectState control={control} setIsOpen={setIsStateModalOpen} />
|
||||
@ -409,11 +409,25 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
||||
<SelectPriority control={control} />
|
||||
<SelectAssignee control={control} />
|
||||
<SelectLabels control={control} />
|
||||
<Controller
|
||||
control={control}
|
||||
name="target_date"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<input
|
||||
type="date"
|
||||
value={value ?? ""}
|
||||
onChange={(e: any) => {
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
className="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-xs duration-300"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<SelectParent
|
||||
control={control}
|
||||
isOpen={parentIssueListModalOpen}
|
||||
setIsOpen={setParentIssueListModalOpen}
|
||||
issues={issues}
|
||||
issues={issues?.results ?? []}
|
||||
/>
|
||||
<Menu as="div" className="relative inline-block">
|
||||
<Menu.Button className="grid place-items-center p-1 hover:bg-gray-100 border rounded-md shadow-sm cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm duration-300">
|
||||
|
@ -2,35 +2,49 @@
|
||||
import React, { useState } from "react";
|
||||
// headless ui
|
||||
import { Combobox, Dialog, Transition } from "@headlessui/react";
|
||||
// ui
|
||||
import { Button } from "ui";
|
||||
// icons
|
||||
import { MagnifyingGlassIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssue, IssueResponse } from "types";
|
||||
import { IIssue } from "types";
|
||||
import { classNames } from "constants/common";
|
||||
import useUser from "lib/hooks/useUser";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
handleClose: () => void;
|
||||
value?: any;
|
||||
onChange: (...event: any[]) => void;
|
||||
issues: IssueResponse | undefined;
|
||||
issues: IIssue[];
|
||||
title?: string;
|
||||
multiple?: boolean;
|
||||
};
|
||||
|
||||
const IssuesListModal: React.FC<Props> = ({ isOpen, handleClose: onClose, onChange, issues }) => {
|
||||
const IssuesListModal: React.FC<Props> = ({
|
||||
isOpen,
|
||||
handleClose: onClose,
|
||||
value,
|
||||
onChange,
|
||||
issues,
|
||||
title = "Issues",
|
||||
multiple = false,
|
||||
}) => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [values, setValues] = useState<string[]>([]);
|
||||
|
||||
const { activeProject } = useUser();
|
||||
|
||||
const handleClose = () => {
|
||||
onClose();
|
||||
setQuery("");
|
||||
setValues([]);
|
||||
};
|
||||
|
||||
const filteredIssues: IIssue[] =
|
||||
query === ""
|
||||
? issues?.results ?? []
|
||||
: issues?.results.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ??
|
||||
[];
|
||||
? issues ?? []
|
||||
: issues?.filter((issue) => issue.name.toLowerCase().includes(query.toLowerCase())) ?? [];
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -59,7 +73,14 @@ const IssuesListModal: React.FC<Props> = ({ isOpen, handleClose: onClose, onChan
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="relative mx-auto max-w-2xl transform divide-y divide-gray-500 divide-opacity-10 rounded-xl bg-white bg-opacity-80 shadow-2xl ring-1 ring-black ring-opacity-5 backdrop-blur backdrop-filter transition-all">
|
||||
<Combobox onChange={onChange}>
|
||||
<Combobox
|
||||
value={value}
|
||||
onChange={(val) => {
|
||||
if (multiple) setValues(val);
|
||||
else onChange(val);
|
||||
}}
|
||||
// multiple={multiple}
|
||||
>
|
||||
<div className="relative m-1">
|
||||
<MagnifyingGlassIcon
|
||||
className="pointer-events-none absolute top-3.5 left-4 h-5 w-5 text-gray-900 text-opacity-40"
|
||||
@ -80,7 +101,7 @@ const IssuesListModal: React.FC<Props> = ({ isOpen, handleClose: onClose, onChan
|
||||
<li className="p-2">
|
||||
{query === "" && (
|
||||
<h2 className="mt-4 mb-2 px-3 text-xs font-semibold text-gray-900">
|
||||
Issues
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
<ul className="text-sm text-gray-700">
|
||||
@ -95,20 +116,26 @@ const IssuesListModal: React.FC<Props> = ({ isOpen, handleClose: onClose, onChan
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
// setIssueIdFromList(issue.id);
|
||||
handleClose();
|
||||
if (!multiple) handleClose();
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={`h-1.5 w-1.5 block rounded-full`}
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<span className="text-xs text-gray-500">
|
||||
{activeProject?.identifier}-{issue.sequence_id}
|
||||
</span>{" "}
|
||||
{issue.name}
|
||||
{({ selected }) => (
|
||||
<>
|
||||
{multiple ? (
|
||||
<input type="checkbox" checked={selected} readOnly />
|
||||
) : null}
|
||||
<span
|
||||
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
|
||||
style={{
|
||||
backgroundColor: issue.state_detail.color,
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-xs text-gray-500">
|
||||
{activeProject?.identifier}-{issue.sequence_id}
|
||||
</span>{" "}
|
||||
{issue.name}
|
||||
</>
|
||||
)}
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</ul>
|
||||
@ -128,6 +155,16 @@ const IssuesListModal: React.FC<Props> = ({ isOpen, handleClose: onClose, onChan
|
||||
</div>
|
||||
)}
|
||||
</Combobox>
|
||||
{multiple ? (
|
||||
<div className="flex justify-end items-center gap-2 p-3">
|
||||
<Button type="button" theme="danger" size="sm" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button" size="sm" onClick={() => onChange(values)}>
|
||||
Add to Cycle
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
// react
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
// next
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
@ -10,7 +10,7 @@ import { Listbox, Transition } from "@headlessui/react";
|
||||
// icons
|
||||
import { PencilIcon, TrashIcon } from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import { IIssue, IssueResponse, IState, NestedKeyOf, Properties, WorkspaceMember } from "types";
|
||||
import { IIssue, IssueResponse, NestedKeyOf, Properties, WorkspaceMember } from "types";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// fetch keys
|
||||
@ -20,13 +20,7 @@ import { PROJECT_ISSUES_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
import issuesServices from "lib/services/issues.services";
|
||||
import workspaceService from "lib/services/workspace.service";
|
||||
// constants
|
||||
import {
|
||||
addSpaceIfCamelCase,
|
||||
classNames,
|
||||
renderShortNumericDateFormat,
|
||||
replaceUnderscoreIfSnakeCase,
|
||||
} from "constants/common";
|
||||
import IssuePreviewModal from "../PreviewModal";
|
||||
import { addSpaceIfCamelCase, classNames, renderShortNumericDateFormat } from "constants/common";
|
||||
|
||||
// types
|
||||
type Props = {
|
||||
@ -44,9 +38,6 @@ const ListView: React.FC<Props> = ({
|
||||
setSelectedIssue,
|
||||
handleDeleteIssue,
|
||||
}) => {
|
||||
const [issuePreviewModal, setIssuePreviewModal] = useState(false);
|
||||
const [previewModalIssueId, setPreviewModalIssueId] = useState<string | null>(null);
|
||||
|
||||
const { activeWorkspace, activeProject, states } = useUser();
|
||||
|
||||
const partialUpdateIssue = (formData: Partial<IIssue>, issueId: string) => {
|
||||
@ -74,368 +65,348 @@ const ListView: React.FC<Props> = ({
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||
);
|
||||
|
||||
const handleHover = (issueId: string) => {
|
||||
document.addEventListener("keydown", (e) => {
|
||||
// if (e.code === "Space") {
|
||||
// e.preventDefault();
|
||||
// setPreviewModalIssueId(issueId);
|
||||
// setIssuePreviewModal(true);
|
||||
// }
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex flex-col">
|
||||
<IssuePreviewModal
|
||||
isOpen={issuePreviewModal}
|
||||
setIsOpen={setIssuePreviewModal}
|
||||
issueId={previewModalIssueId}
|
||||
/>
|
||||
<div className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full p-0.5 align-middle">
|
||||
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
||||
<table className="min-w-full">
|
||||
<thead className="bg-gray-100">
|
||||
<tr>
|
||||
<th
|
||||
scope="col"
|
||||
className="px-3 py-3.5 text-left uppercase text-sm font-semibold text-gray-900"
|
||||
>
|
||||
NAME
|
||||
</th>
|
||||
{Object.keys(properties).map(
|
||||
(key) =>
|
||||
properties[key as keyof Properties] && (
|
||||
<th
|
||||
key={key}
|
||||
scope="col"
|
||||
className="px-3 py-3.5 text-left uppercase text-sm font-semibold text-gray-900"
|
||||
>
|
||||
{replaceUnderscoreIfSnakeCase(key)}
|
||||
</th>
|
||||
)
|
||||
)}
|
||||
<th
|
||||
scope="col"
|
||||
className="px-3 py-3.5 text-right text-sm font-semibold text-gray-900"
|
||||
>
|
||||
ACTIONS
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white">
|
||||
{Object.keys(groupedByIssues).map((singleGroup) => (
|
||||
<React.Fragment key={singleGroup}>
|
||||
{selectedGroup !== null ? (
|
||||
<tr className="border-t border-gray-200">
|
||||
<th
|
||||
colSpan={14}
|
||||
scope="colgroup"
|
||||
className="bg-gray-50 px-4 py-2 text-left font-medium text-gray-900 capitalize"
|
||||
>
|
||||
<div className="mt-4 flex flex-col space-y-5">
|
||||
{Object.keys(groupedByIssues).map((singleGroup) => (
|
||||
<div key={singleGroup} className="overflow-x-auto">
|
||||
<div className="inline-block min-w-full p-0.5 align-middle">
|
||||
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 md:rounded-lg">
|
||||
<table className="min-w-full">
|
||||
{selectedGroup !== null ? (
|
||||
<thead className="bg-gray-100">
|
||||
<tr>
|
||||
<th
|
||||
colSpan={14}
|
||||
scope="col"
|
||||
className="px-3 py-3.5 text-left uppercase text-sm font-semibold text-gray-900"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedGroup === "state_detail.name" ? (
|
||||
<span
|
||||
className="flex-shrink-0 h-1.5 w-1.5 block rounded-full"
|
||||
style={{
|
||||
backgroundColor: states?.find((s) => s.name === singleGroup)?.color,
|
||||
}}
|
||||
></span>
|
||||
) : null}
|
||||
{singleGroup === null || singleGroup === "null"
|
||||
? selectedGroup === "priority" && "No priority"
|
||||
: addSpaceIfCamelCase(singleGroup)}
|
||||
<span className="ml-2 text-gray-500 font-normal text-sm">
|
||||
{groupedByIssues[singleGroup as keyof IIssue].length}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
) : null}
|
||||
{groupedByIssues[singleGroup].length > 0
|
||||
? groupedByIssues[singleGroup].map((issue: IIssue, index: number) => {
|
||||
const assignees = [
|
||||
...(issue?.assignees_list ?? []),
|
||||
...(issue?.assignees ?? []),
|
||||
]?.map(
|
||||
(assignee) =>
|
||||
people?.find((p) => p.member.id === assignee)?.member.email
|
||||
);
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
) : (
|
||||
<thead className="bg-gray-100">
|
||||
<tr>
|
||||
<th
|
||||
colSpan={14}
|
||||
scope="col"
|
||||
className="px-3 py-3.5 text-left uppercase text-sm font-semibold text-gray-900"
|
||||
>
|
||||
ALL ISSUES
|
||||
<span className="ml-2 text-gray-500 font-normal text-sm">
|
||||
{groupedByIssues[singleGroup as keyof IIssue].length}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
)}
|
||||
<tbody className="bg-white">
|
||||
{groupedByIssues[singleGroup].length > 0
|
||||
? groupedByIssues[singleGroup].map((issue: IIssue, index: number) => {
|
||||
const assignees = [
|
||||
...(issue?.assignees_list ?? []),
|
||||
...(issue?.assignees ?? []),
|
||||
]?.map(
|
||||
(assignee) => people?.find((p) => p.member.id === assignee)?.member.email
|
||||
);
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={issue.id}
|
||||
className={classNames(
|
||||
index === 0 ? "border-gray-300" : "border-gray-200",
|
||||
"border-t"
|
||||
)}
|
||||
onMouseEnter={() => handleHover(issue.id)}
|
||||
>
|
||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 w-[15rem]">
|
||||
<Link href={`/projects/${issue.project}/issues/${issue.id}`}>
|
||||
<a className="hover:text-theme duration-300">{issue.name}</a>
|
||||
</Link>
|
||||
</td>
|
||||
{Object.keys(properties).map(
|
||||
(key) =>
|
||||
properties[key as keyof Properties] && (
|
||||
<React.Fragment key={key}>
|
||||
{(key as keyof Properties) === "key" ? (
|
||||
<td className="px-3 py-4 font-medium text-gray-900 text-xs whitespace-nowrap">
|
||||
{activeProject?.identifier}-{issue.sequence_id}
|
||||
</td>
|
||||
) : (key as keyof Properties) === "priority" ? (
|
||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.priority}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ priority: data }, issue.id);
|
||||
}}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="">
|
||||
<Listbox.Button className="inline-flex items-center whitespace-nowrap rounded-full bg-gray-50 py-1 px-0.5 text-xs font-medium text-gray-500 hover:bg-gray-100 border">
|
||||
<span
|
||||
className={classNames(
|
||||
issue.priority ? "" : "text-gray-900",
|
||||
"hidden truncate capitalize sm:block w-16"
|
||||
)}
|
||||
>
|
||||
{issue.priority ?? "None"}
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
return (
|
||||
<tr
|
||||
key={issue.id}
|
||||
className={classNames(
|
||||
index === 0 ? "border-gray-300" : "border-gray-200",
|
||||
"border-t"
|
||||
)}
|
||||
>
|
||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 w-[15rem]">
|
||||
<Link href={`/projects/${issue.project}/issues/${issue.id}`}>
|
||||
<a className="hover:text-theme duration-300">{issue.name}</a>
|
||||
</Link>
|
||||
</td>
|
||||
{Object.keys(properties).map(
|
||||
(key) =>
|
||||
properties[key as keyof Properties] && (
|
||||
<React.Fragment key={key}>
|
||||
{(key as keyof Properties) === "key" ? (
|
||||
<td className="px-3 py-4 font-medium text-gray-900 text-xs whitespace-nowrap">
|
||||
{activeProject?.identifier}-{issue.sequence_id}
|
||||
</td>
|
||||
) : (key as keyof Properties) === "priority" ? (
|
||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.priority}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ priority: data }, issue.id);
|
||||
}}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="">
|
||||
<Listbox.Button className="inline-flex items-center whitespace-nowrap rounded-full bg-gray-50 py-1 px-0.5 text-xs font-medium text-gray-500 hover:bg-gray-100 border">
|
||||
<span
|
||||
className={classNames(
|
||||
issue.priority ? "" : "text-gray-900",
|
||||
"hidden truncate capitalize sm:block w-16"
|
||||
)}
|
||||
>
|
||||
<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">
|
||||
{PRIORITIES?.map((priority) => (
|
||||
<Listbox.Option
|
||||
key={priority}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "bg-indigo-50" : "bg-white",
|
||||
"cursor-pointer capitalize select-none px-3 py-2"
|
||||
{issue.priority ?? "None"}
|
||||
</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 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">
|
||||
{PRIORITIES?.map((priority) => (
|
||||
<Listbox.Option
|
||||
key={priority}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "bg-indigo-50" : "bg-white",
|
||||
"cursor-pointer capitalize select-none px-3 py-2"
|
||||
)
|
||||
}
|
||||
value={priority}
|
||||
>
|
||||
{priority}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
</td>
|
||||
) : (key as keyof Properties) === "assignee" ? (
|
||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.assignees}
|
||||
onChange={(data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
if (newData.includes(data)) {
|
||||
newData.splice(newData.indexOf(data), 1);
|
||||
} else {
|
||||
newData.push(data);
|
||||
}
|
||||
partialUpdateIssue(
|
||||
{ assignees_list: newData },
|
||||
issue.id
|
||||
);
|
||||
}}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Listbox.Button className="rounded-full bg-gray-50 px-5 py-1 text-xs text-gray-500 hover:bg-gray-100 border">
|
||||
{() => {
|
||||
if (assignees.length > 0)
|
||||
return (
|
||||
<>
|
||||
{assignees.map((assignee, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={
|
||||
"hidden truncate sm:block text-left"
|
||||
}
|
||||
>
|
||||
{assignee}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
else return <span>None</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 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">
|
||||
{people?.map((person) => (
|
||||
<Listbox.Option
|
||||
key={person.id}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "bg-indigo-50" : "bg-white",
|
||||
"cursor-pointer select-none px-3 py-2"
|
||||
)
|
||||
}
|
||||
value={person.member.id}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center gap-x-1 ${
|
||||
assignees.includes(
|
||||
person.member.first_name
|
||||
)
|
||||
}
|
||||
value={priority}
|
||||
? "font-medium"
|
||||
: "font-normal"
|
||||
}`}
|
||||
>
|
||||
{priority}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
</td>
|
||||
) : (key as keyof Properties) === "assignee" ? (
|
||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.assignees}
|
||||
onChange={(data: any) => {
|
||||
const newData = issue.assignees ?? [];
|
||||
if (newData.includes(data)) {
|
||||
newData.splice(newData.indexOf(data), 1);
|
||||
} else {
|
||||
newData.push(data);
|
||||
}
|
||||
partialUpdateIssue(
|
||||
{ assignees_list: newData },
|
||||
issue.id
|
||||
);
|
||||
}}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Listbox.Button className="rounded-full bg-gray-50 px-5 py-1 text-xs text-gray-500 hover:bg-gray-100 border">
|
||||
{() => {
|
||||
if (assignees.length > 0)
|
||||
return (
|
||||
<>
|
||||
{assignees.map((assignee, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={
|
||||
"hidden truncate sm:block text-left"
|
||||
}
|
||||
>
|
||||
{assignee}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
else return <span>None</span>;
|
||||
}}
|
||||
</Listbox.Button>
|
||||
{person.member.avatar &&
|
||||
person.member.avatar !== "" ? (
|
||||
<div className="relative w-4 h-4">
|
||||
<Image
|
||||
src={person.member.avatar}
|
||||
alt="avatar"
|
||||
className="rounded-full"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p>
|
||||
{person.member.first_name.charAt(0)}
|
||||
</p>
|
||||
)}
|
||||
<p>{person.member.first_name}</p>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
</td>
|
||||
) : (key as keyof Properties) === "state" ? (
|
||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.state}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ state: data }, issue.id);
|
||||
}}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Listbox.Button
|
||||
className="inline-flex items-center whitespace-nowrap rounded-full px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 border"
|
||||
style={{
|
||||
border: `2px solid ${issue.state_detail.color}`,
|
||||
backgroundColor: `${issue.state_detail.color}20`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={classNames(
|
||||
issue.state ? "" : "text-gray-900",
|
||||
"hidden capitalize sm:block w-16"
|
||||
)}
|
||||
>
|
||||
{addSpaceIfCamelCase(issue.state_detail.name)}
|
||||
</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 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">
|
||||
{people?.map((person) => (
|
||||
<Listbox.Option
|
||||
key={person.id}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "bg-indigo-50" : "bg-white",
|
||||
"cursor-pointer select-none px-3 py-2"
|
||||
)
|
||||
}
|
||||
value={person.member.id}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center gap-x-1 ${
|
||||
assignees.includes(
|
||||
person.member.first_name
|
||||
)
|
||||
? "font-medium"
|
||||
: "font-normal"
|
||||
}`}
|
||||
>
|
||||
{person.member.avatar &&
|
||||
person.member.avatar !== "" ? (
|
||||
<div className="relative w-4 h-4">
|
||||
<Image
|
||||
src={person.member.avatar}
|
||||
alt="avatar"
|
||||
className="rounded-full"
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p>
|
||||
{person.member.first_name.charAt(0)}
|
||||
</p>
|
||||
)}
|
||||
<p>{person.member.first_name}</p>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
</td>
|
||||
) : (key as keyof Properties) === "state" ? (
|
||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative">
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.state}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ state: data }, issue.id);
|
||||
}}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div>
|
||||
<Listbox.Button
|
||||
className="inline-flex items-center whitespace-nowrap rounded-full px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 border"
|
||||
style={{
|
||||
border: `2px solid ${issue.state_detail.color}`,
|
||||
backgroundColor: `${issue.state_detail.color}20`,
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={classNames(
|
||||
issue.state ? "" : "text-gray-900",
|
||||
"hidden capitalize sm:block w-16"
|
||||
)}
|
||||
>
|
||||
{addSpaceIfCamelCase(issue.state_detail.name)}
|
||||
</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 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">
|
||||
{states?.map((state) => (
|
||||
<Listbox.Option
|
||||
key={state.id}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "bg-indigo-50" : "bg-white",
|
||||
"cursor-pointer select-none px-3 py-2"
|
||||
)
|
||||
}
|
||||
value={state.id}
|
||||
>
|
||||
{addSpaceIfCamelCase(state.name)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
</td>
|
||||
) : (key as keyof Properties) === "due_date" ? (
|
||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 whitespace-nowrap">
|
||||
{issue.target_date
|
||||
? renderShortNumericDateFormat(issue.target_date)
|
||||
: "-"}
|
||||
</td>
|
||||
) : (
|
||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative capitalize">
|
||||
{issue[key as keyof IIssue] ??
|
||||
(issue[key as keyof IIssue] as any)?.name ??
|
||||
"None"}
|
||||
</td>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
<td className="px-3">
|
||||
<div className="flex justify-end items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center bg-blue-100 text-blue-600 hover:bg-blue-200 duration-300 font-medium px-2 py-1 rounded-md text-sm outline-none"
|
||||
onClick={() => {
|
||||
setSelectedIssue({
|
||||
...issue,
|
||||
actionType: "edit",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<PencilIcon className="h-3 w-3" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center bg-red-100 text-red-600 hover:bg-red-200 duration-300 font-medium px-2 py-1 rounded-md text-sm outline-none"
|
||||
onClick={() => {
|
||||
handleDeleteIssue(issue.id);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<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">
|
||||
{states?.map((state) => (
|
||||
<Listbox.Option
|
||||
key={state.id}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "bg-indigo-50" : "bg-white",
|
||||
"cursor-pointer select-none px-3 py-2"
|
||||
)
|
||||
}
|
||||
value={state.id}
|
||||
>
|
||||
{addSpaceIfCamelCase(state.name)}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
</td>
|
||||
) : (key as keyof Properties) === "target_date" ? (
|
||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 whitespace-nowrap">
|
||||
{issue.target_date
|
||||
? renderShortNumericDateFormat(issue.target_date)
|
||||
: "-"}
|
||||
</td>
|
||||
) : (
|
||||
<td className="px-3 py-4 text-sm font-medium text-gray-900 relative capitalize">
|
||||
{issue[key as keyof IIssue] ??
|
||||
(issue[key as keyof IIssue] as any)?.name ??
|
||||
"None"}
|
||||
</td>
|
||||
)}
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
<td className="px-3">
|
||||
<div className="flex justify-end items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center bg-blue-100 text-blue-600 hover:bg-blue-200 duration-300 font-medium px-2 py-1 rounded-md text-sm outline-none"
|
||||
onClick={() => {
|
||||
setSelectedIssue({
|
||||
...issue,
|
||||
actionType: "edit",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<PencilIcon className="h-3 w-3" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center bg-red-100 text-red-600 hover:bg-red-200 duration-300 font-medium px-2 py-1 rounded-md text-sm outline-none"
|
||||
onClick={() => {
|
||||
handleDeleteIssue(issue.id);
|
||||
}}
|
||||
>
|
||||
<TrashIcon className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -1,138 +0,0 @@
|
||||
// next
|
||||
import { useRouter } from "next/router";
|
||||
// react
|
||||
import { Fragment } from "react";
|
||||
// headless ui
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// services
|
||||
import issuesServices from "lib/services/issues.services";
|
||||
import projectService from "lib/services/project.service";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
// types
|
||||
import { IIssue, ProjectMember } from "types";
|
||||
// constants
|
||||
import { PROJECT_ISSUES_DETAILS, PROJECT_MEMBERS } from "constants/fetch-keys";
|
||||
import { Button } from "ui";
|
||||
import { ChartBarIcon, Squares2X2Icon, TagIcon, UserIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
issueId: string | null;
|
||||
};
|
||||
|
||||
const IssuePreviewModal = ({ isOpen, setIsOpen, issueId }: Props) => {
|
||||
const closeModal = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const { activeWorkspace, activeProject } = useUser();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { data: issueDetails } = useSWR<IIssue | null>(
|
||||
activeWorkspace && activeProject && issueId ? PROJECT_ISSUES_DETAILS(issueId) : null,
|
||||
activeWorkspace && activeProject && issueId
|
||||
? () => issuesServices.getIssue(activeWorkspace.slug, activeProject.id, issueId)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: users } = useSWR<ProjectMember[] | null>(
|
||||
activeWorkspace && activeProject ? PROJECT_MEMBERS(activeProject.id) : null,
|
||||
activeWorkspace && activeProject
|
||||
? () => projectService.projectMembers(activeWorkspace.slug, activeProject.id)
|
||||
: null
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition.Root appear show={isOpen} as={Fragment}>
|
||||
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-black bg-opacity-25" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 overflow-y-auto">
|
||||
<div className="flex min-h-full items-center justify-center p-4 text-center">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-out duration-300"
|
||||
enterFrom="opacity-0 scale-95"
|
||||
enterTo="opacity-100 scale-100"
|
||||
leave="ease-in duration-200"
|
||||
leaveFrom="opacity-100 scale-100"
|
||||
leaveTo="opacity-0 scale-95"
|
||||
>
|
||||
<Dialog.Panel className="w-full max-w-3xl transform overflow-hidden rounded-2xl bg-white p-6 text-left align-middle shadow-xl transition-all">
|
||||
<Dialog.Title
|
||||
as="h3"
|
||||
className="text-xl flex flex-col gap-1 font-medium leading-6 text-gray-900"
|
||||
>
|
||||
{issueDetails?.project_detail.identifier}-{issueDetails?.sequence_id}{" "}
|
||||
{issueDetails?.name}
|
||||
<span className="text-sm text-gray-500 font-normal">
|
||||
Created by{" "}
|
||||
{users?.find((u) => u.id === issueDetails?.created_by)?.member.first_name}
|
||||
</span>
|
||||
</Dialog.Title>
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-gray-500">{issueDetails?.description}</p>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
<span className="flex items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 sm:text-sm">
|
||||
<Squares2X2Icon className="h-3 w-3" />
|
||||
{issueDetails?.state_detail.name}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 capitalize sm:text-sm">
|
||||
<ChartBarIcon className="h-3 w-3" />
|
||||
{issueDetails?.priority}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 capitalize sm:text-sm">
|
||||
<TagIcon className="h-3 w-3" />
|
||||
{issueDetails?.label_details && issueDetails.label_details.length > 0
|
||||
? issueDetails.label_details.map((label) => (
|
||||
<span key={label.id}>{label.name}</span>
|
||||
))
|
||||
: "None"}
|
||||
</span>
|
||||
<span className="flex items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 py-1 capitalize sm:text-sm">
|
||||
<UserIcon className="h-3 w-3" />
|
||||
{issueDetails?.assignee_details && issueDetails.assignee_details.length > 0
|
||||
? issueDetails.assignee_details.map((assignee) => (
|
||||
<span key={assignee.id}>{assignee.first_name}</span>
|
||||
))
|
||||
: "None"}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-4 flex gap-3 justify-end">
|
||||
<Button
|
||||
onClick={() =>
|
||||
router.push(`/projects/${activeProject?.id}/issues/${issueId}`)
|
||||
}
|
||||
>
|
||||
View in Detail
|
||||
</Button>
|
||||
<Button onClick={closeModal}>Close</Button>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IssuePreviewModal;
|
@ -4,13 +4,14 @@ import useSWR from "swr";
|
||||
// headless ui
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
// react hook form
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { useForm, Controller, UseFormWatch } from "react-hook-form";
|
||||
// services
|
||||
import stateServices from "lib/services/state.services";
|
||||
import issuesServices from "lib/services/issues.services";
|
||||
import workspaceService from "lib/services/workspace.service";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
import useToast from "lib/hooks/useToast";
|
||||
// fetching keys
|
||||
import {
|
||||
PROJECT_ISSUES_LIST,
|
||||
@ -20,6 +21,7 @@ import {
|
||||
} from "constants/fetch-keys";
|
||||
// commons
|
||||
import { classNames, copyTextToClipboard } from "constants/common";
|
||||
import { PRIORITIES } from "constants/";
|
||||
// ui
|
||||
import { Input, Button, Spinner } from "ui";
|
||||
import { Popover } from "@headlessui/react";
|
||||
@ -38,25 +40,40 @@ import {
|
||||
} from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import type { Control } from "react-hook-form";
|
||||
import type { IIssue, IIssueLabels, IssueResponse, IState, WorkspaceMember } from "types";
|
||||
import type {
|
||||
IIssue,
|
||||
IIssueLabels,
|
||||
IssueResponse,
|
||||
IState,
|
||||
NestedKeyOf,
|
||||
WorkspaceMember,
|
||||
} from "types";
|
||||
import { TwitterPicker } from "react-color";
|
||||
import useToast from "lib/hooks/useToast";
|
||||
import IssuesListModal from "components/project/issues/IssuesListModal";
|
||||
|
||||
type Props = {
|
||||
control: Control<IIssue, any>;
|
||||
submitChanges: (formData: Partial<IIssue>) => void;
|
||||
issueDetail: IIssue | undefined;
|
||||
watch: UseFormWatch<IIssue>;
|
||||
};
|
||||
|
||||
const PRIORITIES = ["high", "medium", "low"];
|
||||
|
||||
const defaultValues: Partial<IIssueLabels> = {
|
||||
name: "",
|
||||
colour: "#ff0000",
|
||||
};
|
||||
|
||||
const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDetail }) => {
|
||||
const { activeWorkspace, activeProject, cycles } = useUser();
|
||||
const IssueDetailSidebar: React.FC<Props> = ({
|
||||
control,
|
||||
watch: watchIssue,
|
||||
submitChanges,
|
||||
issueDetail,
|
||||
}) => {
|
||||
const [isBlockerModalOpen, setIsBlockerModalOpen] = useState(false);
|
||||
const [isBlockedModalOpen, setIsBlockedModalOpen] = useState(false);
|
||||
const [isParentModalOpen, setIsParentModalOpen] = useState(false);
|
||||
|
||||
const { activeWorkspace, activeProject, cycles, issues } = useUser();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@ -72,15 +89,6 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null
|
||||
);
|
||||
|
||||
const { data: projectIssues } = useSWR<IssueResponse>(
|
||||
activeProject && activeWorkspace
|
||||
? PROJECT_ISSUES_LIST(activeWorkspace.slug, activeProject.id)
|
||||
: null,
|
||||
activeProject && activeWorkspace
|
||||
? () => issuesServices.getIssues(activeWorkspace.slug, activeProject.id)
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: issueLabels, mutate: issueLabelMutate } = useSWR<IIssueLabels[]>(
|
||||
activeProject && activeWorkspace ? PROJECT_ISSUE_LABELS(activeProject.id) : null,
|
||||
activeProject && activeWorkspace
|
||||
@ -110,7 +118,19 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
||||
});
|
||||
};
|
||||
|
||||
const sidebarSections = [
|
||||
const sidebarSections: Array<
|
||||
Array<{
|
||||
label: string;
|
||||
name: NestedKeyOf<IIssue>;
|
||||
canSelectMultipleOptions: boolean;
|
||||
icon: (props: any) => JSX.Element;
|
||||
options?: Array<{ label: string; value: any }>;
|
||||
modal: boolean;
|
||||
issuesList?: Array<IIssue>;
|
||||
isOpen?: boolean;
|
||||
setIsOpen?: (arg: boolean) => void;
|
||||
}>
|
||||
> = [
|
||||
[
|
||||
{
|
||||
label: "Status",
|
||||
@ -121,6 +141,7 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
||||
label: state.name,
|
||||
value: state.id,
|
||||
})),
|
||||
modal: false,
|
||||
},
|
||||
{
|
||||
label: "Assignees",
|
||||
@ -131,6 +152,7 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
||||
label: person.member.first_name,
|
||||
value: person.member.id,
|
||||
})),
|
||||
modal: false,
|
||||
},
|
||||
{
|
||||
label: "Priority",
|
||||
@ -141,34 +163,52 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
||||
label: property,
|
||||
value: property,
|
||||
})),
|
||||
modal: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
label: "Blocker",
|
||||
name: "blockers_list",
|
||||
canSelectMultipleOptions: true,
|
||||
label: "Parent",
|
||||
name: "parent",
|
||||
canSelectMultipleOptions: false,
|
||||
icon: UserIcon,
|
||||
options: projectIssues?.results?.map((issue) => ({
|
||||
label: issue.name,
|
||||
value: issue.id,
|
||||
})),
|
||||
issuesList:
|
||||
issues?.results.filter(
|
||||
(i) =>
|
||||
i.id !== issueDetail?.id &&
|
||||
i.id !== issueDetail?.parent &&
|
||||
i.parent !== issueDetail?.id
|
||||
) ?? [],
|
||||
modal: true,
|
||||
isOpen: isParentModalOpen,
|
||||
setIsOpen: setIsParentModalOpen,
|
||||
},
|
||||
// {
|
||||
// label: "Blocker",
|
||||
// name: "blockers_list",
|
||||
// canSelectMultipleOptions: true,
|
||||
// icon: UserIcon,
|
||||
// issuesList: issues?.results.filter((i) => i.id !== issueDetail?.id) ?? [],
|
||||
// modal: true,
|
||||
// isOpen: isBlockerModalOpen,
|
||||
// setIsOpen: setIsBlockerModalOpen,
|
||||
// },
|
||||
// {
|
||||
// label: "Blocked",
|
||||
// name: "blocked_list",
|
||||
// canSelectMultipleOptions: true,
|
||||
// icon: UserIcon,
|
||||
// issuesList: issues?.results.filter((i) => i.id !== issueDetail?.id) ?? [],
|
||||
// modal: true,
|
||||
// isOpen: isBlockedModalOpen,
|
||||
// setIsOpen: setIsBlockedModalOpen,
|
||||
// },
|
||||
{
|
||||
label: "Blocked",
|
||||
name: "blocked_list",
|
||||
canSelectMultipleOptions: true,
|
||||
icon: UserIcon,
|
||||
options: projectIssues?.results?.map((issue) => ({
|
||||
label: issue.name,
|
||||
value: issue.id,
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: "Due Date",
|
||||
label: "Target Date",
|
||||
name: "target_date",
|
||||
canSelectMultipleOptions: true,
|
||||
icon: CalendarDaysIcon,
|
||||
modal: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
@ -181,6 +221,7 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
||||
label: cycle.name,
|
||||
value: cycle.id,
|
||||
})),
|
||||
modal: false,
|
||||
},
|
||||
],
|
||||
];
|
||||
@ -249,12 +290,12 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
||||
{sidebarSections.map((section, index) => (
|
||||
<div key={index} className="py-1">
|
||||
{section.map((item) => (
|
||||
<div key={item.label} className="flex justify-between items-center gap-x-2 py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm">
|
||||
<item.icon className="h-4 w-4" />
|
||||
<div key={item.label} className="flex items-center py-2 flex-wrap">
|
||||
<div className="flex items-center gap-x-2 text-sm sm:basis-1/2">
|
||||
<item.icon className="flex-shrink-0 h-4 w-4" />
|
||||
<p>{item.label}</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="sm:basis-1/2">
|
||||
{item.name === "target_date" ? (
|
||||
<Controller
|
||||
control={control}
|
||||
@ -262,15 +303,52 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<input
|
||||
type="date"
|
||||
value={""}
|
||||
value={value ?? ""}
|
||||
onChange={(e: any) => {
|
||||
submitChanges({ target_date: e.target.value });
|
||||
onChange(e.target.value);
|
||||
}}
|
||||
className="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-xs duration-300"
|
||||
className="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-xs duration-300 w-full"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : item.modal ? (
|
||||
<Controller
|
||||
control={control}
|
||||
name={item.name as keyof IIssue}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<>
|
||||
<IssuesListModal
|
||||
isOpen={Boolean(item?.isOpen)}
|
||||
handleClose={() => item.setIsOpen && item.setIsOpen(false)}
|
||||
onChange={(val) => {
|
||||
console.log(val);
|
||||
// submitChanges({ [item.name]: val });
|
||||
onChange(val);
|
||||
}}
|
||||
issues={item?.issuesList ?? []}
|
||||
title={`Select ${item.label}`}
|
||||
multiple={item.canSelectMultipleOptions}
|
||||
value={value}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="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-xs duration-300 w-full"
|
||||
onClick={() => item.setIsOpen && item.setIsOpen(true)}
|
||||
>
|
||||
{watchIssue(`${item.name as keyof IIssue}`) &&
|
||||
watchIssue(`${item.name as keyof IIssue}`) !== ""
|
||||
? `${activeProject?.identifier}-
|
||||
${
|
||||
issues?.results.find(
|
||||
(i) => i.id === watchIssue(`${item.name as keyof IIssue}`)
|
||||
)?.sequence_id
|
||||
}`
|
||||
: `Select ${item.label}`}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Controller
|
||||
control={control}
|
||||
@ -288,11 +366,11 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
||||
>
|
||||
{({ open }) => (
|
||||
<div className="relative">
|
||||
<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">
|
||||
<Listbox.Button className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<span
|
||||
className={classNames(
|
||||
value ? "" : "text-gray-900",
|
||||
"hidden truncate sm:block w-16 text-left",
|
||||
"hidden truncate sm:block text-left",
|
||||
item.label === "Priority" ? "capitalize" : ""
|
||||
)}
|
||||
>
|
||||
@ -419,12 +497,12 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
||||
+
|
||||
</Button>
|
||||
</form>
|
||||
<div className="flex justify-between items-center gap-x-2">
|
||||
<div className="flex items-center gap-x-2 text-sm">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex items-center gap-x-2 text-sm basis-1/2">
|
||||
<TagIcon className="w-4 h-4" />
|
||||
<p>Label</p>
|
||||
</div>
|
||||
<div>
|
||||
<div className="basis-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="labels_list"
|
||||
@ -440,11 +518,11 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDeta
|
||||
<>
|
||||
<Listbox.Label className="sr-only">Label</Listbox.Label>
|
||||
<div className="relative">
|
||||
<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">
|
||||
<Listbox.Button className="flex justify-between items-center gap-1 hover:bg-gray-100 border rounded-md shadow-sm px-2 w-full py-1 cursor-pointer focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 text-xs duration-300">
|
||||
<span
|
||||
className={classNames(
|
||||
value ? "" : "text-gray-900",
|
||||
"hidden truncate capitalize sm:block w-16 text-left"
|
||||
"hidden truncate capitalize sm:block text-left"
|
||||
)}
|
||||
>
|
||||
{value && value.length > 0
|
||||
|
@ -64,14 +64,14 @@ const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentDeletion
|
||||
<div key={comment.id}>
|
||||
<div className="w-full h-full flex justify-between">
|
||||
<div className="flex gap-x-2 w-full">
|
||||
<div className="flex-shrink-0 -ml-1.5">
|
||||
<div className="flex-shrink-0">
|
||||
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
|
||||
<Image
|
||||
src={comment.actor_detail.avatar}
|
||||
alt={comment.actor_detail.name}
|
||||
height={30}
|
||||
width={30}
|
||||
className="rounded-full"
|
||||
className="rounded"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
@ -82,11 +82,7 @@ const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentDeletion
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<p>
|
||||
{comment.actor_detail.first_name} {comment.actor_detail.last_name}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">{timeAgo(comment.created_at)}</p>
|
||||
<div className="w-full mt-2">
|
||||
<div>
|
||||
{isEditing ? (
|
||||
<form className="flex flex-col gap-2" onSubmit={handleSubmit(onEnter)}>
|
||||
<TextArea
|
||||
@ -120,13 +116,19 @@ const CommentCard: React.FC<Props> = ({ comment, onSubmit, handleCommentDeletion
|
||||
) : (
|
||||
<>
|
||||
{comment.comment.split("\n").map((item, index) => (
|
||||
<p key={index} className="text-sm text-gray-600">
|
||||
<p key={index} className="text-sm">
|
||||
{item}
|
||||
</p>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 flex items-center gap-2 mt-1">
|
||||
<span>
|
||||
{comment.actor_detail.first_name} {comment.actor_detail.last_name}
|
||||
</span>
|
||||
<span>{timeAgo(comment.created_at)}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{user?.id === comment.actor && (
|
||||
|
@ -72,7 +72,7 @@ const IssueCommentSection: React.FC<Props> = ({ comments, issueId, projectId, wo
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="p-2 bg-indigo-50 rounded-md">
|
||||
<div className="bg-gray-100 rounded-md">
|
||||
<div className="w-full">
|
||||
<TextArea
|
||||
id="comment"
|
||||
|
@ -7,7 +7,7 @@ import projectServices from "lib/services/project.service";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// ui
|
||||
import { Input, Select, TextArea } from "ui";
|
||||
import { Button, Input, Select, TextArea } from "ui";
|
||||
// types
|
||||
import { IProject } from "types";
|
||||
// constants
|
||||
@ -17,11 +17,12 @@ type Props = {
|
||||
register: UseFormRegister<IProject>;
|
||||
errors: any;
|
||||
setError: UseFormSetError<IProject>;
|
||||
isSubmitting: boolean;
|
||||
};
|
||||
|
||||
const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
||||
|
||||
const GeneralSettings: React.FC<Props> = ({ register, errors, setError }) => {
|
||||
const GeneralSettings: React.FC<Props> = ({ register, errors, setError, isSubmitting }) => {
|
||||
const { activeWorkspace } = useUser();
|
||||
|
||||
const checkIdentifier = (slug: string, value: string) => {
|
||||
@ -111,6 +112,11 @@ const GeneralSettings: React.FC<Props> = ({ register, errors, setError }) => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Updating Project..." : "Update Project"}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
|
@ -47,6 +47,7 @@ const LabelsSettings: React.FC = () => {
|
||||
setValue,
|
||||
formState: { errors, isSubmitting },
|
||||
watch,
|
||||
setError,
|
||||
} = useForm<IIssueLabels>({ defaultValues });
|
||||
|
||||
const { data: issueLabels, mutate } = useSWR<IIssueLabels[]>(
|
||||
@ -163,23 +164,33 @@ const LabelsSettings: React.FC = () => {
|
||||
)}
|
||||
</Popover>
|
||||
</div>
|
||||
<Input
|
||||
type="text"
|
||||
id="labelName"
|
||||
name="name"
|
||||
register={register}
|
||||
placeholder="Lable title"
|
||||
/>
|
||||
<div className="w-full flex flex-col justify-center">
|
||||
<Input
|
||||
type="text"
|
||||
id="labelName"
|
||||
name="name"
|
||||
register={register}
|
||||
placeholder="Lable title"
|
||||
validations={{
|
||||
required: "Label title is required",
|
||||
}}
|
||||
error={errors.name}
|
||||
/>
|
||||
</div>
|
||||
<Button type="button" theme="secondary" onClick={() => setNewLabelForm(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
{isUpdating ? (
|
||||
<Button type="button" onClick={handleSubmit(handleLabelUpdate)}>
|
||||
Update
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleSubmit(handleLabelUpdate)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? "Updating" : "Update"}
|
||||
</Button>
|
||||
) : (
|
||||
<Button type="button" onClick={handleSubmit(handleNewLabel)}>
|
||||
Add
|
||||
<Button type="button" onClick={handleSubmit(handleNewLabel)} disabled={isSubmitting}>
|
||||
{isSubmitting ? "Adding" : "Add"}
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
@ -193,7 +204,7 @@ const LabelsSettings: React.FC = () => {
|
||||
<div className="bg-white p-2 flex items-center justify-between text-gray-900 rounded-md">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className={`h-1.5 w-1.5 rounded-full`}
|
||||
className="flex-shrink-0 h-1.5 w-1.5 rounded-full"
|
||||
style={{
|
||||
backgroundColor: label.colour,
|
||||
}}
|
||||
|
12
apps/app/constants/global.tsx
Normal file
12
apps/app/constants/global.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
export const getPriorityIcon = (priority: string) => {
|
||||
switch (priority) {
|
||||
case "urgent":
|
||||
return <span className="material-symbols-rounded">signal_cellular_alt</span>;
|
||||
case "high":
|
||||
return <span className="material-symbols-rounded">signal_cellular_alt_2_bar</span>;
|
||||
case "medium":
|
||||
return <span className="material-symbols-rounded">signal_cellular_alt_1_bar</span>;
|
||||
default:
|
||||
return <span>N/A</span>;
|
||||
}
|
||||
};
|
@ -12,7 +12,7 @@ import CreateProjectModal from "components/project/CreateProjectModal";
|
||||
// types
|
||||
import type { Props } from "./types";
|
||||
|
||||
const AdminLayout: React.FC<Props> = ({ meta, children }) => {
|
||||
const AppLayout: React.FC<Props> = ({ meta, children, noPadding = false, bg = "primary" }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
@ -28,10 +28,16 @@ const AdminLayout: React.FC<Props> = ({ meta, children }) => {
|
||||
<CreateProjectModal isOpen={isOpen} setIsOpen={setIsOpen} />
|
||||
<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>
|
||||
<main
|
||||
className={`h-full w-full min-w-0 overflow-y-auto ${noPadding ? "" : "p-5"} ${
|
||||
bg === "primary" ? "bg-primary" : bg === "secondary" ? "bg-secondary" : "bg-primary"
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminLayout;
|
||||
export default AppLayout;
|
2
apps/app/layouts/types.d.ts
vendored
2
apps/app/layouts/types.d.ts
vendored
@ -8,4 +8,6 @@ export type Meta = {
|
||||
export type Props = {
|
||||
meta?: Meta;
|
||||
children: React.ReactNode;
|
||||
noPadding?: boolean;
|
||||
bg?: "primary" | "secondary";
|
||||
};
|
||||
|
@ -16,7 +16,7 @@ const initialValues: Properties = {
|
||||
assignee: true,
|
||||
priority: false,
|
||||
start_date: false,
|
||||
due_date: false,
|
||||
target_date: false,
|
||||
cycle: false,
|
||||
};
|
||||
|
||||
|
@ -5,7 +5,7 @@ import type { NextPage } from "next";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
// layouts
|
||||
import AdminLayout from "layouts/AdminLayout";
|
||||
import AppLayout from "layouts/AppLayout";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// ui
|
||||
@ -74,7 +74,7 @@ const MyIssues: NextPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<AppLayout>
|
||||
<div className="w-full h-full flex flex-col space-y-5">
|
||||
{myIssues ? (
|
||||
<>
|
||||
@ -274,7 +274,7 @@ const MyIssues: NextPage = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -9,7 +9,7 @@ import Dropzone, { useDropzone } from "react-dropzone";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// layouts
|
||||
import AdminLayout from "layouts/AdminLayout";
|
||||
import AppLayout from "layouts/AppLayout";
|
||||
// services
|
||||
import userService from "lib/services/user.service";
|
||||
import fileServices from "lib/services/file.services";
|
||||
@ -112,7 +112,7 @@ const Profile: NextPage = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<AdminLayout
|
||||
<AppLayout
|
||||
meta={{
|
||||
title: "Plane - My Profile",
|
||||
}}
|
||||
@ -303,7 +303,7 @@ const Profile: NextPage = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -10,7 +10,7 @@ import useUser from "lib/hooks/useUser";
|
||||
// hoc
|
||||
import withAuthWrapper from "lib/hoc/withAuthWrapper";
|
||||
// layouts
|
||||
import AdminLayout from "layouts/AdminLayout";
|
||||
import AppLayout from "layouts/AppLayout";
|
||||
// ui
|
||||
import { Button } from "ui";
|
||||
// swr
|
||||
@ -52,7 +52,7 @@ const MyWorkspacesInvites: NextPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout
|
||||
<AppLayout
|
||||
meta={{
|
||||
title: "Plane - My Workspace Invites",
|
||||
}}
|
||||
@ -147,7 +147,7 @@ const MyWorkspacesInvites: NextPage = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -12,7 +12,7 @@ import useUser from "lib/hooks/useUser";
|
||||
// fetching keys
|
||||
import { CYCLE_ISSUES, CYCLE_LIST } from "constants/fetch-keys";
|
||||
// layouts
|
||||
import AdminLayout from "layouts/AdminLayout";
|
||||
import AppLayout from "layouts/AppLayout";
|
||||
// components
|
||||
import CycleView from "components/project/cycles/CycleView";
|
||||
import ConfirmIssueDeletion from "components/project/issues/ConfirmIssueDeletion";
|
||||
@ -165,7 +165,7 @@ const ProjectSprints: NextPage = () => {
|
||||
}, [selectedIssues]);
|
||||
|
||||
return (
|
||||
<AdminLayout
|
||||
<AppLayout
|
||||
meta={{
|
||||
title: "Plane - Cycles",
|
||||
}}
|
||||
@ -254,7 +254,7 @@ const ProjectSprints: NextPage = () => {
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</AdminLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -12,7 +12,6 @@ import { Controller, useForm } from "react-hook-form";
|
||||
import { Disclosure, Menu, Tab, Transition } from "@headlessui/react";
|
||||
// services
|
||||
import issuesServices from "lib/services/issues.services";
|
||||
import stateServices from "lib/services/state.services";
|
||||
// fetch keys
|
||||
import {
|
||||
PROJECT_ISSUES_ACTIVITY,
|
||||
@ -23,7 +22,7 @@ import {
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// layouts
|
||||
import AdminLayout from "layouts/AdminLayout";
|
||||
import AppLayout from "layouts/AppLayout";
|
||||
// components
|
||||
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
||||
import IssueCommentSection from "components/project/issues/issue-detail/comment/IssueCommentSection";
|
||||
@ -38,7 +37,7 @@ import { Spinner, TextArea } from "ui";
|
||||
import HeaderButton from "ui/HeaderButton";
|
||||
import { BreadcrumbItem, Breadcrumbs } from "ui/Breadcrumbs";
|
||||
// types
|
||||
import { IIssue, IIssueComment, IssueResponse, IState } from "types";
|
||||
import { IIssue, IIssueComment, IssueResponse } from "types";
|
||||
// icons
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
@ -208,7 +207,7 @@ const IssueDetail: NextPage = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<AppLayout noPadding={true} bg="secondary">
|
||||
<CreateUpdateIssuesModal
|
||||
isOpen={isOpen}
|
||||
setIsOpen={setIsOpen}
|
||||
@ -222,46 +221,23 @@ const IssueDetail: NextPage = () => {
|
||||
setIsOpen={setIsAddAsSubIssueOpen}
|
||||
parent={issueDetail}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between w-full mb-5">
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem
|
||||
title={`${activeProject?.name ?? "Project"} Issues`}
|
||||
link={`/projects/${activeProject?.id}/issues`}
|
||||
/>
|
||||
<BreadcrumbItem
|
||||
title={`Issue ${activeProject?.identifier ?? "Project"}-${
|
||||
issueDetail?.sequence_id ?? "..."
|
||||
} Details`}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
<div className="flex items-center gap-x-3">
|
||||
<HeaderButton
|
||||
Icon={ChevronLeftIcon}
|
||||
label="Previous"
|
||||
className={`${!prevIssue ? "cursor-not-allowed opacity-70" : ""}`}
|
||||
onClick={() => {
|
||||
if (!prevIssue) return;
|
||||
router.push(`/projects/${prevIssue.project}/issues/${prevIssue.id}`);
|
||||
}}
|
||||
/>
|
||||
<HeaderButton
|
||||
Icon={ChevronRightIcon}
|
||||
disabled={!nextIssue}
|
||||
label="Next"
|
||||
className={`${!nextIssue ? "cursor-not-allowed opacity-70" : ""}`}
|
||||
onClick={() => {
|
||||
if (!nextIssue) return;
|
||||
router.push(`/projects/${nextIssue.project}/issues/${nextIssue?.id}`);
|
||||
}}
|
||||
position="reverse"
|
||||
/>
|
||||
</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-4">
|
||||
<div className="flex gap-5">
|
||||
<div className="basis-3/4 space-y-5 p-5">
|
||||
<div className="mb-5">
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem
|
||||
title={`${activeProject?.name ?? "Project"} Issues`}
|
||||
link={`/projects/${activeProject?.id}/issues`}
|
||||
/>
|
||||
<BreadcrumbItem
|
||||
title={`Issue ${activeProject?.identifier ?? "Project"}-${
|
||||
issueDetail?.sequence_id ?? "..."
|
||||
} Details`}
|
||||
/>
|
||||
</Breadcrumbs>
|
||||
</div>
|
||||
<div className="rounded-lg">
|
||||
{issueDetail.parent !== null && issueDetail.parent !== "" ? (
|
||||
<div className="bg-gray-100 flex items-center gap-2 p-2 text-xs rounded mb-5 w-min whitespace-nowrap">
|
||||
<Link href={`/projects/${activeProject.id}/issues/${issueDetail.parent}`}>
|
||||
@ -273,7 +249,8 @@ const IssueDetail: NextPage = () => {
|
||||
}}
|
||||
/>
|
||||
<span className="flex-shrink-0 text-gray-600">
|
||||
{activeProject.identifier}-{issueDetail.sequence_id}
|
||||
{activeProject.identifier}-
|
||||
{issues?.results.find((i) => i.id === issueDetail.parent)?.sequence_id}
|
||||
</span>
|
||||
<span className="font-medium truncate">
|
||||
{issues?.results
|
||||
@ -365,7 +342,7 @@ const IssueDetail: NextPage = () => {
|
||||
</div>
|
||||
<div className="mt-2">
|
||||
{subIssues && subIssues.length > 0 ? (
|
||||
<Disclosure>
|
||||
<Disclosure defaultOpen={true}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="flex justify-between items-center">
|
||||
@ -437,7 +414,7 @@ const IssueDetail: NextPage = () => {
|
||||
{subIssues.map((subIssue) => (
|
||||
<div
|
||||
key={subIssue.id}
|
||||
className="flex justify-between items-center gap-2 p-2 hover:bg-gray-100"
|
||||
className="group flex justify-between items-center gap-2 rounded p-2 hover:bg-gray-100"
|
||||
>
|
||||
<Link href={`/projects/${activeProject.id}/issues/${subIssue.id}`}>
|
||||
<a className="flex items-center gap-2 rounded text-xs">
|
||||
@ -455,9 +432,9 @@ const IssueDetail: NextPage = () => {
|
||||
</span>
|
||||
</a>
|
||||
</Link>
|
||||
<div>
|
||||
<div className="opacity-0 group-hover:opacity-100">
|
||||
<Menu as="div" className="relative inline-block">
|
||||
<Menu.Button className="grid relative place-items-center focus:outline-none">
|
||||
<Menu.Button className="grid relative place-items-center hover:bg-gray-200 p-1 focus:outline-none">
|
||||
<EllipsisHorizontalIcon className="h-4 w-4" />
|
||||
</Menu.Button>
|
||||
|
||||
@ -553,7 +530,7 @@ const IssueDetail: NextPage = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-secondary rounded-lg p-4 space-y-5">
|
||||
<div className="bg-secondary rounded-lg space-y-5">
|
||||
<Tab.Group>
|
||||
<Tab.List className="flex gap-x-3">
|
||||
{["Comments", "Activity"].map((item) => (
|
||||
@ -589,11 +566,34 @@ const IssueDetail: NextPage = () => {
|
||||
</Tab.Group>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky top-0 h-min bg-secondary p-4 rounded-lg">
|
||||
<div className="basis-1/4 rounded-lg space-y-5 p-5 border-l">
|
||||
<div className="flex justify-end items-center gap-x-3 mb-5">
|
||||
<HeaderButton
|
||||
Icon={ChevronLeftIcon}
|
||||
label="Previous"
|
||||
className={`${!prevIssue ? "cursor-not-allowed opacity-70" : ""}`}
|
||||
onClick={() => {
|
||||
if (!prevIssue) return;
|
||||
router.push(`/projects/${prevIssue.project}/issues/${prevIssue.id}`);
|
||||
}}
|
||||
/>
|
||||
<HeaderButton
|
||||
Icon={ChevronRightIcon}
|
||||
disabled={!nextIssue}
|
||||
label="Next"
|
||||
className={`${!nextIssue ? "cursor-not-allowed opacity-70" : ""}`}
|
||||
onClick={() => {
|
||||
if (!nextIssue) return;
|
||||
router.push(`/projects/${nextIssue.project}/issues/${nextIssue?.id}`);
|
||||
}}
|
||||
position="reverse"
|
||||
/>
|
||||
</div>
|
||||
<IssueDetailSidebar
|
||||
control={control}
|
||||
issueDetail={issueDetail}
|
||||
submitChanges={submitChanges}
|
||||
watch={watch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -602,7 +602,7 @@ const IssueDetail: NextPage = () => {
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</AdminLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -18,7 +18,7 @@ import projectService from "lib/services/project.service";
|
||||
// commons
|
||||
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
|
||||
// layouts
|
||||
import AdminLayout from "layouts/AdminLayout";
|
||||
import AppLayout from "layouts/AppLayout";
|
||||
// hooks
|
||||
import useIssuesFilter from "lib/hooks/useIssuesFilter";
|
||||
// components
|
||||
@ -123,7 +123,7 @@ const ProjectIssues: NextPage = () => {
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<AppLayout>
|
||||
<CreateUpdateIssuesModal
|
||||
isOpen={isOpen && selectedIssue?.actionType !== "delete"}
|
||||
setIsOpen={setIsOpen}
|
||||
@ -203,85 +203,83 @@ const ProjectIssues: NextPage = () => {
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel className="absolute mr-5 right-1/2 z-10 mt-3 w-screen max-w-xs translate-x-1/2 transform px-2 sm:px-0 bg-white rounded-lg shadow-lg overflow-hidden">
|
||||
<div className="overflow-hidden py-8 px-4">
|
||||
<div className="relative flex flex-col gap-1 gap-y-4">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-base text-gray-600">Group by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
groupByOptions.find((option) => option.key === groupByProperty)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
>
|
||||
{groupByOptions.map((option) => (
|
||||
<Popover.Panel className="absolute mr-5 right-1/2 z-10 mt-1 w-screen max-w-xs translate-x-1/2 transform p-4 bg-white rounded-lg shadow-lg overflow-hidden">
|
||||
<div className="relative flex flex-col gap-1 gap-y-4">
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-base text-gray-600">Group by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
groupByOptions.find((option) => option.key === groupByProperty)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
>
|
||||
{groupByOptions.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-base text-gray-600">Order by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
orderByOptions.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
>
|
||||
{orderByOptions.map((option) =>
|
||||
groupByProperty === "priority" &&
|
||||
option.key === "priority" ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setGroupByProperty(option.key)}
|
||||
onClick={() => setOrderBy(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-base text-gray-600">Order by</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
orderByOptions.find((option) => option.key === orderBy)?.name ??
|
||||
"Select"
|
||||
}
|
||||
>
|
||||
{orderByOptions.map((option) =>
|
||||
groupByProperty === "priority" &&
|
||||
option.key === "priority" ? null : (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setOrderBy(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-base text-gray-600">Issue type</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
filterIssueOptions.find((option) => option.key === filterIssue)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
>
|
||||
{filterIssueOptions.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setFilterIssue(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="border-b-2"></div>
|
||||
<div className="relative flex flex-col gap-1">
|
||||
<h4 className="text-base text-gray-600">Properties</h4>
|
||||
<div>
|
||||
{Object.keys(properties).map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={`px-2 py-1 inline capitalize rounded border border-indigo-600 text-sm m-1 ${
|
||||
properties[key as keyof Properties]
|
||||
? "border-indigo-600 bg-indigo-600 text-white"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => setProperties(key as keyof Properties)}
|
||||
>
|
||||
{replaceUnderscoreIfSnakeCase(key)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<h4 className="text-base text-gray-600">Issue type</h4>
|
||||
<CustomMenu
|
||||
label={
|
||||
filterIssueOptions.find((option) => option.key === filterIssue)
|
||||
?.name ?? "Select"
|
||||
}
|
||||
>
|
||||
{filterIssueOptions.map((option) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={option.key}
|
||||
onClick={() => setFilterIssue(option.key)}
|
||||
>
|
||||
{option.name}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
<div className="border-b-2"></div>
|
||||
<div className="relative flex flex-col gap-1">
|
||||
<h4 className="text-base text-gray-600">Properties</h4>
|
||||
<div>
|
||||
{Object.keys(properties).map((key) => (
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className={`px-2 py-1 inline capitalize rounded border border-indigo-600 text-sm m-1 ${
|
||||
properties[key as keyof Properties]
|
||||
? "border-indigo-600 bg-indigo-600 text-white"
|
||||
: ""
|
||||
}`}
|
||||
onClick={() => setProperties(key as keyof Properties)}
|
||||
>
|
||||
{replaceUnderscoreIfSnakeCase(key)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -344,7 +342,7 @@ const ProjectIssues: NextPage = () => {
|
||||
</EmptySpace>
|
||||
</div>
|
||||
)}
|
||||
</AdminLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -15,7 +15,7 @@ import useToast from "lib/hooks/useToast";
|
||||
// fetching keys
|
||||
import { PROJECT_MEMBERS, PROJECT_INVITATIONS } from "constants/fetch-keys";
|
||||
// layouts
|
||||
import AdminLayout from "layouts/AdminLayout";
|
||||
import AppLayout from "layouts/AppLayout";
|
||||
// components
|
||||
import SendProjectInvitationModal from "components/project/SendProjectInvitationModal";
|
||||
import ConfirmProjectMemberRemove from "components/project/ConfirmProjectMemberRemove";
|
||||
@ -90,7 +90,7 @@ const ProjectMembers: NextPage = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<AppLayout>
|
||||
<ConfirmProjectMemberRemove
|
||||
isOpen={Boolean(selectedRemoveMember) || Boolean(selectedInviteRemoveMember)}
|
||||
onClose={() => {
|
||||
@ -309,7 +309,7 @@ const ProjectMembers: NextPage = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</AdminLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,52 +1,55 @@
|
||||
import React, { useEffect, useCallback, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
// next
|
||||
import type { NextPage } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import dynamic from "next/dynamic";
|
||||
// swr
|
||||
import { mutate } from "swr";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
import useSWR, { mutate } from "swr";
|
||||
// react hook form
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { useForm } from "react-hook-form";
|
||||
// headless ui
|
||||
import { Listbox, Tab, Transition } from "@headlessui/react";
|
||||
import { Tab } from "@headlessui/react";
|
||||
// layouts
|
||||
import AdminLayout from "layouts/AdminLayout";
|
||||
import AppLayout from "layouts/AppLayout";
|
||||
// service
|
||||
import projectServices from "lib/services/project.service";
|
||||
import workspaceService from "lib/services/workspace.service";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
import useToast from "lib/hooks/useToast";
|
||||
// fetch keys
|
||||
import { PROJECT_DETAILS, PROJECTS_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
// commons
|
||||
import { addSpaceIfCamelCase, debounce } from "constants/common";
|
||||
// components
|
||||
import CreateUpdateStateModal from "components/project/issues/BoardView/state/CreateUpdateStateModal";
|
||||
import { PROJECT_DETAILS, PROJECTS_LIST } from "constants/fetch-keys";
|
||||
// ui
|
||||
import { Spinner, Button, Input, TextArea, Select } from "ui";
|
||||
import { Spinner } from "ui";
|
||||
import { Breadcrumbs, BreadcrumbItem } from "ui/Breadcrumbs";
|
||||
// icons
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
CheckIcon,
|
||||
PlusIcon,
|
||||
PencilSquareIcon,
|
||||
RectangleGroupIcon,
|
||||
PencilIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import type { IProject, IWorkspace, WorkspaceMember } from "types";
|
||||
import type { IProject, IWorkspace } from "types";
|
||||
|
||||
const defaultValues: Partial<IProject> = {
|
||||
name: "",
|
||||
description: "",
|
||||
};
|
||||
|
||||
const NETWORK_CHOICES = { "0": "Secret", "2": "Public" };
|
||||
|
||||
const ProjectSettings: NextPage = () => {
|
||||
const GeneralSettings = dynamic(() => import("components/project/settings/GeneralSettings"), {
|
||||
loading: () => <p>Loading...</p>,
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const ControlSettings = dynamic(() => import("components/project/settings/ControlSettings"), {
|
||||
loading: () => <p>Loading...</p>,
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const StatesSettings = dynamic(() => import("components/project/settings/StatesSettings"), {
|
||||
loading: () => <p>Loading...</p>,
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const LabelsSettings = dynamic(() => import("components/project/settings/LabelsSettings"), {
|
||||
loading: () => <p>Loading...</p>,
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
@ -58,15 +61,11 @@ const ProjectSettings: NextPage = () => {
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const [isCreateStateModalOpen, setIsCreateStateModalOpen] = useState(false);
|
||||
const [selectedState, setSelectedState] = useState<string | undefined>();
|
||||
const [newGroupForm, setNewGroupForm] = useState(false);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const { projectId } = router.query;
|
||||
|
||||
const { activeWorkspace, activeProject, states } = useUser();
|
||||
const { activeWorkspace, activeProject } = useUser();
|
||||
|
||||
const { setToastAlert } = useToast();
|
||||
|
||||
@ -77,17 +76,6 @@ const ProjectSettings: NextPage = () => {
|
||||
: null
|
||||
);
|
||||
|
||||
const { data: people } = useSWR<WorkspaceMember[]>(
|
||||
activeWorkspace ? WORKSPACE_MEMBERS(activeWorkspace.slug) : null,
|
||||
activeWorkspace ? () => workspaceService.workspaceMembers(activeWorkspace.slug) : null,
|
||||
{
|
||||
onErrorRetry(err, _, __, revalidate, revalidateOpts) {
|
||||
if (err?.status === 403) return;
|
||||
setTimeout(() => revalidate(revalidateOpts), 5000);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
projectDetails &&
|
||||
reset({
|
||||
@ -136,28 +124,9 @@ const ProjectSettings: NextPage = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const checkIdentifier = (slug: string, value: string) => {
|
||||
projectServices.checkProjectIdentifierAvailability(slug, value).then((response) => {
|
||||
console.log(response);
|
||||
if (response.exists) setError("identifier", { message: "Identifier already exists" });
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const checkIdentifierAvailability = useCallback(debounce(checkIdentifier, 1500), []);
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<AppLayout>
|
||||
<div className="space-y-5 mb-5">
|
||||
<CreateUpdateStateModal
|
||||
isOpen={isCreateStateModalOpen || Boolean(selectedState)}
|
||||
handleClose={() => {
|
||||
setSelectedState(undefined);
|
||||
setIsCreateStateModalOpen(false);
|
||||
}}
|
||||
projectId={projectId as string}
|
||||
data={selectedState ? states?.find((state) => state.id === selectedState) : undefined}
|
||||
/>
|
||||
<Breadcrumbs>
|
||||
<BreadcrumbItem title="Projects" link="/projects" />
|
||||
<BreadcrumbItem title={`${activeProject?.name ?? "Project"} Settings`} />
|
||||
@ -165,385 +134,50 @@ const ProjectSettings: NextPage = () => {
|
||||
</div>
|
||||
{projectDetails ? (
|
||||
<div className="space-y-3">
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="mt-3">
|
||||
<Tab.Group>
|
||||
<Tab.List className="flex items-center gap-x-4 gap-y-2 flex-wrap">
|
||||
{["General", "Control", "States", "Labels"].map((tab, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
className={({ selected }) =>
|
||||
`px-6 py-2 border-2 border-theme hover:bg-theme hover:text-white text-xs font-medium rounded-md duration-300 ${
|
||||
selected ? "bg-theme text-white" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{tab}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels className="mt-8">
|
||||
<Tab.Group>
|
||||
<Tab.List className="flex items-center gap-x-4 gap-y-2 flex-wrap mb-8">
|
||||
{["General", "Control", "States", "Labels"].map((tab, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
className={({ selected }) =>
|
||||
`px-6 py-2 border-2 border-theme hover:bg-theme hover:text-white text-xs font-medium rounded-md outline-none duration-300 ${
|
||||
selected ? "bg-theme text-white" : ""
|
||||
}`
|
||||
}
|
||||
>
|
||||
{tab}
|
||||
</Tab>
|
||||
))}
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<Tab.Panel>
|
||||
<section className="space-y-5">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900">General</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
This information will be displayed to every member of the project.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="col-span-2">
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
error={errors.name}
|
||||
register={register}
|
||||
placeholder="Project Name"
|
||||
label="Name"
|
||||
validations={{
|
||||
required: "Name is required",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Select
|
||||
name="network"
|
||||
id="network"
|
||||
options={Object.keys(NETWORK_CHOICES).map((key) => ({
|
||||
value: key,
|
||||
label: NETWORK_CHOICES[key as keyof typeof NETWORK_CHOICES],
|
||||
}))}
|
||||
label="Network"
|
||||
register={register}
|
||||
validations={{
|
||||
required: "Network is required",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
id="identifier"
|
||||
name="identifier"
|
||||
error={errors.identifier}
|
||||
register={register}
|
||||
placeholder="Enter identifier"
|
||||
label="Identifier"
|
||||
onChange={(e: any) => {
|
||||
if (!activeWorkspace || !e.target.value) return;
|
||||
checkIdentifierAvailability(activeWorkspace.slug, e.target.value);
|
||||
}}
|
||||
validations={{
|
||||
required: "Identifier is required",
|
||||
minLength: {
|
||||
value: 1,
|
||||
message: "Identifier must at least be of 1 character",
|
||||
},
|
||||
maxLength: {
|
||||
value: 9,
|
||||
message: "Identifier must at most be of 9 characters",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<TextArea
|
||||
id="description"
|
||||
name="description"
|
||||
error={errors.description}
|
||||
register={register}
|
||||
label="Description"
|
||||
placeholder="Enter project description"
|
||||
validations={{
|
||||
required: "Description is required",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Updating Project..." : "Update Project"}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
<GeneralSettings
|
||||
register={register}
|
||||
errors={errors}
|
||||
setError={setError}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<section className="space-y-5">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900">Control</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">Set the control for the project.</p>
|
||||
</div>
|
||||
<div className="flex justify-between gap-3">
|
||||
<div className="w-full md:w-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="project_lead"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Listbox value={value} onChange={onChange}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Listbox.Label>
|
||||
<div className="text-gray-500 mb-2">Project Lead</div>
|
||||
</Listbox.Label>
|
||||
<div className="relative">
|
||||
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<span className="block truncate">
|
||||
{people?.find((person) => person.member.id === value)
|
||||
?.member.first_name ?? "Select Lead"}
|
||||
</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<ChevronDownIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</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 z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
||||
{people?.map((person) => (
|
||||
<Listbox.Option
|
||||
key={person.id}
|
||||
className={({ active }) =>
|
||||
`${
|
||||
active ? "text-white bg-theme" : "text-gray-900"
|
||||
} cursor-default select-none relative py-2 pl-3 pr-9`
|
||||
}
|
||||
value={person.member.id}
|
||||
>
|
||||
{({ selected, active }) => (
|
||||
<>
|
||||
<span
|
||||
className={`${
|
||||
selected ? "font-semibold" : "font-normal"
|
||||
} block truncate`}
|
||||
>
|
||||
{person.member.first_name}
|
||||
</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.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-1/2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="default_assignee"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Listbox value={value} onChange={onChange}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Listbox.Label>
|
||||
<div className="text-gray-500 mb-2">Default Assignee</div>
|
||||
</Listbox.Label>
|
||||
<div className="relative">
|
||||
<Listbox.Button className="bg-white relative w-full border border-gray-300 rounded-md shadow-sm pl-3 pr-10 py-2 text-left cursor-default focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm">
|
||||
<span className="block truncate">
|
||||
{people?.find((p) => p.member.id === value)?.member
|
||||
.first_name ?? "Select Default Assignee"}
|
||||
</span>
|
||||
<span className="absolute inset-y-0 right-0 flex items-center pr-2 pointer-events-none">
|
||||
<ChevronDownIcon
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</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 z-10 mt-1 w-full bg-white shadow-lg max-h-60 rounded-md py-1 text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm">
|
||||
{people?.map((person) => (
|
||||
<Listbox.Option
|
||||
key={person.id}
|
||||
className={({ active }) =>
|
||||
`${
|
||||
active ? "text-white bg-theme" : "text-gray-900"
|
||||
} cursor-default select-none relative py-2 pl-3 pr-9`
|
||||
}
|
||||
value={person.member.id}
|
||||
>
|
||||
{({ selected, active }) => (
|
||||
<>
|
||||
<span
|
||||
className={`${
|
||||
selected ? "font-semibold" : "font-normal"
|
||||
} block truncate`}
|
||||
>
|
||||
{person.member.first_name}
|
||||
</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.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Updating Project..." : "Update Project"}
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
<ControlSettings control={control} isSubmitting={isSubmitting} />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<section className="space-y-5">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900">State</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Manage the state of this project.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-between gap-3">
|
||||
<div className="w-full space-y-5">
|
||||
{states?.map((state) => (
|
||||
<div
|
||||
key={state.id}
|
||||
className="bg-white px-4 py-2 rounded flex justify-between items-center"
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{
|
||||
backgroundColor: state.color,
|
||||
}}
|
||||
></div>
|
||||
<h4>{addSpaceIfCamelCase(state.name)}</h4>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" onClick={() => setSelectedState(state.id)}>
|
||||
<PencilSquareIcon className="h-5 w-5 text-gray-400" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
className="flex items-center gap-x-1"
|
||||
onClick={() => setIsCreateStateModalOpen(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
<span>Add State</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<section className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900">Labels</h3>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
Manage the labels of this project.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className="flex items-center gap-x-1"
|
||||
onClick={() => setNewGroupForm(true)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
New group
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-5">
|
||||
<div
|
||||
className={`bg-white px-4 py-2 flex items-center gap-2 ${
|
||||
newGroupForm ? "" : "hidden"
|
||||
}`}
|
||||
>
|
||||
<Input type="text" name="groupName" />
|
||||
<Button
|
||||
type="button"
|
||||
theme="secondary"
|
||||
onClick={() => setNewGroupForm(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="button">Save</Button>
|
||||
</div>
|
||||
{["", ""].map((group, index) => (
|
||||
<div key={index} className="bg-white p-4 text-gray-900 rounded-md">
|
||||
<h3 className="font-medium leading-5 flex items-center gap-2">
|
||||
<RectangleGroupIcon className="h-5 w-5" />
|
||||
This is the label group title
|
||||
</h3>
|
||||
<div className="pl-5 mt-4">
|
||||
<div className="group text-sm flex justify-between items-center p-2 hover:bg-gray-100 rounded">
|
||||
<h5 className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-red-600 rounded-full"></div>
|
||||
This is the label title
|
||||
</h5>
|
||||
<div className="hidden group-hover:block">
|
||||
<PencilIcon className="h-3 w-3" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</form>
|
||||
</form>
|
||||
<Tab.Panel>
|
||||
<StatesSettings projectId={projectId} />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<LabelsSettings />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full w-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</AdminLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -4,7 +4,7 @@ import type { NextPage } from "next";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// layouts
|
||||
import AdminLayout from "layouts/AdminLayout";
|
||||
import AppLayout from "layouts/AppLayout";
|
||||
// components
|
||||
import CreateProjectModal from "components/project/CreateProjectModal";
|
||||
import ConfirmProjectDeletion from "components/project/ConfirmProjectDeletion";
|
||||
@ -61,7 +61,7 @@ const Projects: NextPage = () => {
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<AppLayout>
|
||||
<CreateProjectModal isOpen={isOpen && !deleteProject} setIsOpen={setIsOpen} />
|
||||
<ConfirmProjectDeletion
|
||||
isOpen={isOpen && !!deleteProject}
|
||||
@ -125,7 +125,7 @@ const Projects: NextPage = () => {
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</AdminLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -4,7 +4,7 @@ import Link from "next/link";
|
||||
// react
|
||||
import React from "react";
|
||||
// layouts
|
||||
import AdminLayout from "layouts/AdminLayout";
|
||||
import AppLayout from "layouts/AppLayout";
|
||||
// swr
|
||||
import useSWR from "swr";
|
||||
// hooks
|
||||
@ -46,7 +46,7 @@ const Workspace: NextPage = () => {
|
||||
const hours = new Date().getHours();
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
<AppLayout>
|
||||
<div className="h-full w-full space-y-5">
|
||||
{user ? (
|
||||
<div className="font-medium text-2xl">
|
||||
@ -167,7 +167,7 @@ const Workspace: NextPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -17,7 +17,7 @@ import { WORKSPACE_INVITATIONS, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||
// hoc
|
||||
import withAuthWrapper from "lib/hoc/withAuthWrapper";
|
||||
// layouts
|
||||
import AdminLayout from "layouts/AdminLayout";
|
||||
import AppLayout from "layouts/AppLayout";
|
||||
// components
|
||||
import SendWorkspaceInvitationModal from "components/workspace/SendWorkspaceInvitationModal";
|
||||
import ConfirmWorkspaceMemberRemove from "components/workspace/ConfirmWorkspaceMemberRemove";
|
||||
@ -71,7 +71,7 @@ const WorkspaceInvite: NextPage = () => {
|
||||
];
|
||||
|
||||
return (
|
||||
<AdminLayout
|
||||
<AppLayout
|
||||
meta={{
|
||||
title: "Plane - Workspace Invite",
|
||||
}}
|
||||
@ -299,8 +299,8 @@ const WorkspaceInvite: NextPage = () => {
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</AdminLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default withAuthWrapper(WorkspaceInvite);
|
||||
export default withAuthWrapper(WorkspaceInvite);
|
||||
|
@ -9,7 +9,7 @@ import Dropzone from "react-dropzone";
|
||||
import workspaceService from "lib/services/workspace.service";
|
||||
import fileServices from "lib/services/file.services";
|
||||
// layouts
|
||||
import AdminLayout from "layouts/AdminLayout";
|
||||
import AppLayout from "layouts/AppLayout";
|
||||
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
@ -83,7 +83,7 @@ const WorkspaceSettings = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<AdminLayout
|
||||
<AppLayout
|
||||
meta={{
|
||||
title: "Plane - Workspace Settings",
|
||||
}}
|
||||
@ -228,7 +228,7 @@ const WorkspaceSettings = () => {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@200;300;400;500;600;700;800&display=swap");
|
||||
|
||||
@import url("https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@48,400,0,0");
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
4
apps/app/types/issues.d.ts
vendored
4
apps/app/types/issues.d.ts
vendored
@ -27,7 +27,7 @@ export interface IIssue {
|
||||
name: string;
|
||||
description: string;
|
||||
priority: string | null;
|
||||
start_date: null;
|
||||
start_date: string | null;
|
||||
target_date: string | null;
|
||||
sequence_id: number;
|
||||
attachments: any[];
|
||||
@ -102,7 +102,7 @@ export type Properties = {
|
||||
assignee: boolean;
|
||||
priority: boolean;
|
||||
start_date: boolean;
|
||||
due_date: boolean;
|
||||
target_date: boolean;
|
||||
cycle: boolean;
|
||||
};
|
||||
|
||||
|
@ -9,12 +9,7 @@ type EmptySpaceProps = {
|
||||
title: string;
|
||||
description: string;
|
||||
children: any;
|
||||
Icon?: (
|
||||
props: React.SVGProps<SVGSVGElement> & {
|
||||
title?: string | undefined;
|
||||
titleId?: string | undefined;
|
||||
}
|
||||
) => JSX.Element;
|
||||
Icon?: (props: any) => JSX.Element;
|
||||
link?: { text: string; href: string };
|
||||
};
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user