forked from github/plane
Merge branch 'stage-release' of https://github.com/makeplane/plane
This commit is contained in:
commit
93552f190d
@ -103,25 +103,25 @@ const CommandPalette: React.FC = () => {
|
|||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
if (e.ctrlKey && e.key === "/") {
|
if ((e.ctrlKey || e.metaKey) && e.key === "/") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsPaletteOpen(true);
|
setIsPaletteOpen(true);
|
||||||
} else if (e.ctrlKey && e.key === "i") {
|
} else if ((e.ctrlKey || e.metaKey) && e.key === "i") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsIssueModalOpen(true);
|
setIsIssueModalOpen(true);
|
||||||
} else if (e.ctrlKey && e.key === "p") {
|
} else if ((e.ctrlKey || e.metaKey) && e.key === "p") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsProjectModalOpen(true);
|
setIsProjectModalOpen(true);
|
||||||
} else if (e.ctrlKey && e.key === "b") {
|
} else if ((e.ctrlKey || e.metaKey) && e.key === "b") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
toggleCollapsed();
|
toggleCollapsed();
|
||||||
} else if (e.ctrlKey && e.key === "h") {
|
} else if ((e.ctrlKey || e.metaKey) && e.key === "h") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsShortcutsModalOpen(true);
|
setIsShortcutsModalOpen(true);
|
||||||
} else if (e.ctrlKey && e.key === "q") {
|
} else if ((e.ctrlKey || e.metaKey) && e.key === "q") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsCreateCycleModalOpen(true);
|
setIsCreateCycleModalOpen(true);
|
||||||
} else if (e.ctrlKey && e.altKey && e.key === "c") {
|
} else if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === "c") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!router.query.issueId) return;
|
if (!router.query.issueId) return;
|
||||||
|
@ -5,7 +5,11 @@ import Link from "next/link";
|
|||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
// common
|
// common
|
||||||
import { addSpaceIfCamelCase, renderShortNumericDateFormat } from "constants/common";
|
import {
|
||||||
|
addSpaceIfCamelCase,
|
||||||
|
findHowManyDaysLeft,
|
||||||
|
renderShortNumericDateFormat,
|
||||||
|
} from "constants/common";
|
||||||
// types
|
// types
|
||||||
import { IIssue, Properties, NestedKeyOf } from "types";
|
import { IIssue, Properties, NestedKeyOf } from "types";
|
||||||
// icons
|
// icons
|
||||||
@ -23,7 +27,9 @@ import { divide } from "lodash";
|
|||||||
type Props = {
|
type Props = {
|
||||||
selectedGroup: NestedKeyOf<IIssue> | null;
|
selectedGroup: NestedKeyOf<IIssue> | null;
|
||||||
groupTitle: string;
|
groupTitle: string;
|
||||||
groupedByIssues: any;
|
groupedByIssues: {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
};
|
||||||
index: number;
|
index: number;
|
||||||
setIsIssueOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setIsIssueOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
properties: Properties;
|
properties: Properties;
|
||||||
@ -158,25 +164,12 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none"
|
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300 outline-none"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setPreloadedData({
|
setPreloadedData({
|
||||||
// ...state,
|
|
||||||
actionType: "edit",
|
actionType: "edit",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<PencilIcon className="h-4 w-4" />
|
<PencilIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
{/* <button
|
|
||||||
type="button"
|
|
||||||
className="h-7 w-7 p-1 grid place-items-center rounded hover:bg-gray-200 duration-300"
|
|
||||||
onClick={() =>
|
|
||||||
setSelectedState({
|
|
||||||
...state,
|
|
||||||
actionType: "delete",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<TrashIcon className="h-4 w-4 text-red-500" />
|
|
||||||
</button> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
<StrictModeDroppable key={groupTitle} droppableId={groupTitle}>
|
||||||
@ -188,7 +181,7 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
{groupedByIssues[groupTitle].map((childIssue: any, index: number) => (
|
{groupedByIssues[groupTitle].map((childIssue, index: number) => (
|
||||||
<Draggable key={childIssue.id} draggableId={childIssue.id} index={index}>
|
<Draggable key={childIssue.id} draggableId={childIssue.id} index={index}>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<Link href={`/projects/${childIssue.project}/issues/${childIssue.id}`}>
|
<Link href={`/projects/${childIssue.project}/issues/${childIssue.id}`}>
|
||||||
@ -203,6 +196,9 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
className="px-2 py-3 space-y-1.5 select-none"
|
className="px-2 py-3 space-y-1.5 select-none"
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
>
|
>
|
||||||
|
<span className="group-hover:text-theme break-all">
|
||||||
|
{childIssue.name}
|
||||||
|
</span>
|
||||||
{Object.keys(properties).map(
|
{Object.keys(properties).map(
|
||||||
(key) =>
|
(key) =>
|
||||||
properties[key as keyof Properties] &&
|
properties[key as keyof Properties] &&
|
||||||
@ -227,34 +223,65 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
: key === "target_date"
|
: 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-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"
|
: "text-sm text-gray-500"
|
||||||
} gap-1
|
} gap-1 relative
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{key === "target_date" ? (
|
{key === "start_date" && childIssue.start_date !== null && (
|
||||||
<>
|
<span className="text-sm">
|
||||||
<CalendarDaysIcon className="h-4 w-4" />{" "}
|
<CalendarDaysIcon className="h-4 w-4" />
|
||||||
|
{renderShortNumericDateFormat(childIssue.start_date)} -
|
||||||
{childIssue.target_date
|
{childIssue.target_date
|
||||||
? renderShortNumericDateFormat(childIssue.target_date)
|
? renderShortNumericDateFormat(childIssue.target_date)
|
||||||
: "N/A"}
|
: "None"}
|
||||||
</>
|
</span>
|
||||||
) : (
|
|
||||||
""
|
|
||||||
)}
|
)}
|
||||||
{key === "name" && (
|
{key === "target_date" && (
|
||||||
<span className="group-hover:text-theme">
|
<>
|
||||||
{childIssue.name}
|
<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"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{key === "key" && (
|
||||||
|
<span className="text-xs">
|
||||||
|
{childIssue.project_detail?.identifier}-
|
||||||
|
{childIssue.sequence_id}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{key === "state" && (
|
{key === "state" && (
|
||||||
<>{addSpaceIfCamelCase(childIssue["state_detail"].name)}</>
|
<>{addSpaceIfCamelCase(childIssue["state_detail"].name)}</>
|
||||||
)}
|
)}
|
||||||
{key === "priority" && <>{childIssue.priority}</>}
|
{key === "priority" && <>{childIssue.priority}</>}
|
||||||
{key === "description" && <>{childIssue.description}</>}
|
|
||||||
{key === "assignee" ? (
|
{key === "assignee" ? (
|
||||||
<div className="flex items-center gap-1 text-xs">
|
<div className="flex items-center gap-1 text-xs">
|
||||||
{childIssue?.assignee_details?.length > 0 ? (
|
{childIssue?.assignee_details?.length > 0 ? (
|
||||||
childIssue?.assignee_details?.map(
|
childIssue?.assignee_details?.map(
|
||||||
(assignee: any, index: number) => (
|
(assignee, index: number) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
className={`relative z-[1] h-5 w-5 rounded-full ${
|
className={`relative z-[1] h-5 w-5 rounded-full ${
|
||||||
@ -282,7 +309,7 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<span>None</span>
|
<span>No assignee.</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
@ -290,29 +317,6 @@ const SingleBoard: React.FC<Props> = ({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* <div
|
|
||||||
className={`p-2 bg-indigo-50 flex items-center justify-between ${
|
|
||||||
snapshot.isDragging ? "bg-indigo-200" : ""
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="flex flex-col"
|
|
||||||
{...provided.dragHandleProps}
|
|
||||||
>
|
|
||||||
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600" />
|
|
||||||
<EllipsisHorizontalIcon className="h-4 w-4 text-gray-600 mt-[-0.7rem]" />
|
|
||||||
</button>
|
|
||||||
<div className="flex gap-1 items-center">
|
|
||||||
<button type="button">
|
|
||||||
<HeartIcon className="h-4 w-4 text-yellow-500" />
|
|
||||||
</button>
|
|
||||||
<button type="button">
|
|
||||||
<CheckCircleIcon className="h-4 w-4 text-green-500" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div> */}
|
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
@ -67,8 +67,6 @@ const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues
|
|||||||
|
|
||||||
setIssueDeletionData(removedItem);
|
setIssueDeletionData(removedItem);
|
||||||
setIsIssueDeletionOpen(true);
|
setIsIssueDeletionOpen(true);
|
||||||
|
|
||||||
console.log(removedItem);
|
|
||||||
} else {
|
} else {
|
||||||
if (type === "state") {
|
if (type === "state") {
|
||||||
const newStates = Array.from(states ?? []);
|
const newStates = Array.from(states ?? []);
|
||||||
@ -168,21 +166,6 @@ const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* <CreateUpdateStateModal
|
|
||||||
isOpen={
|
|
||||||
isOpen &&
|
|
||||||
preloadedData?.actionType !== "delete" &&
|
|
||||||
preloadedData?.actionType !== "createIssue"
|
|
||||||
}
|
|
||||||
setIsOpen={setIsOpen}
|
|
||||||
data={preloadedData as Partial<IIssue>}
|
|
||||||
projectId={projectId as string}
|
|
||||||
/> */}
|
|
||||||
{/* <ConfirmStateDeletion
|
|
||||||
isOpen={isOpen && preloadedData?.actionType === "delete"}
|
|
||||||
setIsOpen={setIsOpen}
|
|
||||||
data={preloadedData as Partial<IIssue>}
|
|
||||||
/> */}
|
|
||||||
<ConfirmIssueDeletion
|
<ConfirmIssueDeletion
|
||||||
isOpen={isIssueDeletionOpen}
|
isOpen={isIssueDeletionOpen}
|
||||||
handleClose={() => setIsIssueDeletionOpen(false)}
|
handleClose={() => setIsIssueDeletionOpen(false)}
|
||||||
@ -199,21 +182,6 @@ const BoardView: React.FC<Props> = ({ properties, selectedGroup, groupedByIssues
|
|||||||
{groupedByIssues ? (
|
{groupedByIssues ? (
|
||||||
<div className="h-full w-full">
|
<div className="h-full w-full">
|
||||||
<DragDropContext onDragEnd={handleOnDragEnd}>
|
<DragDropContext onDragEnd={handleOnDragEnd}>
|
||||||
{/* <StrictModeDroppable droppableId="trashBox">
|
|
||||||
{(provided, snapshot) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`fixed bottom-2 right-8 z-10 px-2 py-1 flex items-center gap-2 rounded-lg mb-5 text-red-600 text-sm bg-red-100 border-2 border-transparent ${
|
|
||||||
snapshot.isDraggingOver ? "border-red-600" : ""
|
|
||||||
}`}
|
|
||||||
{...provided.droppableProps}
|
|
||||||
ref={provided.innerRef}
|
|
||||||
>
|
|
||||||
<TrashIcon className="h-3 w-3" />
|
|
||||||
Drop to delete
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</StrictModeDroppable> */}
|
|
||||||
<div className="h-full w-full overflow-hidden">
|
<div className="h-full w-full overflow-hidden">
|
||||||
<StrictModeDroppable droppableId="state" type="state" direction="horizontal">
|
<StrictModeDroppable droppableId="state" type="state" direction="horizontal">
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
|
@ -5,6 +5,8 @@ import { Controller } from "react-hook-form";
|
|||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
// icons
|
// icons
|
||||||
import { CheckIcon } from "@heroicons/react/20/solid";
|
import { CheckIcon } from "@heroicons/react/20/solid";
|
||||||
|
// constants
|
||||||
|
import { PRIORITIES } from "constants/";
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import type { IIssue } from "types";
|
import type { IIssue } from "types";
|
||||||
@ -15,8 +17,6 @@ type Props = {
|
|||||||
control: Control<IIssue, any>;
|
control: Control<IIssue, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PRIORITIES = ["high", "medium", "low"];
|
|
||||||
|
|
||||||
const SelectPriority: React.FC<Props> = ({ control }) => {
|
const SelectPriority: React.FC<Props> = ({ control }) => {
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
|
@ -15,6 +15,7 @@ import { IIssue, IssueResponse, NestedKeyOf, Properties, WorkspaceMember } from
|
|||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
// fetch keys
|
// fetch keys
|
||||||
|
import { PRIORITIES } from "constants/";
|
||||||
import { PROJECT_ISSUES_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
import { PROJECT_ISSUES_LIST, WORKSPACE_MEMBERS } from "constants/fetch-keys";
|
||||||
// services
|
// services
|
||||||
import issuesServices from "lib/services/issues.services";
|
import issuesServices from "lib/services/issues.services";
|
||||||
@ -37,8 +38,6 @@ type Props = {
|
|||||||
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
|
handleDeleteIssue: React.Dispatch<React.SetStateAction<string | undefined>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PRIORITIES = ["high", "medium", "low"];
|
|
||||||
|
|
||||||
const ListView: React.FC<Props> = ({
|
const ListView: React.FC<Props> = ({
|
||||||
properties,
|
properties,
|
||||||
groupedByIssues,
|
groupedByIssues,
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { NestedKeyOf } from "types";
|
||||||
|
|
||||||
export const classNames = (...classes: string[]) => {
|
export const classNames = (...classes: string[]) => {
|
||||||
return classes.filter(Boolean).join(" ");
|
return classes.filter(Boolean).join(" ");
|
||||||
};
|
};
|
||||||
@ -30,6 +32,32 @@ export const groupBy = (array: any[], key: string) => {
|
|||||||
}, {});
|
}, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const orderArrayBy = (
|
||||||
|
array: any[],
|
||||||
|
key: string,
|
||||||
|
ordering: "ascending" | "descending" = "ascending"
|
||||||
|
) => {
|
||||||
|
const innerKey = key.split("."); // split the key by dot
|
||||||
|
return array.sort((a, b) => {
|
||||||
|
const keyA = innerKey.reduce((obj, i) => obj[i], a); // get the value of the inner key
|
||||||
|
const keyB = innerKey.reduce((obj, i) => obj[i], b); // get the value of the inner key
|
||||||
|
if (keyA < keyB) {
|
||||||
|
return ordering === "ascending" ? -1 : 1;
|
||||||
|
}
|
||||||
|
if (keyA > keyB) {
|
||||||
|
return ordering === "ascending" ? 1 : -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findHowManyDaysLeft = (date: string | Date) => {
|
||||||
|
const today = new Date();
|
||||||
|
const eventDate = new Date(date);
|
||||||
|
const timeDiff = Math.abs(eventDate.getTime() - today.getTime());
|
||||||
|
return Math.ceil(timeDiff / (1000 * 3600 * 24));
|
||||||
|
};
|
||||||
|
|
||||||
export const timeAgo = (time: any) => {
|
export const timeAgo = (time: any) => {
|
||||||
switch (typeof time) {
|
switch (typeof time) {
|
||||||
case "number":
|
case "number":
|
||||||
|
1
apps/app/constants/index.ts
Normal file
1
apps/app/constants/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const PRIORITIES = ["urgent", "high", "medium", "low"];
|
94
apps/app/lib/hooks/useIssuesFilter.tsx
Normal file
94
apps/app/lib/hooks/useIssuesFilter.tsx
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
// hooks
|
||||||
|
import useTheme from "./useTheme";
|
||||||
|
import useUser from "./useUser";
|
||||||
|
// commons
|
||||||
|
import { groupBy, orderArrayBy } from "constants/common";
|
||||||
|
// constants
|
||||||
|
import { PRIORITIES } from "constants/";
|
||||||
|
// types
|
||||||
|
import type { IssueResponse, IIssue, NestedKeyOf } from "types";
|
||||||
|
|
||||||
|
const useIssuesFilter = (projectIssues?: IssueResponse) => {
|
||||||
|
const { issueView, setIssueView, groupByProperty, setGroupByProperty } = useTheme();
|
||||||
|
|
||||||
|
const [orderBy, setOrderBy] = useState<NestedKeyOf<IIssue> | null>(null);
|
||||||
|
|
||||||
|
const [filterIssue, setFilterIssue] = useState<"activeIssue" | "backlogIssue" | null>(null);
|
||||||
|
|
||||||
|
const { states } = useUser();
|
||||||
|
|
||||||
|
let groupedByIssues: {
|
||||||
|
[key: string]: IIssue[];
|
||||||
|
} = {
|
||||||
|
...(groupByProperty === "state_detail.name"
|
||||||
|
? Object.fromEntries(
|
||||||
|
states
|
||||||
|
?.sort((a, b) => a.sequence - b.sequence)
|
||||||
|
?.map((state) => [
|
||||||
|
state.name,
|
||||||
|
projectIssues?.results.filter((issue) => issue.state === state.name) ?? [],
|
||||||
|
]) ?? []
|
||||||
|
)
|
||||||
|
: groupByProperty === "priority"
|
||||||
|
? Object.fromEntries(
|
||||||
|
PRIORITIES.map((priority) => [
|
||||||
|
priority,
|
||||||
|
projectIssues?.results.filter((issue) => issue.priority === priority) ?? [],
|
||||||
|
])
|
||||||
|
)
|
||||||
|
: {}),
|
||||||
|
...groupBy(projectIssues?.results ?? [], groupByProperty ?? ""),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (orderBy !== null) {
|
||||||
|
groupedByIssues = Object.fromEntries(
|
||||||
|
Object.entries(groupedByIssues).map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
orderArrayBy(value, orderBy, "descending"),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterIssue !== null) {
|
||||||
|
if (filterIssue === "activeIssue") {
|
||||||
|
groupedByIssues = Object.keys(groupedByIssues).reduce((acc, key) => {
|
||||||
|
const value = groupedByIssues[key];
|
||||||
|
const filteredValue = value.filter(
|
||||||
|
(issue) =>
|
||||||
|
issue.state_detail.group === "started" || issue.state_detail.group === "unstarted"
|
||||||
|
);
|
||||||
|
if (filteredValue.length > 0) {
|
||||||
|
acc[key] = filteredValue;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as typeof groupedByIssues);
|
||||||
|
} else if (filterIssue === "backlogIssue") {
|
||||||
|
groupedByIssues = Object.keys(groupedByIssues).reduce((acc, key) => {
|
||||||
|
const value = groupedByIssues[key];
|
||||||
|
const filteredValue = value.filter(
|
||||||
|
(issue) =>
|
||||||
|
issue.state_detail.group === "backlog" || issue.state_detail.group === "cancelled"
|
||||||
|
);
|
||||||
|
if (filteredValue.length > 0) {
|
||||||
|
acc[key] = filteredValue;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as typeof groupedByIssues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
groupedByIssues,
|
||||||
|
issueView,
|
||||||
|
setIssueView,
|
||||||
|
groupByProperty,
|
||||||
|
setGroupByProperty,
|
||||||
|
orderBy,
|
||||||
|
setOrderBy,
|
||||||
|
filterIssue,
|
||||||
|
setFilterIssue,
|
||||||
|
} as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useIssuesFilter;
|
@ -1,4 +1,4 @@
|
|||||||
import { useState, useContext, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
// swr
|
// swr
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// api routes
|
// api routes
|
||||||
@ -80,7 +80,22 @@ const useIssuesProperties = (workspaceSlug?: string, projectId?: string) => {
|
|||||||
[workspaceSlug, projectId, issueProperties, user]
|
[workspaceSlug, projectId, issueProperties, user]
|
||||||
);
|
);
|
||||||
|
|
||||||
return [properties, updateIssueProperties] as const;
|
const newProperties = Object.keys(properties).reduce((obj: any, key) => {
|
||||||
|
if (
|
||||||
|
key !== "children" &&
|
||||||
|
key !== "name" &&
|
||||||
|
key !== "parent" &&
|
||||||
|
key !== "project" &&
|
||||||
|
key !== "description" &&
|
||||||
|
key !== "attachments" &&
|
||||||
|
key !== "sequence_id"
|
||||||
|
) {
|
||||||
|
obj[key] = properties[key as keyof Properties];
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
return [newProperties, updateIssueProperties] as const;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useIssuesProperties;
|
export default useIssuesProperties;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
// react
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
// next
|
// next
|
||||||
import type { NextPage } from "next";
|
import type { NextPage } from "next";
|
||||||
@ -6,52 +5,75 @@ import { useRouter } from "next/router";
|
|||||||
// swr
|
// swr
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// headless ui
|
// headless ui
|
||||||
import { Menu, Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
// services
|
// hoc
|
||||||
import stateServices from "lib/services/state.services";
|
import withAuth from "lib/hoc/withAuthWrapper";
|
||||||
import issuesServices from "lib/services/issues.services";
|
|
||||||
// hooks
|
// hooks
|
||||||
import useUser from "lib/hooks/useUser";
|
import useUser from "lib/hooks/useUser";
|
||||||
import useTheme from "lib/hooks/useTheme";
|
|
||||||
import useIssuesProperties from "lib/hooks/useIssuesProperties";
|
import useIssuesProperties from "lib/hooks/useIssuesProperties";
|
||||||
// fetching keys
|
// api routes
|
||||||
import { PROJECT_ISSUES_LIST, STATE_LIST } from "constants/fetch-keys";
|
import { PROJECT_MEMBERS } from "constants/api-routes";
|
||||||
|
// services
|
||||||
|
import projectService from "lib/services/project.service";
|
||||||
// commons
|
// commons
|
||||||
import { groupBy } from "constants/common";
|
import { classNames, replaceUnderscoreIfSnakeCase } from "constants/common";
|
||||||
// layouts
|
// layouts
|
||||||
import AdminLayout from "layouts/AdminLayout";
|
import AdminLayout from "layouts/AdminLayout";
|
||||||
|
// hooks
|
||||||
|
import useIssuesFilter from "lib/hooks/useIssuesFilter";
|
||||||
// components
|
// components
|
||||||
import ListView from "components/project/issues/ListView";
|
import ListView from "components/project/issues/ListView";
|
||||||
import BoardView from "components/project/issues/BoardView";
|
import BoardView from "components/project/issues/BoardView";
|
||||||
import ConfirmIssueDeletion from "components/project/issues/ConfirmIssueDeletion";
|
import ConfirmIssueDeletion from "components/project/issues/ConfirmIssueDeletion";
|
||||||
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
import CreateUpdateIssuesModal from "components/project/issues/CreateUpdateIssueModal";
|
||||||
// ui
|
// ui
|
||||||
import { Spinner } from "ui";
|
import { Spinner, CustomMenu, BreadcrumbItem, Breadcrumbs } from "ui";
|
||||||
import { EmptySpace, EmptySpaceItem } from "ui/EmptySpace";
|
import { EmptySpace, EmptySpaceItem } from "ui/EmptySpace";
|
||||||
import HeaderButton from "ui/HeaderButton";
|
import HeaderButton from "ui/HeaderButton";
|
||||||
import { BreadcrumbItem, Breadcrumbs } from "ui";
|
|
||||||
// icons
|
// icons
|
||||||
import { ChevronDownIcon, ListBulletIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
import { ChevronDownIcon, ListBulletIcon, RectangleStackIcon } from "@heroicons/react/24/outline";
|
||||||
import { PlusIcon, EyeIcon, EyeSlashIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
import { PlusIcon, Squares2X2Icon } from "@heroicons/react/20/solid";
|
||||||
// types
|
// types
|
||||||
import type { IIssue, IssueResponse, Properties, IState, NestedKeyOf, ProjectMember } from "types";
|
import type { IIssue, Properties, NestedKeyOf, ProjectMember } from "types";
|
||||||
import { PROJECT_MEMBERS } from "constants/api-routes";
|
|
||||||
import projectService from "lib/services/project.service";
|
|
||||||
|
|
||||||
const PRIORITIES = ["high", "medium", "low"];
|
const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> }> = [
|
||||||
|
{ name: "State", key: "state_detail.name" },
|
||||||
|
{ name: "Priority", key: "priority" },
|
||||||
|
{ name: "Created By", key: "created_by" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const orderByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> }> = [
|
||||||
|
{ name: "Created", key: "created_at" },
|
||||||
|
{ name: "Update", key: "updated_at" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const filterIssueOptions: Array<{
|
||||||
|
name: string;
|
||||||
|
key: "activeIssue" | "backlogIssue" | null;
|
||||||
|
}> = [
|
||||||
|
{
|
||||||
|
name: "All",
|
||||||
|
key: null,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Active Issues",
|
||||||
|
key: "activeIssue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Backlog Issues",
|
||||||
|
key: "backlogIssue",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const ProjectIssues: NextPage = () => {
|
const ProjectIssues: NextPage = () => {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const { issueView, setIssueView, groupByProperty, setGroupByProperty } = useTheme();
|
|
||||||
|
|
||||||
const [selectedIssue, setSelectedIssue] = useState<
|
const [selectedIssue, setSelectedIssue] = useState<
|
||||||
(IIssue & { actionType: "edit" | "delete" }) | undefined
|
(IIssue & { actionType: "edit" | "delete" }) | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
const [editIssue, setEditIssue] = useState<string | undefined>();
|
|
||||||
const [deleteIssue, setDeleteIssue] = useState<string | undefined>(undefined);
|
const [deleteIssue, setDeleteIssue] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const { activeWorkspace, activeProject, issues } = useUser();
|
const { activeWorkspace, activeProject, issues: projectIssues } = useUser();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@ -62,22 +84,6 @@ const ProjectIssues: NextPage = () => {
|
|||||||
projectId as string
|
projectId as string
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data: projectIssues } = useSWR<IssueResponse>(
|
|
||||||
projectId && activeWorkspace
|
|
||||||
? PROJECT_ISSUES_LIST(activeWorkspace.slug, projectId as string)
|
|
||||||
: null,
|
|
||||||
activeWorkspace && projectId
|
|
||||||
? () => issuesServices.getIssues(activeWorkspace.slug, projectId as string)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: states } = useSWR<IState[]>(
|
|
||||||
activeWorkspace && activeProject ? STATE_LIST(activeProject.id) : null,
|
|
||||||
activeWorkspace && activeProject
|
|
||||||
? () => stateServices.getStates(activeWorkspace.slug, activeProject.id)
|
|
||||||
: null
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: members } = useSWR<ProjectMember[]>(
|
const { data: members } = useSWR<ProjectMember[]>(
|
||||||
activeWorkspace && activeProject ? PROJECT_MEMBERS : null,
|
activeWorkspace && activeProject ? PROJECT_MEMBERS : null,
|
||||||
activeWorkspace && activeProject
|
activeWorkspace && activeProject
|
||||||
@ -85,6 +91,18 @@ const ProjectIssues: NextPage = () => {
|
|||||||
: null
|
: null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
issueView,
|
||||||
|
setIssueView,
|
||||||
|
groupByProperty,
|
||||||
|
setGroupByProperty,
|
||||||
|
groupedByIssues,
|
||||||
|
setOrderBy,
|
||||||
|
setFilterIssue,
|
||||||
|
orderBy,
|
||||||
|
filterIssue,
|
||||||
|
} = useIssuesFilter(projectIssues);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
@ -94,35 +112,6 @@ const ProjectIssues: NextPage = () => {
|
|||||||
}
|
}
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
const groupedByIssues: {
|
|
||||||
[key: string]: IIssue[];
|
|
||||||
} = {
|
|
||||||
...(groupByProperty === "state_detail.name"
|
|
||||||
? Object.fromEntries(
|
|
||||||
states
|
|
||||||
?.sort((a, b) => a.sequence - b.sequence)
|
|
||||||
?.map((state) => [
|
|
||||||
state.name,
|
|
||||||
projectIssues?.results.filter((issue) => issue.state === state.name) ?? [],
|
|
||||||
]) ?? []
|
|
||||||
)
|
|
||||||
: groupByProperty === "priority"
|
|
||||||
? Object.fromEntries(
|
|
||||||
PRIORITIES.map((priority) => [
|
|
||||||
priority,
|
|
||||||
projectIssues?.results.filter((issue) => issue.priority === priority) ?? [],
|
|
||||||
])
|
|
||||||
)
|
|
||||||
: {}),
|
|
||||||
...groupBy(projectIssues?.results ?? [], groupByProperty ?? ""),
|
|
||||||
};
|
|
||||||
|
|
||||||
const groupByOptions: Array<{ name: string; key: NestedKeyOf<IIssue> }> = [
|
|
||||||
{ name: "State", key: "state_detail.name" },
|
|
||||||
{ name: "Priority", key: "priority" },
|
|
||||||
{ name: "Created By", key: "created_by" },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminLayout>
|
<AdminLayout>
|
||||||
<CreateUpdateIssuesModal
|
<CreateUpdateIssuesModal
|
||||||
@ -149,7 +138,7 @@ const ProjectIssues: NextPage = () => {
|
|||||||
</Breadcrumbs>
|
</Breadcrumbs>
|
||||||
<div className="flex items-center justify-between w-full">
|
<div className="flex items-center justify-between w-full">
|
||||||
<h2 className="text-2xl font-medium">Project Issues</h2>
|
<h2 className="text-2xl font-medium">Project Issues</h2>
|
||||||
<div className="flex items-center gap-x-3">
|
<div className="flex items-center md:gap-x-6 sm:gap-x-3">
|
||||||
<div className="flex items-center gap-x-1">
|
<div className="flex items-center gap-x-1">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -176,70 +165,23 @@ const ProjectIssues: NextPage = () => {
|
|||||||
<Squares2X2Icon className="h-4 w-4" />
|
<Squares2X2Icon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<Menu as="div" className="relative inline-block w-40">
|
|
||||||
<div className="w-full">
|
|
||||||
<Menu.Button className="inline-flex justify-between items-center w-full rounded-md shadow-sm p-2 border border-gray-300 text-xs font-semibold text-gray-700 hover:bg-gray-100 focus:outline-none">
|
|
||||||
<span className="flex gap-x-1 items-center">
|
|
||||||
{groupByOptions.find((option) => option.key === groupByProperty)?.name ??
|
|
||||||
"No Grouping"}
|
|
||||||
</span>
|
|
||||||
<div className="flex-grow flex justify-end">
|
|
||||||
<ChevronDownIcon className="h-4 w-4" aria-hidden="true" />
|
|
||||||
</div>
|
|
||||||
</Menu.Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Transition
|
|
||||||
as={React.Fragment}
|
|
||||||
enter="transition ease-out duration-100"
|
|
||||||
enterFrom="transform opacity-0 scale-95"
|
|
||||||
enterTo="transform opacity-100 scale-100"
|
|
||||||
leave="transition ease-in duration-75"
|
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
|
||||||
leaveTo="transform opacity-0 scale-95"
|
|
||||||
>
|
|
||||||
<Menu.Items className="origin-top-left absolute left-0 mt-2 w-full rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none z-50">
|
|
||||||
<div className="p-1">
|
|
||||||
{groupByOptions.map((option) => (
|
|
||||||
<Menu.Item key={option.key}>
|
|
||||||
{({ active }) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`${
|
|
||||||
active ? "bg-theme text-white" : "text-gray-900"
|
|
||||||
} group flex w-full items-center rounded-md p-2 text-xs`}
|
|
||||||
onClick={() => setGroupByProperty(option.key)}
|
|
||||||
>
|
|
||||||
{option.name}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
))}
|
|
||||||
{issueView === "list" ? (
|
|
||||||
<Menu.Item>
|
|
||||||
{({ active }) => (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`hover:bg-theme hover:text-white ${
|
|
||||||
active ? "bg-theme text-white" : "text-gray-900"
|
|
||||||
} group flex w-full items-center rounded-md p-2 text-xs`}
|
|
||||||
onClick={() => setGroupByProperty(null)}
|
|
||||||
>
|
|
||||||
No grouping
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</Menu.Item>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</Menu.Items>
|
|
||||||
</Transition>
|
|
||||||
</Menu>
|
|
||||||
<Popover className="relative">
|
<Popover className="relative">
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<Popover.Button className="inline-flex justify-between items-center rounded-md shadow-sm p-2 border border-gray-300 text-xs font-semibold text-gray-700 hover:bg-gray-100 focus:outline-none w-40">
|
<Popover.Button
|
||||||
<span>Properties</span>
|
className={classNames(
|
||||||
<ChevronDownIcon className="h-4 w-4" />
|
open ? "text-gray-900" : "text-gray-500",
|
||||||
|
"group inline-flex items-center rounded-md bg-transparent text-base font-medium hover:text-gray-900 focus:outline-none border border-gray-300 px-3 py-1"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<span>View</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={classNames(
|
||||||
|
open ? "text-gray-600" : "text-gray-400",
|
||||||
|
"ml-2 h-4 w-4 group-hover:text-gray-500"
|
||||||
|
)}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</Popover.Button>
|
</Popover.Button>
|
||||||
|
|
||||||
<Transition
|
<Transition
|
||||||
@ -251,25 +193,83 @@ const ProjectIssues: NextPage = () => {
|
|||||||
leaveFrom="opacity-100 translate-y-0"
|
leaveFrom="opacity-100 translate-y-0"
|
||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute left-1/2 z-10 mt-1 -translate-x-1/2 transform px-2 sm:px-0 w-full">
|
<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-gray-0 backdrop-filter backdrop-blur-xl bg-opacity-100 rounded-lg shadow-lg overflow-hidden">
|
||||||
<div className="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5">
|
<div className="overflow-hidden py-8 px-4">
|
||||||
<div className="relative grid bg-white p-1">
|
<div className="relative flex flex-col gap-1 gap-y-4">
|
||||||
{Object.keys(properties).map((key) => (
|
<div className="flex justify-between">
|
||||||
<button
|
<h4 className="text-base text-gray-600">Group by</h4>
|
||||||
key={key}
|
<CustomMenu
|
||||||
className={`text-gray-900 hover:bg-theme hover:text-white flex justify-between w-full items-center rounded-md p-2 text-xs`}
|
label={
|
||||||
onClick={() => setProperties(key as keyof Properties)}
|
groupByOptions.find((option) => option.key === groupByProperty)
|
||||||
|
?.name ?? "Select"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<p className="capitalize">{key.replace("_", " ")}</p>
|
{groupByOptions.map((option) => (
|
||||||
<span className="self-end">
|
<CustomMenu.MenuItem
|
||||||
{properties[key as keyof Properties] ? (
|
key={option.key}
|
||||||
<EyeIcon width="18" height="18" />
|
onClick={() => setGroupByProperty(option.key)}
|
||||||
) : (
|
>
|
||||||
<EyeSlashIcon width="18" height="18" />
|
{option.name}
|
||||||
)}
|
</CustomMenu.MenuItem>
|
||||||
</span>
|
))}
|
||||||
</button>
|
</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) => (
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
@ -335,4 +335,4 @@ const ProjectIssues: NextPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ProjectIssues;
|
export default withAuth(ProjectIssues);
|
||||||
|
@ -103,7 +103,9 @@ const Workspace: NextPage = () => {
|
|||||||
<a className="hover:text-theme duration-300">{issue.name}</a>
|
<a className="hover:text-theme duration-300">{issue.name}</a>
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-4">{issue.sequence_id}</td>
|
<td className="px-3 py-4">
|
||||||
|
{issue.project_detail?.identifier}-{issue.sequence_id}
|
||||||
|
</td>
|
||||||
<td className="px-3 py-4">
|
<td className="px-3 py-4">
|
||||||
<span
|
<span
|
||||||
className="rounded px-2 py-1 text-xs font-medium"
|
className="rounded px-2 py-1 text-xs font-medium"
|
||||||
|
1
apps/app/types/state.d.ts
vendored
1
apps/app/types/state.d.ts
vendored
@ -11,4 +11,5 @@ export interface IState {
|
|||||||
project: string;
|
project: string;
|
||||||
workspace: string;
|
workspace: string;
|
||||||
sequence: number;
|
sequence: number;
|
||||||
|
group: "backlog" | "unstarted" | "started" | "completed" | "cancelled";
|
||||||
}
|
}
|
||||||
|
74
apps/app/ui/CustomMenu/index.tsx
Normal file
74
apps/app/ui/CustomMenu/index.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import React from "react";
|
||||||
|
// next
|
||||||
|
import Link from "next/link";
|
||||||
|
// headless ui
|
||||||
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
// icons
|
||||||
|
import { ChevronDownIcon } from "@heroicons/react/20/solid";
|
||||||
|
// commons
|
||||||
|
import { classNames } from "constants/common";
|
||||||
|
// types
|
||||||
|
import type { MenuItemProps, Props } from "./types";
|
||||||
|
|
||||||
|
const CustomMenu = ({ children, label }: Props) => {
|
||||||
|
return (
|
||||||
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
|
<div>
|
||||||
|
<Menu.Button className="inline-flex w-32 justify-between gap-x-4 rounded-md border border-gray-300 bg-white px-4 py-1 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:ring-offset-gray-100">
|
||||||
|
<span className="truncate w-20">{label}</span>
|
||||||
|
<ChevronDownIcon className="-mr-1 ml-2 h-5 w-5" aria-hidden="true" />
|
||||||
|
</Menu.Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Transition
|
||||||
|
as={React.Fragment}
|
||||||
|
enter="transition ease-out duration-100"
|
||||||
|
enterFrom="transform opacity-0 scale-95"
|
||||||
|
enterTo="transform opacity-100 scale-100"
|
||||||
|
leave="transition ease-in duration-75"
|
||||||
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<Menu.Items className="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
|
||||||
|
<div className="py-1">{children}</div>
|
||||||
|
</Menu.Items>
|
||||||
|
</Transition>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MenuItem: React.FC<MenuItemProps> = ({ children, renderAs, href, onClick }) => {
|
||||||
|
return (
|
||||||
|
<Menu.Item>
|
||||||
|
{({ active }) =>
|
||||||
|
renderAs === "a" ? (
|
||||||
|
<Link href={href ?? ""}>
|
||||||
|
<a
|
||||||
|
className={classNames(
|
||||||
|
active ? "bg-gray-100 text-gray-900" : "text-gray-700",
|
||||||
|
"block px-4 py-2 text-sm"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
className={classNames(
|
||||||
|
active ? "bg-gray-100 text-gray-900" : "text-gray-700",
|
||||||
|
"block w-full px-4 py-2 text-left text-sm"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
CustomMenu.MenuItem = MenuItem;
|
||||||
|
|
||||||
|
export default CustomMenu;
|
11
apps/app/ui/CustomMenu/types.d.ts
vendored
Normal file
11
apps/app/ui/CustomMenu/types.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
export type Props = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MenuItemProps = {
|
||||||
|
children: string;
|
||||||
|
renderAs?: "button" | "a";
|
||||||
|
href?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
};
|
@ -3,6 +3,7 @@ export { default as Input } from "./Input";
|
|||||||
export { default as Select } from "./Select";
|
export { default as Select } from "./Select";
|
||||||
export { default as TextArea } from "./TextArea";
|
export { default as TextArea } from "./TextArea";
|
||||||
export { default as CustomListbox } from "./CustomListbox";
|
export { default as CustomListbox } from "./CustomListbox";
|
||||||
|
export { default as CustomMenu } from "./CustomMenu";
|
||||||
export { default as Spinner } from "./Spinner";
|
export { default as Spinner } from "./Spinner";
|
||||||
export { default as Tooltip } from "./Tooltip";
|
export { default as Tooltip } from "./Tooltip";
|
||||||
export { default as SearchListbox } from "./SearchListbox";
|
export { default as SearchListbox } from "./SearchListbox";
|
||||||
|
Loading…
Reference in New Issue
Block a user