dev: profile page, issue details page design
This commit is contained in:
parent
2a57b111f0
commit
dbf2a138b3
40 changed files with 2301 additions and 2034 deletions
|
|
@ -36,6 +36,7 @@ type Props = {
|
|||
>;
|
||||
bgColor?: string;
|
||||
stateId?: string;
|
||||
createdBy?: string;
|
||||
};
|
||||
|
||||
const SingleBoard: React.FC<Props> = ({
|
||||
|
|
@ -48,6 +49,7 @@ const SingleBoard: React.FC<Props> = ({
|
|||
setPreloadedData,
|
||||
bgColor = "#0f2b16",
|
||||
stateId,
|
||||
createdBy,
|
||||
}) => {
|
||||
// Collapse/Expand
|
||||
const [show, setState] = useState<any>(true);
|
||||
|
|
@ -118,6 +120,8 @@ const SingleBoard: React.FC<Props> = ({
|
|||
>
|
||||
{groupTitle === null || groupTitle === "null"
|
||||
? "None"
|
||||
: createdBy
|
||||
? createdBy
|
||||
: addSpaceIfCamelCase(groupTitle)}
|
||||
</h2>
|
||||
<span className="text-gray-500 text-sm ml-0.5">
|
||||
|
|
@ -280,7 +284,7 @@ const SingleBoard: React.FC<Props> = ({
|
|||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={`h-5 w-5 bg-gray-500 text-white border-2 border-white grid place-items-center rounded-full`}
|
||||
className={`h-5 w-5 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
|
||||
>
|
||||
{assignee.first_name.charAt(0)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,9 +18,9 @@ import SingleBoard from "components/project/issues/BoardView/SingleBoard";
|
|||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
||||
// ui
|
||||
import { Spinner, Button } from "ui";
|
||||
import { Spinner } from "ui";
|
||||
// types
|
||||
import type { IState, IIssue, Properties, NestedKeyOf } from "types";
|
||||
import type { IState, IIssue, Properties, NestedKeyOf, ProjectMember } from "types";
|
||||
|
||||
type Props = {
|
||||
properties: Properties;
|
||||
|
|
@ -28,9 +28,10 @@ type Props = {
|
|||
groupedByIssues: {
|
||||
[key: string]: IIssue[];
|
||||
};
|
||||
members: ProjectMember[] | undefined;
|
||||
};
|
||||
|
||||
const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues }) => {
|
||||
const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues, members }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [isIssueOpen, setIsIssueOpen] = useState(false);
|
||||
|
|
@ -164,7 +165,7 @@ const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues
|
|||
/>
|
||||
{groupedByIssues ? (
|
||||
groupedByIssues ? (
|
||||
<div className="h-full w-full">
|
||||
<div className="w-full" style={{ height: "calc(82vh - 1.5rem)" }}>
|
||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||
<div className="h-full w-full overflow-hidden">
|
||||
<StrictModeDroppable droppableId="state" type="state" direction="horizontal">
|
||||
|
|
@ -180,6 +181,12 @@ const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues
|
|||
key={singleGroup}
|
||||
selectedGroup={selectedGroup}
|
||||
groupTitle={singleGroup}
|
||||
createdBy={
|
||||
members
|
||||
? members?.find((m) => m.member.id === singleGroup)?.member
|
||||
.first_name
|
||||
: undefined
|
||||
}
|
||||
groupedByIssues={groupedByIssues}
|
||||
index={index}
|
||||
setIsIssueOpen={setIsIssueOpen}
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ const SelectAssignee: React.FC<Props> = ({ control }) => {
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<div className="p-1">
|
||||
{people?.map((person) => (
|
||||
<Listbox.Option
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ const SelectSprint: React.FC<Props> = ({ control, setIsOpen }) => {
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<div className="p-1">
|
||||
{sprints?.map((sprint) => (
|
||||
<Listbox.Option
|
||||
|
|
@ -63,16 +63,6 @@ const SelectSprint: React.FC<Props> = ({ control, setIsOpen }) => {
|
|||
<span className={`block ${selected && "font-semibold"}`}>
|
||||
{sprint.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>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ const SelectLabels: React.FC<Props> = ({ control }) => {
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute mt-1 bg-white shadow-lg max-h-28 rounded-md text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<div className="p-1">
|
||||
{issueLabels?.map((label) => (
|
||||
<Listbox.Option
|
||||
|
|
@ -121,18 +121,6 @@ const SelectLabels: React.FC<Props> = ({ control }) => {
|
|||
>
|
||||
{label.name}
|
||||
</span>
|
||||
|
||||
{selected ? (
|
||||
<span
|
||||
className={`absolute inset-y-0 right-0 flex items-center pr-4 ${
|
||||
active || (value ?? []).some((i) => i === label.id)
|
||||
? "text-white"
|
||||
: "text-indigo-600"
|
||||
}`}
|
||||
>
|
||||
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</Listbox.Option>
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ const SelectParent: React.FC<Props> = ({ control }) => {
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute mt-1 bg-white shadow-lg max-h-28 max-w-[15rem] rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 max-w-[15rem] rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<div className="p-1">
|
||||
{projectIssues?.results?.map((issue) => (
|
||||
<Listbox.Option
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ const SelectPriority: React.FC<Props> = ({ control }) => {
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute mt-1 w-full bg-white shadow-lg max-h-28 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-xs">
|
||||
<Listbox.Options className="absolute z-10 mt-1 w-full w-[5rem] bg-white shadow-lg max-h-28 rounded-md py-1 ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none text-xs">
|
||||
<div className="p-1">
|
||||
{PRIORITIES.map((priority) => (
|
||||
<Listbox.Option
|
||||
|
|
@ -55,21 +55,11 @@ const SelectPriority: React.FC<Props> = ({ control }) => {
|
|||
<>
|
||||
<span
|
||||
className={`block capitalize ${
|
||||
selected ? "font-semibold" : "font-normal"
|
||||
selected ? "font-medium" : "font-normal"
|
||||
}`}
|
||||
>
|
||||
{priority}
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ const SelectProject: React.FC<Props> = ({ control }) => {
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<div className="p-1">
|
||||
{projects ? (
|
||||
projects.length > 0 ? (
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ const SelectState: React.FC<Props> = ({ control, data, setIsOpen }) => {
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<Listbox.Options className="absolute z-10 mt-1 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<div className="p-1">
|
||||
{states ? (
|
||||
states.filter((i) => i.id !== data?.id).length > 0 ? (
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||
setToastAlert({
|
||||
title: "Success",
|
||||
type: "success",
|
||||
message: "Issue added to sprint successfully",
|
||||
message: "Issue added to cycle successfully",
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
|
|
@ -325,7 +325,7 @@ const CreateUpdateIssuesModal: React.FC<Props> = ({
|
|||
register={register}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center flex-wrap gap-2">
|
||||
<SelectState control={control} setIsOpen={setIsStateModalOpen} />
|
||||
<SelectCycles control={control} setIsOpen={setIsCycleModalOpen} />
|
||||
<SelectPriority control={control} />
|
||||
|
|
|
|||
|
|
@ -9,7 +9,15 @@ 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,
|
||||
IState,
|
||||
NestedKeyOf,
|
||||
ProjectMember,
|
||||
Properties,
|
||||
WorkspaceMember,
|
||||
} from "types";
|
||||
// hooks
|
||||
import useUser from "lib/hooks/useUser";
|
||||
// fetch keys
|
||||
|
|
@ -32,6 +40,7 @@ type Props = {
|
|||
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||
setSelectedIssue: any;
|
||||
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||
members: ProjectMember[] | undefined;
|
||||
};
|
||||
|
||||
const PRIORITIES = ["high", "medium", "low"];
|
||||
|
|
@ -42,6 +51,7 @@ const ListView: React.FC<Props> = ({
|
|||
selectedGroup,
|
||||
setSelectedIssue,
|
||||
handleDeleteIssue,
|
||||
members,
|
||||
}) => {
|
||||
const { activeWorkspace, activeProject, states } = useUser();
|
||||
|
||||
|
|
@ -71,255 +81,185 @@ const ListView: React.FC<Props> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="mt-4 flex flex-col">
|
||||
<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>
|
||||
{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"
|
||||
>
|
||||
{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 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>
|
||||
{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"
|
||||
>
|
||||
{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
|
||||
);
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={issue.id}
|
||||
className={classNames(
|
||||
index === 0 ? "border-gray-300" : "border-gray-200",
|
||||
"border-t"
|
||||
)}
|
||||
>
|
||||
{Object.keys(properties).map(
|
||||
(key) =>
|
||||
properties[key as keyof Properties] && (
|
||||
<td
|
||||
key={key}
|
||||
className="px-3 py-4 text-sm font-medium text-gray-900 relative"
|
||||
>
|
||||
{(key as keyof Properties) === "name" ? (
|
||||
<p className="w-[15rem]">
|
||||
<Link
|
||||
href={`/projects/${issue.project}/issues/${issue.id}`}
|
||||
>
|
||||
<a className="hover:text-theme duration-300">
|
||||
{issue.name}
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
) : (key as keyof Properties) === "key" ? (
|
||||
<p className="text-xs whitespace-nowrap">
|
||||
{activeProject?.identifier}-{issue.sequence_id}
|
||||
</p>
|
||||
) : (key as keyof Properties) === "description" ? (
|
||||
<p className="truncate text-xs max-w-[15rem]">
|
||||
{issue.description}
|
||||
</p>
|
||||
) : (key as keyof Properties) === "priority" ? (
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.priority}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ priority: data }, issue.id);
|
||||
}}
|
||||
className="flex-shrink-0"
|
||||
return (
|
||||
<tr
|
||||
key={issue.id}
|
||||
className={classNames(
|
||||
index === 0 ? "border-gray-300" : "border-gray-200",
|
||||
"border-t"
|
||||
)}
|
||||
>
|
||||
{Object.keys(properties).map(
|
||||
(key) =>
|
||||
properties[key as keyof Properties] && (
|
||||
<td
|
||||
key={key}
|
||||
className="px-3 py-4 text-sm font-medium text-gray-900 relative"
|
||||
>
|
||||
{(key as keyof Properties) === "name" ? (
|
||||
<p className="w-[15rem]">
|
||||
<Link
|
||||
href={`/projects/${issue.project}/issues/${issue.id}`}
|
||||
>
|
||||
{({ 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"
|
||||
<a className="hover:text-theme duration-300">
|
||||
{issue.name}
|
||||
</a>
|
||||
</Link>
|
||||
</p>
|
||||
) : (key as keyof Properties) === "key" ? (
|
||||
<p className="text-xs whitespace-nowrap">
|
||||
{activeProject?.identifier}-{issue.sequence_id}
|
||||
</p>
|
||||
) : (key as keyof Properties) === "description" ? (
|
||||
<p className="truncate text-xs max-w-[15rem]">
|
||||
{issue.description}
|
||||
</p>
|
||||
) : (key as keyof Properties) === "priority" ? (
|
||||
<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"
|
||||
)
|
||||
}
|
||||
value={priority}
|
||||
>
|
||||
{priority}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
) : (key as keyof Properties) === "assignee" ? (
|
||||
<>
|
||||
<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>
|
||||
{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">
|
||||
{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 ${
|
||||
assignees.includes(
|
||||
person.member.email
|
||||
)
|
||||
? "font-medium"
|
||||
: "font-normal"
|
||||
}`}
|
||||
>
|
||||
{person.member.email}
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
</>
|
||||
) : (key as keyof Properties) === "state" ? (
|
||||
<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>
|
||||
) : (key as keyof Properties) === "assignee" ? (
|
||||
<>
|
||||
<Listbox
|
||||
as="div"
|
||||
value={issue.state}
|
||||
onChange={(data: string) => {
|
||||
partialUpdateIssue({ state: data }, issue.id);
|
||||
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="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`,
|
||||
<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>;
|
||||
}}
|
||||
>
|
||||
<span
|
||||
className={classNames(
|
||||
issue.state ? "" : "text-gray-900",
|
||||
"hidden capitalize sm:block w-16"
|
||||
)}
|
||||
>
|
||||
{addSpaceIfCamelCase(issue.state_detail.name)}
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
|
|
@ -330,18 +270,26 @@ const ListView: React.FC<Props> = ({
|
|||
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) => (
|
||||
{people?.map((person) => (
|
||||
<Listbox.Option
|
||||
key={state.id}
|
||||
key={person.id}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
active ? "bg-indigo-50" : "bg-white",
|
||||
"cursor-pointer select-none px-3 py-2"
|
||||
)
|
||||
}
|
||||
value={state.id}
|
||||
value={person.member.id}
|
||||
>
|
||||
{addSpaceIfCamelCase(state.name)}
|
||||
<div
|
||||
className={`flex items-center ${
|
||||
assignees.includes(person.member.email)
|
||||
? "font-medium"
|
||||
: "font-normal"
|
||||
}`}
|
||||
>
|
||||
{person.member.email}
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
|
|
@ -350,58 +298,115 @@ const ListView: React.FC<Props> = ({
|
|||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
) : (key as keyof Properties) === "children" ? (
|
||||
<p>No children.</p>
|
||||
) : (key as keyof Properties) === "target_date" ? (
|
||||
<p className="whitespace-nowrap">
|
||||
{issue.target_date
|
||||
? renderShortNumericDateFormat(issue.target_date)
|
||||
: "-"}
|
||||
</p>
|
||||
) : (
|
||||
<p className="capitalize text-sm">
|
||||
{issue[key as keyof IIssue] ??
|
||||
(issue[key as keyof IIssue] as any)?.name ??
|
||||
"None"}
|
||||
</p>
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
)}
|
||||
<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>
|
||||
</div>
|
||||
</>
|
||||
) : (key as keyof Properties) === "state" ? (
|
||||
<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>
|
||||
) : (key as keyof Properties) === "children" ? (
|
||||
<p>No children.</p>
|
||||
) : (key as keyof Properties) === "target_date" ? (
|
||||
<p className="whitespace-nowrap">
|
||||
{issue.target_date
|
||||
? renderShortNumericDateFormat(issue.target_date)
|
||||
: "-"}
|
||||
</p>
|
||||
) : (
|
||||
<p className="capitalize text-sm">
|
||||
{issue[key as keyof IIssue] ??
|
||||
(issue[key as keyof IIssue] as any)?.name ??
|
||||
"None"}
|
||||
</p>
|
||||
)}
|
||||
</td>
|
||||
)
|
||||
)}
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -19,11 +19,20 @@ import {
|
|||
PROJECT_ISSUE_LABELS,
|
||||
} from "constants/fetch-keys";
|
||||
// commons
|
||||
import { classNames } from "constants/common";
|
||||
import { classNames, copyTextToClipboard } from "constants/common";
|
||||
// ui
|
||||
import { Input, Button } from "ui";
|
||||
// icons
|
||||
import { Bars3BottomRightIcon, PlusIcon, UserIcon, TagIcon } from "@heroicons/react/24/outline";
|
||||
import {
|
||||
UserIcon,
|
||||
TagIcon,
|
||||
UserGroupIcon,
|
||||
ChevronDownIcon,
|
||||
Squares2X2Icon,
|
||||
ChartBarIcon,
|
||||
ClipboardDocumentIcon,
|
||||
LinkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
// types
|
||||
import type { Control } from "react-hook-form";
|
||||
import type { IIssue, IIssueLabels, IssueResponse, IState, WorkspaceMember } from "types";
|
||||
|
|
@ -31,6 +40,7 @@ import type { IIssue, IIssueLabels, IssueResponse, IState, WorkspaceMember } fro
|
|||
type Props = {
|
||||
control: Control<IIssue, any>;
|
||||
submitChanges: (formData: Partial<IIssue>) => void;
|
||||
issueDetail: IIssue | undefined;
|
||||
};
|
||||
|
||||
const PRIORITIES = ["high", "medium", "low"];
|
||||
|
|
@ -39,7 +49,7 @@ const defaultValues: Partial<IIssueLabels> = {
|
|||
name: "",
|
||||
};
|
||||
|
||||
const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
|
||||
const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges, issueDetail }) => {
|
||||
const { activeWorkspace, activeProject } = useUser();
|
||||
|
||||
const { data: states } = useSWR<IState[]>(
|
||||
|
|
@ -90,65 +100,88 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
});
|
||||
};
|
||||
|
||||
const sidebarOptions = [
|
||||
{
|
||||
label: "Priority",
|
||||
name: "priority",
|
||||
canSelectMultipleOptions: false,
|
||||
icon: ChartBarIcon,
|
||||
options: PRIORITIES.map((property) => ({
|
||||
label: property,
|
||||
value: property,
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: "Status",
|
||||
name: "state",
|
||||
canSelectMultipleOptions: false,
|
||||
icon: Squares2X2Icon,
|
||||
options: states?.map((state) => ({
|
||||
label: state.name,
|
||||
value: state.id,
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: "Assignees",
|
||||
name: "assignees_list",
|
||||
canSelectMultipleOptions: true,
|
||||
icon: UserGroupIcon,
|
||||
options: people?.map((person) => ({
|
||||
label: person.member.first_name,
|
||||
value: person.member.id,
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: "Blocker",
|
||||
name: "blockers_list",
|
||||
canSelectMultipleOptions: true,
|
||||
icon: UserIcon,
|
||||
options: projectIssues?.results?.map((issue) => ({
|
||||
label: issue.name,
|
||||
value: issue.id,
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: "Blocked",
|
||||
name: "blocked_list",
|
||||
canSelectMultipleOptions: true,
|
||||
icon: UserIcon,
|
||||
options: projectIssues?.results?.map((issue) => ({
|
||||
label: issue.name,
|
||||
value: issue.id,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<div className="h-full w-full">
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col gap-y-4">
|
||||
{[
|
||||
{
|
||||
label: "Priority",
|
||||
name: "priority",
|
||||
canSelectMultipleOptions: false,
|
||||
icon: Bars3BottomRightIcon,
|
||||
options: PRIORITIES.map((property) => ({
|
||||
label: property,
|
||||
value: property,
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: "Status",
|
||||
name: "state",
|
||||
canSelectMultipleOptions: false,
|
||||
icon: Bars3BottomRightIcon,
|
||||
options: states?.map((state) => ({
|
||||
label: state.name,
|
||||
value: state.id,
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: "Assignees",
|
||||
name: "assignees_list",
|
||||
canSelectMultipleOptions: true,
|
||||
icon: UserIcon,
|
||||
options: people?.map((person) => ({
|
||||
label: person.member.first_name,
|
||||
value: person.member.id,
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: "Blocker",
|
||||
name: "blockers_list",
|
||||
canSelectMultipleOptions: true,
|
||||
icon: UserIcon,
|
||||
options: projectIssues?.results?.map((issue) => ({
|
||||
label: issue.name,
|
||||
value: issue.id,
|
||||
})),
|
||||
},
|
||||
{
|
||||
label: "Blocked",
|
||||
name: "blocked_list",
|
||||
canSelectMultipleOptions: true,
|
||||
icon: UserIcon,
|
||||
options: projectIssues?.results?.map((issue) => ({
|
||||
label: issue.name,
|
||||
value: issue.id,
|
||||
})),
|
||||
},
|
||||
].map((item) => (
|
||||
<div className="flex items-center gap-x-2" key={item.label}>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<item.icon className="w-5 h-5 text-gray-500" />
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900">Quick Actions</h3>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 hover:bg-gray-100 border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
|
||||
onClick={() =>
|
||||
copyTextToClipboard(
|
||||
`https://app.plane.so/projects/${activeProject?.id}/issues/${issueDetail?.id}`
|
||||
)
|
||||
}
|
||||
>
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 hover:bg-gray-100 border rounded-md shadow-sm focus:outline-none focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 duration-300"
|
||||
onClick={() => copyTextToClipboard(`${issueDetail?.id}`)}
|
||||
>
|
||||
<ClipboardDocumentIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
{sidebarOptions.map((item) => (
|
||||
<div className="flex items-center justify-between gap-x-2" key={item.label}>
|
||||
<div className="flex items-center gap-x-2 text-sm">
|
||||
<item.icon className="h-4 w-4" />
|
||||
<p>{item.label}</p>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -160,68 +193,61 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
as="div"
|
||||
value={value}
|
||||
multiple={item.canSelectMultipleOptions}
|
||||
onChange={(value) => submitChanges({ [item.name]: value })}
|
||||
onChange={(value: any) => submitChanges({ [item.name]: value })}
|
||||
className="flex-shrink-0"
|
||||
>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<Listbox.Label className="sr-only">{item.label}</Listbox.Label>
|
||||
<div className="relative">
|
||||
<Listbox.Button className="relative inline-flex items-center whitespace-nowrap rounded-full bg-gray-50 py-2 px-2 text-sm font-medium text-gray-500 hover:bg-gray-100 sm:px-3 border border-dashed">
|
||||
<PlusIcon
|
||||
className="h-5 w-5 flex-shrink-0 text-gray-300 sm:-ml-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span
|
||||
className={classNames(
|
||||
value ? "" : "text-gray-900",
|
||||
"hidden truncate capitalize sm:ml-2 sm:block w-16"
|
||||
)}
|
||||
>
|
||||
{value
|
||||
? Array.isArray(value)
|
||||
? value
|
||||
.map(
|
||||
(i: any) =>
|
||||
item.options?.find((option) => option.value === i)
|
||||
?.label
|
||||
)
|
||||
.join(", ") || `Select ${item.label}`
|
||||
: item.options?.find((option) => option.value === value)?.label
|
||||
: `Select ${item.label}`}
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
<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">
|
||||
<span
|
||||
className={classNames(
|
||||
value ? "" : "text-gray-900",
|
||||
"hidden truncate sm:block w-16 text-left",
|
||||
item.label === "Priority" ? "capitalize" : ""
|
||||
)}
|
||||
>
|
||||
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-56 w-52 overflow-auto rounded-lg bg-white py-3 text-base shadow ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
{value
|
||||
? Array.isArray(value)
|
||||
? value
|
||||
.map(
|
||||
(i: any) =>
|
||||
item.options?.find((option) => option.value === i)?.label
|
||||
)
|
||||
.join(", ") || item.label
|
||||
: item.options?.find((option) => option.value === value)?.label
|
||||
: "None"}
|
||||
</span>
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={React.Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<div className="p-1">
|
||||
{item.options?.map((option) => (
|
||||
<Listbox.Option
|
||||
key={option.value}
|
||||
className={({ active, selected }) =>
|
||||
classNames(
|
||||
active || selected ? "bg-indigo-50" : "bg-white",
|
||||
"relative cursor-default select-none py-2 px-3"
|
||||
)
|
||||
`${
|
||||
active || selected ? "text-white bg-theme" : "text-gray-900"
|
||||
} ${
|
||||
item.label === "Priority" && "capitalize"
|
||||
} cursor-pointer select-none relative p-2 rounded-md truncate`
|
||||
}
|
||||
value={option.value}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="ml-3 block capitalize font-medium">
|
||||
{option.label}
|
||||
</span>
|
||||
</div>
|
||||
{option.label}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
|
|
@ -230,11 +256,11 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
</div>
|
||||
))}
|
||||
<div>
|
||||
<form className="flex" onSubmit={handleSubmit(onSubmit)}>
|
||||
<form className="flex items-center gap-x-2" onSubmit={handleSubmit(onSubmit)}>
|
||||
<Input
|
||||
id="name"
|
||||
name="name"
|
||||
placeholder="Add label"
|
||||
placeholder="Add new label"
|
||||
register={register}
|
||||
validations={{
|
||||
required: false,
|
||||
|
|
@ -246,9 +272,9 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
</Button>
|
||||
</form>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<TagIcon className="w-5 h-5 text-gray-500" />
|
||||
<div className="flex justify-between items-center gap-x-2">
|
||||
<div className="flex items-center gap-x-2 text-sm">
|
||||
<TagIcon className="w-4 h-4" />
|
||||
<p>Label</p>
|
||||
</div>
|
||||
<div>
|
||||
|
|
@ -267,15 +293,11 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
<>
|
||||
<Listbox.Label className="sr-only">Label</Listbox.Label>
|
||||
<div className="relative">
|
||||
<Listbox.Button className="relative inline-flex items-center whitespace-nowrap rounded-full bg-gray-50 py-2 px-2 text-sm font-medium text-gray-500 hover:bg-gray-100 sm:px-3 border border-dashed">
|
||||
<PlusIcon
|
||||
className="h-5 w-5 flex-shrink-0 text-gray-300 sm:-ml-1"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<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">
|
||||
<span
|
||||
className={classNames(
|
||||
value ? "" : "text-gray-900",
|
||||
"hidden truncate capitalize sm:ml-2 sm:block w-16"
|
||||
"hidden truncate capitalize sm:block w-16 text-left"
|
||||
)}
|
||||
>
|
||||
{value && value.length > 0
|
||||
|
|
@ -285,8 +307,9 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
issueLabels?.find((option) => option.id === i)?.name
|
||||
)
|
||||
.join(", ")
|
||||
: `Select label`}
|
||||
: "None"}
|
||||
</span>
|
||||
<ChevronDownIcon className="h-3 w-3" />
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
|
|
@ -296,25 +319,22 @@ const IssueDetailSidebar: React.FC<Props> = ({ control, submitChanges }) => {
|
|||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute right-0 z-10 mt-1 max-h-56 w-52 overflow-auto rounded-lg bg-white py-3 text-base shadow ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
{issueLabels?.map((label: any) => (
|
||||
<Listbox.Option
|
||||
key={label.id}
|
||||
className={({ active, selected }) =>
|
||||
classNames(
|
||||
active || selected ? "bg-indigo-50" : "bg-white",
|
||||
"relative cursor-default select-none py-2 px-3"
|
||||
)
|
||||
}
|
||||
value={label.id}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span className="ml-3 block capitalize font-medium">
|
||||
{label.name}
|
||||
</span>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
<Listbox.Options className="absolute z-10 right-0 mt-1 w-40 bg-white shadow-lg max-h-28 rounded-md py-1 text-xs ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none">
|
||||
<div className="p-1">
|
||||
{issueLabels?.map((label: any) => (
|
||||
<Listbox.Option
|
||||
key={label.id}
|
||||
className={({ active, selected }) =>
|
||||
`${
|
||||
active || selected ? "text-white bg-theme" : "text-gray-900"
|
||||
} cursor-pointer select-none relative p-2 rounded-md truncate`
|
||||
}
|
||||
value={label.id}
|
||||
>
|
||||
{label.name}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</div>
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
|
|
|
|||
122
components/project/issues/issue-detail/activity/index.tsx
Normal file
122
components/project/issues/issue-detail/activity/index.tsx
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
// next
|
||||
import Image from "next/image";
|
||||
import {
|
||||
ChartBarIcon,
|
||||
ChatBubbleBottomCenterTextIcon,
|
||||
Squares2X2Icon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { addSpaceIfCamelCase, timeAgo } from "constants/common";
|
||||
import { IState } from "types";
|
||||
import { Spinner } from "ui";
|
||||
|
||||
type Props = {
|
||||
issueActivities: any[] | undefined;
|
||||
states: IState[] | undefined;
|
||||
};
|
||||
|
||||
const activityIcons = {
|
||||
state: <Squares2X2Icon className="h-4 w-4" />,
|
||||
priority: <ChartBarIcon className="h-4 w-4" />,
|
||||
name: <ChatBubbleBottomCenterTextIcon className="h-4 w-4" />,
|
||||
description: <ChatBubbleBottomCenterTextIcon className="h-4 w-4" />,
|
||||
};
|
||||
|
||||
const IssueActivitySection: React.FC<Props> = ({ issueActivities, states }) => {
|
||||
return (
|
||||
<>
|
||||
{issueActivities ? (
|
||||
<div className="space-y-3">
|
||||
{issueActivities.map((activity) => {
|
||||
if (activity.field !== "updated_by")
|
||||
return (
|
||||
<div key={activity.id} className="relative flex gap-x-2 w-full">
|
||||
{issueActivities.length > 1 ? (
|
||||
<span
|
||||
className="absolute top-5 left-2.5 h-full w-0.5 bg-gray-200"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
{activity.field ? (
|
||||
<div className="relative z-10 flex-shrink-0 -ml-1">
|
||||
<div
|
||||
className={`h-7 w-7 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
|
||||
>
|
||||
{activityIcons[activity.field as keyof typeof activityIcons]}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative z-10 flex-shrink-0 border-2 border-white -ml-1.5">
|
||||
{activity.actor_detail.avatar && activity.actor_detail.avatar !== "" ? (
|
||||
<Image
|
||||
src={activity.actor_detail.avatar}
|
||||
alt={activity.actor_detail.name}
|
||||
height={30}
|
||||
width={30}
|
||||
className="rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`h-8 w-8 bg-gray-700 text-white border-2 border-white grid place-items-center rounded-full`}
|
||||
>
|
||||
{activity.actor_detail.first_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full">
|
||||
<p>
|
||||
{activity.actor_detail.first_name} {activity.actor_detail.last_name}{" "}
|
||||
<span>{activity.verb}</span>{" "}
|
||||
{activity.verb !== "created" ? (
|
||||
<span>{activity.field ?? "commented"}</span>
|
||||
) : (
|
||||
" this issue"
|
||||
)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500">{timeAgo(activity.created_at)}</p>
|
||||
<div className="w-full mt-2">
|
||||
{activity.verb !== "created" && (
|
||||
<div className="text-sm">
|
||||
<div>
|
||||
From:{" "}
|
||||
<span className="text-gray-500">
|
||||
{activity.field === "state"
|
||||
? activity.old_value
|
||||
? addSpaceIfCamelCase(
|
||||
states?.find((s) => s.id === activity.old_value)?.name ?? ""
|
||||
)
|
||||
: "None"
|
||||
: activity.old_value}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
To:{" "}
|
||||
<span className="text-gray-500">
|
||||
{activity.field === "state"
|
||||
? activity.new_value
|
||||
? addSpaceIfCamelCase(
|
||||
states?.find((s) => s.id === activity.new_value)?.name ?? ""
|
||||
)
|
||||
: "None"
|
||||
: activity.new_value}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default IssueActivitySection;
|
||||
|
|
@ -8,11 +8,14 @@ import issuesServices from "lib/services/issues.services";
|
|||
// fetch keys
|
||||
import { PROJECT_ISSUES_COMMENTS } from "constants/fetch-keys";
|
||||
// components
|
||||
import CommentCard from "components/project/issues/comment/IssueCommentCard";
|
||||
import CommentCard from "components/project/issues/issue-detail/comment/IssueCommentCard";
|
||||
// ui
|
||||
import { TextArea, Button, Spinner } from "ui";
|
||||
// types
|
||||
import type { IIssueComment } from "types";
|
||||
// icons
|
||||
import UploadingIcon from "public/animated-icons/uploading.json";
|
||||
|
||||
type Props = {
|
||||
comments?: IIssueComment[];
|
||||
workspaceSlug: string;
|
||||
|
|
@ -67,9 +70,9 @@ const IssueCommentSection: React.FC<Props> = ({ comments, issueId, projectId, wo
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-3 px-2">
|
||||
<div className="space-y-5">
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="p-2 bg-indigo-50 rounded-md mb-6">
|
||||
<div className="p-2 bg-indigo-50 rounded-md">
|
||||
<div className="w-full">
|
||||
<TextArea
|
||||
id="comment"
|
||||
|
|
@ -99,6 +102,7 @@ const IssueCommentSection: React.FC<Props> = ({ comments, issueId, projectId, wo
|
|||
<div className="w-full flex justify-end">
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
{isSubmitting ? "Adding comment..." : "Add comment"}
|
||||
{/* <UploadingIcon /> */}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue