forked from github/plane
feat: quick add (#2240)
* feat: quick add * style: made text color muted
This commit is contained in:
parent
daa0b16960
commit
771ca585db
@ -2,3 +2,4 @@ export * from "./all-boards";
|
|||||||
export * from "./board-header";
|
export * from "./board-header";
|
||||||
export * from "./single-board";
|
export * from "./single-board";
|
||||||
export * from "./single-issue";
|
export * from "./single-issue";
|
||||||
|
export * from "./inline-create-issue-form";
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
// react hook form
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { InlineCreateIssueFormWrapper } from "components/core";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useProjectDetails from "hooks/use-project-details";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||||
|
prePopulatedData?: Partial<IIssue>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InlineInput = () => {
|
||||||
|
const { projectDetails } = useProjectDetails();
|
||||||
|
|
||||||
|
const { register, setFocus } = useFormContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFocus("name");
|
||||||
|
}, [setFocus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-medium leading-5 text-custom-text-300">
|
||||||
|
{projectDetails?.identifier ?? "..."}
|
||||||
|
</h4>
|
||||||
|
<input
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="Issue Title"
|
||||||
|
{...register("name", {
|
||||||
|
required: "Issue title is required.",
|
||||||
|
})}
|
||||||
|
className="w-full px-2 pl-0 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BoardInlineCreateIssueForm: React.FC<Props> = (props) => (
|
||||||
|
<>
|
||||||
|
<InlineCreateIssueFormWrapper
|
||||||
|
className="flex flex-col justify-between gap-1.5 group/card relative select-none px-3.5 py-3 h-[118px] mb-3 rounded bg-custom-background-100 shadow"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<InlineInput />
|
||||||
|
</InlineCreateIssueFormWrapper>
|
||||||
|
{props.isOpen && (
|
||||||
|
<p className="text-xs ml-3 italic text-custom-text-200">
|
||||||
|
Press {"'"}Enter{"'"} to add another issue
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
@ -6,7 +6,7 @@ import { useRouter } from "next/router";
|
|||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
// components
|
// components
|
||||||
import { BoardHeader, SingleBoardIssue } from "components/core";
|
import { BoardHeader, SingleBoardIssue, BoardInlineCreateIssueForm } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { CustomMenu } from "components/ui";
|
import { CustomMenu } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -34,7 +34,8 @@ type Props = {
|
|||||||
viewProps: IIssueViewProps;
|
viewProps: IIssueViewProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleBoard: React.FC<Props> = ({
|
export const SingleBoard: React.FC<Props> = (props) => {
|
||||||
|
const {
|
||||||
addIssueToGroup,
|
addIssueToGroup,
|
||||||
currentState,
|
currentState,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
@ -50,10 +51,13 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
viewProps,
|
viewProps,
|
||||||
}) => {
|
} = props;
|
||||||
|
|
||||||
// collapse/expand
|
// collapse/expand
|
||||||
const [isCollapsed, setIsCollapsed] = useState(true);
|
const [isCollapsed, setIsCollapsed] = useState(true);
|
||||||
|
|
||||||
|
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
const { displayFilters, groupedIssues } = viewProps;
|
const { displayFilters, groupedIssues } = viewProps;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -67,6 +71,24 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
|
|
||||||
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
const isNotAllowed = userAuth.isGuest || userAuth.isViewer || disableUserActions;
|
||||||
|
|
||||||
|
const onCreateClick = () => {
|
||||||
|
setIsInlineCreateIssueFormOpen(true);
|
||||||
|
|
||||||
|
const boardListElement = document.getElementById(`board-list-${groupTitle}`);
|
||||||
|
|
||||||
|
// timeout is needed because the animation
|
||||||
|
// takes time to complete & we can scroll only after that
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
if (boardListElement)
|
||||||
|
boardListElement.scrollBy({
|
||||||
|
top: boardListElement.scrollHeight,
|
||||||
|
left: 0,
|
||||||
|
behavior: "smooth",
|
||||||
|
});
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}, 10);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
<div className={`flex-shrink-0 ${!isCollapsed ? "" : "flex h-full flex-col w-96"}`}>
|
||||||
<BoardHeader
|
<BoardHeader
|
||||||
@ -115,6 +137,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
id={`board-list-${groupTitle}`}
|
||||||
className={`pt-3 ${
|
className={`pt-3 ${
|
||||||
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
|
hasMinimumNumberOfCards ? "overflow-hidden overflow-y-scroll" : ""
|
||||||
} `}
|
} `}
|
||||||
@ -169,6 +192,19 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<>{provided.placeholder}</>
|
<>{provided.placeholder}</>
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
<BoardInlineCreateIssueForm
|
||||||
|
isOpen={isInlineCreateIssueFormOpen}
|
||||||
|
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
|
||||||
|
prePopulatedData={{
|
||||||
|
...(cycleId && { cycle: cycleId.toString() }),
|
||||||
|
...(moduleId && { module: moduleId.toString() }),
|
||||||
|
[displayFilters?.group_by! === "labels"
|
||||||
|
? "labels_list"
|
||||||
|
: displayFilters?.group_by!]:
|
||||||
|
displayFilters?.group_by === "labels" ? [groupTitle] : groupTitle,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{displayFilters?.group_by !== "created_by" && (
|
{displayFilters?.group_by !== "created_by" && (
|
||||||
<div>
|
<div>
|
||||||
@ -177,7 +213,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
|
className="flex items-center gap-2 font-medium text-custom-primary outline-none p-1"
|
||||||
onClick={addIssueToGroup}
|
onClick={() => onCreateClick()}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
Add Issue
|
Add Issue
|
||||||
@ -197,7 +233,7 @@ export const SingleBoard: React.FC<Props> = ({
|
|||||||
position="left"
|
position="left"
|
||||||
noBorder
|
noBorder
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>
|
<CustomMenu.MenuItem onClick={() => onCreateClick()}>
|
||||||
Create new
|
Create new
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
{openIssuesListModal && (
|
{openIssuesListModal && (
|
||||||
|
@ -2,3 +2,4 @@ export * from "./calendar-header";
|
|||||||
export * from "./calendar";
|
export * from "./calendar";
|
||||||
export * from "./single-date";
|
export * from "./single-date";
|
||||||
export * from "./single-issue";
|
export * from "./single-issue";
|
||||||
|
export * from "./inline-create-issue-form";
|
||||||
|
@ -0,0 +1,91 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
// react hook form
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import { InlineCreateIssueFormWrapper } from "components/core";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useProjectDetails from "hooks/use-project-details";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||||
|
prePopulatedData?: Partial<IIssue>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const useCheckIfThereIsSpaceOnRight = (ref: React.RefObject<HTMLDivElement>) => {
|
||||||
|
const [isThereSpaceOnRight, setIsThereSpaceOnRight] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
|
||||||
|
const { right } = ref.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
const width = right + 250;
|
||||||
|
|
||||||
|
if (width > window.innerWidth) setIsThereSpaceOnRight(false);
|
||||||
|
else setIsThereSpaceOnRight(true);
|
||||||
|
}, [ref]);
|
||||||
|
|
||||||
|
return isThereSpaceOnRight;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InlineInput = () => {
|
||||||
|
const { projectDetails } = useProjectDetails();
|
||||||
|
|
||||||
|
const { register, setFocus } = useFormContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFocus("name");
|
||||||
|
}, [setFocus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4 className="text-sm font-medium leading-5 text-custom-text-400">
|
||||||
|
{projectDetails?.identifier ?? "..."}
|
||||||
|
</h4>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="Issue Title"
|
||||||
|
{...register("name", {
|
||||||
|
required: "Issue title is required.",
|
||||||
|
})}
|
||||||
|
className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CalendarInlineCreateIssueForm: React.FC<Props> = (props) => {
|
||||||
|
const { isOpen } = props;
|
||||||
|
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={`absolute -translate-x-1 top-5 transition-all z-20 ${
|
||||||
|
isOpen ? "opacity-100 scale-100" : "opacity-0 pointer-events-none scale-95"
|
||||||
|
} ${isSpaceOnRight ? "left-full" : "right-0"}`}
|
||||||
|
>
|
||||||
|
<InlineCreateIssueFormWrapper
|
||||||
|
{...props}
|
||||||
|
className="flex w-60 p-1 px-1.5 rounded items-center gap-x-3 bg-custom-background-100 shadow-custom-shadow-md transition-opacity"
|
||||||
|
>
|
||||||
|
<InlineInput />
|
||||||
|
</InlineCreateIssueFormWrapper>
|
||||||
|
</div>
|
||||||
|
{/* Added to make any other element as outside click. This will make input also to be outside. */}
|
||||||
|
{isOpen && <div className="w-screen h-screen fixed inset-0 z-10" />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,10 +1,14 @@
|
|||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// react-beautiful-dnd
|
// react-beautiful-dnd
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
// component
|
// component
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
import { SingleCalendarIssue } from "./single-issue";
|
import { SingleCalendarIssue } from "./single-issue";
|
||||||
|
import { CalendarInlineCreateIssueForm } from "./inline-create-issue-form";
|
||||||
// icons
|
// icons
|
||||||
import { PlusSmallIcon } from "@heroicons/react/24/outline";
|
import { PlusSmallIcon } from "@heroicons/react/24/outline";
|
||||||
// helper
|
// helper
|
||||||
@ -26,17 +30,14 @@ type Props = {
|
|||||||
isNotAllowed: boolean;
|
isNotAllowed: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleCalendarDate: React.FC<Props> = ({
|
export const SingleCalendarDate: React.FC<Props> = (props) => {
|
||||||
handleIssueAction,
|
const { handleIssueAction, date, index, isMonthlyView, showWeekEnds, user, isNotAllowed } = props;
|
||||||
date,
|
|
||||||
index,
|
const router = useRouter();
|
||||||
addIssueToDate,
|
const { cycleId, moduleId } = router.query;
|
||||||
isMonthlyView,
|
|
||||||
showWeekEnds,
|
|
||||||
user,
|
|
||||||
isNotAllowed,
|
|
||||||
}) => {
|
|
||||||
const [showAllIssues, setShowAllIssues] = useState(false);
|
const [showAllIssues, setShowAllIssues] = useState(false);
|
||||||
|
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
const totalIssues = date.issues.length;
|
const totalIssues = date.issues.length;
|
||||||
|
|
||||||
@ -78,6 +79,17 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<CalendarInlineCreateIssueForm
|
||||||
|
isOpen={isCreateIssueFormOpen}
|
||||||
|
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||||
|
prePopulatedData={{
|
||||||
|
target_date: date.date,
|
||||||
|
...(cycleId && { cycle: cycleId.toString() }),
|
||||||
|
...(moduleId && { module: moduleId.toString() }),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{totalIssues > 4 && (
|
{totalIssues > 4 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@ -93,7 +105,7 @@ export const SingleCalendarDate: React.FC<Props> = ({
|
|||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
className="flex items-center justify-center gap-1 text-center"
|
className="flex items-center justify-center gap-1 text-center"
|
||||||
onClick={() => addIssueToDate(date.date)}
|
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||||
>
|
>
|
||||||
<PlusSmallIcon className="h-4 w-4 text-custom-text-200" />
|
<PlusSmallIcon className="h-4 w-4 text-custom-text-200" />
|
||||||
Add issue
|
Add issue
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
// react hook form
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useProjectDetails from "hooks/use-project-details";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { InlineCreateIssueFormWrapper } from "components/core";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||||
|
prePopulatedData?: Partial<IIssue>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InlineInput = () => {
|
||||||
|
const { projectDetails } = useProjectDetails();
|
||||||
|
|
||||||
|
const { register, setFocus } = useFormContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFocus("name");
|
||||||
|
}, [setFocus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="w-[14px] h-[14px] rounded-full border border-custom-border-1000 flex-shrink-0" />
|
||||||
|
<h4 className="text-sm text-custom-text-400">{projectDetails?.identifier ?? "..."}</h4>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="Issue Title"
|
||||||
|
{...register("name", {
|
||||||
|
required: "Issue title is required.",
|
||||||
|
})}
|
||||||
|
className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GanttInlineCreateIssueForm: React.FC<Props> = (props) => (
|
||||||
|
<>
|
||||||
|
<InlineCreateIssueFormWrapper
|
||||||
|
className="flex py-3 px-4 mr-2.5 items-center rounded gap-x-2 border bg-custom-background-100 shadow-custom-shadow-sm"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<InlineInput />
|
||||||
|
</InlineCreateIssueFormWrapper>
|
||||||
|
{props.isOpen && (
|
||||||
|
<p className="text-xs ml-3 mt-3 italic text-custom-text-200">
|
||||||
|
Press {"'"}Enter{"'"} to add another issue
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
@ -5,3 +5,4 @@ export * from "./list-view";
|
|||||||
export * from "./spreadsheet-view";
|
export * from "./spreadsheet-view";
|
||||||
export * from "./all-views";
|
export * from "./all-views";
|
||||||
export * from "./issues-view";
|
export * from "./issues-view";
|
||||||
|
export * from "./inline-issue-create-wrapper";
|
||||||
|
270
web/components/core/views/inline-issue-create-wrapper.tsx
Normal file
270
web/components/core/views/inline-issue-create-wrapper.tsx
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// swr
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
|
// react hook form
|
||||||
|
import { useForm, FormProvider } from "react-hook-form";
|
||||||
|
|
||||||
|
// headless ui
|
||||||
|
import { Transition } from "@headlessui/react";
|
||||||
|
|
||||||
|
// services
|
||||||
|
import modulesService from "services/modules.service";
|
||||||
|
import issuesService from "services/issues.service";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useToast from "hooks/use-toast";
|
||||||
|
import useUser from "hooks/use-user";
|
||||||
|
import useKeypress from "hooks/use-keypress";
|
||||||
|
import useIssuesView from "hooks/use-issues-view";
|
||||||
|
import useMyIssues from "hooks/my-issues/use-my-issues";
|
||||||
|
import useGanttChartIssues from "hooks/gantt-chart/issue-view";
|
||||||
|
import useCalendarIssuesView from "hooks/use-calendar-issues-view";
|
||||||
|
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||||
|
import useSpreadsheetIssuesView from "hooks/use-spreadsheet-issues-view";
|
||||||
|
|
||||||
|
// helpers
|
||||||
|
import { getFetchKeysForIssueMutation } from "helpers/string.helper";
|
||||||
|
|
||||||
|
// fetch-keys
|
||||||
|
import {
|
||||||
|
USER_ISSUE,
|
||||||
|
SUB_ISSUES,
|
||||||
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
|
CYCLE_DETAILS,
|
||||||
|
MODULE_DETAILS,
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
const defaultValues: Partial<IIssue> = {
|
||||||
|
name: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||||
|
prePopulatedData?: Partial<IIssue>;
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addIssueToCycle = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
cycleId: string,
|
||||||
|
user: any,
|
||||||
|
params: any
|
||||||
|
) => {
|
||||||
|
if (!workspaceSlug || !projectId) return;
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.addIssueToCycle(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId.toString(),
|
||||||
|
cycleId,
|
||||||
|
{
|
||||||
|
issues: [issueId],
|
||||||
|
},
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
if (cycleId) {
|
||||||
|
mutate(CYCLE_ISSUES_WITH_PARAMS(cycleId, params));
|
||||||
|
mutate(CYCLE_DETAILS(cycleId as string));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addIssueToModule = async (
|
||||||
|
workspaceSlug: string,
|
||||||
|
projectId: string,
|
||||||
|
issueId: string,
|
||||||
|
moduleId: string,
|
||||||
|
user: any,
|
||||||
|
params: any
|
||||||
|
) => {
|
||||||
|
await modulesService
|
||||||
|
.addIssuesToModule(
|
||||||
|
workspaceSlug as string,
|
||||||
|
projectId.toString(),
|
||||||
|
moduleId as string,
|
||||||
|
{
|
||||||
|
issues: [issueId],
|
||||||
|
},
|
||||||
|
user
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
if (moduleId) {
|
||||||
|
mutate(MODULE_ISSUES_WITH_PARAMS(moduleId as string, params));
|
||||||
|
mutate(MODULE_DETAILS(moduleId as string));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InlineCreateIssueFormWrapper: React.FC<Props> = (props) => {
|
||||||
|
const { isOpen, handleClose, onSuccess, prePopulatedData, children, className } = props;
|
||||||
|
|
||||||
|
const ref = useRef<HTMLFormElement>(null);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const { workspaceSlug, projectId, cycleId, moduleId, viewId } = router.query;
|
||||||
|
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
|
const { setToastAlert } = useToast();
|
||||||
|
|
||||||
|
const { displayFilters, params } = useIssuesView();
|
||||||
|
const { params: calendarParams } = useCalendarIssuesView();
|
||||||
|
const { ...viewGanttParams } = params;
|
||||||
|
const { params: spreadsheetParams } = useSpreadsheetIssuesView();
|
||||||
|
const { groupedIssues, mutateMyIssues } = useMyIssues(workspaceSlug?.toString());
|
||||||
|
const { params: ganttParams } = useGanttChartIssues(
|
||||||
|
workspaceSlug?.toString(),
|
||||||
|
projectId?.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
const method = useForm<IIssue>({ defaultValues });
|
||||||
|
const {
|
||||||
|
reset,
|
||||||
|
handleSubmit,
|
||||||
|
getValues,
|
||||||
|
formState: { errors, isSubmitting },
|
||||||
|
} = method;
|
||||||
|
|
||||||
|
useOutsideClickDetector(ref, handleClose);
|
||||||
|
useKeypress("Escape", handleClose);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const values = getValues();
|
||||||
|
|
||||||
|
if (prePopulatedData) reset({ ...defaultValues, ...values, ...prePopulatedData });
|
||||||
|
}, [reset, prePopulatedData, getValues]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isOpen) reset({ ...defaultValues });
|
||||||
|
}, [isOpen, reset]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSubmitting)
|
||||||
|
setToastAlert({
|
||||||
|
type: "info",
|
||||||
|
title: "Creating issue...",
|
||||||
|
message: "Please wait while we create your issue.",
|
||||||
|
});
|
||||||
|
}, [isSubmitting, setToastAlert]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!errors) return;
|
||||||
|
|
||||||
|
Object.keys(errors).forEach((key) => {
|
||||||
|
const error = errors[key as keyof IIssue];
|
||||||
|
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: error?.message?.toString() || "Some error occurred. Please try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}, [errors, setToastAlert]);
|
||||||
|
|
||||||
|
const { calendarFetchKey, ganttFetchKey, spreadsheetFetchKey } = getFetchKeysForIssueMutation({
|
||||||
|
cycleId: cycleId,
|
||||||
|
moduleId: moduleId,
|
||||||
|
viewId: viewId,
|
||||||
|
projectId: projectId?.toString() ?? "",
|
||||||
|
calendarParams,
|
||||||
|
spreadsheetParams,
|
||||||
|
viewGanttParams,
|
||||||
|
ganttParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmitHandler = async (formData: IIssue) => {
|
||||||
|
if (!workspaceSlug || !projectId || !user || isSubmitting) return;
|
||||||
|
|
||||||
|
reset({ ...defaultValues });
|
||||||
|
|
||||||
|
await issuesService
|
||||||
|
.createIssues(workspaceSlug.toString(), projectId.toString(), formData, user)
|
||||||
|
.then(async (res) => {
|
||||||
|
mutate(PROJECT_ISSUES_LIST_WITH_PARAMS(projectId.toString(), params));
|
||||||
|
if (formData.cycle && formData.cycle !== "")
|
||||||
|
await addIssueToCycle(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
res.id,
|
||||||
|
formData.cycle,
|
||||||
|
user,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
if (formData.module && formData.module !== "")
|
||||||
|
await addIssueToModule(
|
||||||
|
workspaceSlug.toString(),
|
||||||
|
projectId.toString(),
|
||||||
|
res.id,
|
||||||
|
formData.module,
|
||||||
|
user,
|
||||||
|
params
|
||||||
|
);
|
||||||
|
|
||||||
|
if (displayFilters.layout === "calendar") mutate(calendarFetchKey);
|
||||||
|
if (displayFilters.layout === "gantt_chart") mutate(ganttFetchKey);
|
||||||
|
if (displayFilters.layout === "spreadsheet") mutate(spreadsheetFetchKey);
|
||||||
|
if (groupedIssues) mutateMyIssues();
|
||||||
|
|
||||||
|
setToastAlert({
|
||||||
|
type: "success",
|
||||||
|
title: "Success!",
|
||||||
|
message: "Issue created successfully.",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onSuccess) await onSuccess(res);
|
||||||
|
|
||||||
|
if (formData.assignees_list?.some((assignee) => assignee === user?.id))
|
||||||
|
mutate(USER_ISSUE(workspaceSlug as string));
|
||||||
|
|
||||||
|
if (formData.parent && formData.parent !== "") mutate(SUB_ISSUES(formData.parent));
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Object.keys(err || {}).forEach((key) => {
|
||||||
|
const error = err?.[key];
|
||||||
|
const errorTitle = error ? (Array.isArray(error) ? error.join(", ") : error) : null;
|
||||||
|
|
||||||
|
setToastAlert({
|
||||||
|
type: "error",
|
||||||
|
title: "Error!",
|
||||||
|
message: errorTitle || "Some error occurred. Please try again.",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Transition
|
||||||
|
show={isOpen}
|
||||||
|
enter="transition ease-in-out duration-200 transform"
|
||||||
|
enterFrom="opacity-0 scale-95"
|
||||||
|
enterTo="opacity-100 scale-100"
|
||||||
|
leave="transition ease-in-out duration-200 transform"
|
||||||
|
leaveFrom="opacity-100 scale-100"
|
||||||
|
leaveTo="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<FormProvider {...method}>
|
||||||
|
<form ref={ref} className={className} onSubmit={handleSubmit(onSubmitHandler)}>
|
||||||
|
{children}
|
||||||
|
</form>
|
||||||
|
</FormProvider>
|
||||||
|
</Transition>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,3 +1,4 @@
|
|||||||
export * from "./all-lists";
|
export * from "./all-lists";
|
||||||
export * from "./single-issue";
|
export * from "./single-issue";
|
||||||
export * from "./single-list";
|
export * from "./single-list";
|
||||||
|
export * from "./inline-create-issue-form";
|
||||||
|
@ -0,0 +1,55 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
// react hook form
|
||||||
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
// hooks
|
||||||
|
import useProjectDetails from "hooks/use-project-details";
|
||||||
|
|
||||||
|
// components
|
||||||
|
import { InlineCreateIssueFormWrapper } from "../inline-issue-create-wrapper";
|
||||||
|
|
||||||
|
// types
|
||||||
|
import { IIssue } from "types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
handleClose: () => void;
|
||||||
|
onSuccess?: (data: IIssue) => Promise<void> | void;
|
||||||
|
prePopulatedData?: Partial<IIssue>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InlineInput = () => {
|
||||||
|
const { projectDetails } = useProjectDetails();
|
||||||
|
|
||||||
|
const { register, setFocus } = useFormContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFocus("name");
|
||||||
|
}, [setFocus]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4 className="text-sm font-medium leading-5 text-custom-text-400">
|
||||||
|
{projectDetails?.identifier ?? "..."}
|
||||||
|
</h4>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
placeholder="Issue Title"
|
||||||
|
{...register("name", {
|
||||||
|
required: "Issue title is required.",
|
||||||
|
})}
|
||||||
|
className="w-full px-2 py-1.5 rounded-md bg-transparent text-sm font-medium leading-5 text-custom-text-200 outline-none"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ListInlineCreateIssueForm: React.FC<Props> = (props) => (
|
||||||
|
<InlineCreateIssueFormWrapper
|
||||||
|
className="flex py-3 px-4 items-center gap-x-5 bg-custom-background-100 shadow-custom-shadow-md"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<InlineInput />
|
||||||
|
</InlineCreateIssueFormWrapper>
|
||||||
|
);
|
@ -1,3 +1,6 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
// next
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@ -10,7 +13,7 @@ import projectService from "services/project.service";
|
|||||||
// hooks
|
// hooks
|
||||||
import useProjects from "hooks/use-projects";
|
import useProjects from "hooks/use-projects";
|
||||||
// components
|
// components
|
||||||
import { SingleListIssue } from "components/core";
|
import { SingleListIssue, ListInlineCreateIssueForm } from "components/core";
|
||||||
// ui
|
// ui
|
||||||
import { Avatar, CustomMenu } from "components/ui";
|
import { Avatar, CustomMenu } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
@ -51,10 +54,10 @@ type Props = {
|
|||||||
viewProps: IIssueViewProps;
|
viewProps: IIssueViewProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const SingleList: React.FC<Props> = ({
|
export const SingleList: React.FC<Props> = (props) => {
|
||||||
|
const {
|
||||||
currentState,
|
currentState,
|
||||||
groupTitle,
|
groupTitle,
|
||||||
addIssueToGroup,
|
|
||||||
handleIssueAction,
|
handleIssueAction,
|
||||||
openIssuesListModal,
|
openIssuesListModal,
|
||||||
handleDraftIssueAction,
|
handleDraftIssueAction,
|
||||||
@ -65,10 +68,13 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
user,
|
user,
|
||||||
userAuth,
|
userAuth,
|
||||||
viewProps,
|
viewProps,
|
||||||
}) => {
|
} = props;
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
|
|
||||||
|
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
const isArchivedIssues = router.pathname.includes("archived-issues");
|
const isArchivedIssues = router.pathname.includes("archived-issues");
|
||||||
|
|
||||||
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
const type = cycleId ? "cycle" : moduleId ? "module" : "issue";
|
||||||
@ -207,7 +213,7 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
className="p-1 text-custom-text-200 hover:bg-custom-background-80"
|
||||||
onClick={addIssueToGroup}
|
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
@ -224,7 +230,9 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
position="right"
|
position="right"
|
||||||
noBorder
|
noBorder
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem onClick={addIssueToGroup}>Create new</CustomMenu.MenuItem>
|
<CustomMenu.MenuItem onClick={() => setIsCreateIssueFormOpen(true)}>
|
||||||
|
Create new
|
||||||
|
</CustomMenu.MenuItem>
|
||||||
{openIssuesListModal && (
|
{openIssuesListModal && (
|
||||||
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
<CustomMenu.MenuItem onClick={openIssuesListModal}>
|
||||||
Add an existing issue
|
Add an existing issue
|
||||||
@ -284,6 +292,29 @@ export const SingleList: React.FC<Props> = ({
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
<div className="flex h-full w-full items-center justify-center">Loading...</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<ListInlineCreateIssueForm
|
||||||
|
isOpen={isCreateIssueFormOpen}
|
||||||
|
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||||
|
prePopulatedData={{
|
||||||
|
...(cycleId && { cycle: cycleId.toString() }),
|
||||||
|
...(moduleId && { module: moduleId.toString() }),
|
||||||
|
[displayFilters?.group_by!]: groupTitle,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isCreateIssueFormOpen && (
|
||||||
|
<div className="w-full bg-custom-background-100 px-6 py-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||||
|
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 py-1 rounded-md"
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</Disclosure.Panel>
|
</Disclosure.Panel>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,7 +4,7 @@ import React, { useState } from "react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { SpreadsheetColumns, SpreadsheetIssues } from "components/core";
|
import { SpreadsheetColumns, SpreadsheetIssues, ListInlineCreateIssueForm } from "components/core";
|
||||||
import { CustomMenu, Spinner } from "components/ui";
|
import { CustomMenu, Spinner } from "components/ui";
|
||||||
import { IssuePeekOverview } from "components/issues";
|
import { IssuePeekOverview } from "components/issues";
|
||||||
// hooks
|
// hooks
|
||||||
@ -33,6 +33,7 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
userAuth,
|
userAuth,
|
||||||
}) => {
|
}) => {
|
||||||
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
const [expandedIssues, setExpandedIssues] = useState<string[]>([]);
|
||||||
|
const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
|
||||||
@ -88,17 +89,26 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
userAuth={userAuth}
|
userAuth={userAuth}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
<ListInlineCreateIssueForm
|
||||||
|
isOpen={isInlineCreateIssueFormOpen}
|
||||||
|
handleClose={() => setIsInlineCreateIssueFormOpen(false)}
|
||||||
|
prePopulatedData={{
|
||||||
|
...(cycleId && { cycle: cycleId.toString() }),
|
||||||
|
...(moduleId && { module: moduleId.toString() }),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
|
className="relative group grid auto-rows-[minmax(44px,1fr)] hover:rounded-sm hover:bg-custom-background-80 border-b border-custom-border-200 w-full min-w-max"
|
||||||
style={{ gridTemplateColumns }}
|
style={{ gridTemplateColumns }}
|
||||||
>
|
>
|
||||||
|
{!isInlineCreateIssueFormOpen && (
|
||||||
|
<>
|
||||||
{type === "issue" ? (
|
{type === "issue" ? (
|
||||||
<button
|
<button
|
||||||
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
|
className="flex gap-1.5 items-center pl-7 py-2.5 text-sm sticky left-0 z-[1] text-custom-text-200 bg-custom-background-100 group-hover:text-custom-text-100 group-hover:bg-custom-background-80 border-custom-border-200 w-full"
|
||||||
onClick={() => {
|
onClick={() => setIsInlineCreateIssueFormOpen(true)}
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
Add Issue
|
Add Issue
|
||||||
@ -120,12 +130,7 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
optionsClassName="left-5 !w-36"
|
optionsClassName="left-5 !w-36"
|
||||||
noBorder
|
noBorder
|
||||||
>
|
>
|
||||||
<CustomMenu.MenuItem
|
<CustomMenu.MenuItem onClick={() => setIsInlineCreateIssueFormOpen(true)}>
|
||||||
onClick={() => {
|
|
||||||
const e = new KeyboardEvent("keydown", { key: "c" });
|
|
||||||
document.dispatchEvent(e);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Create new
|
Create new
|
||||||
</CustomMenu.MenuItem>
|
</CustomMenu.MenuItem>
|
||||||
{openIssuesListModal && (
|
{openIssuesListModal && (
|
||||||
@ -136,6 +141,8 @@ export const SpreadsheetView: React.FC<Props> = ({
|
|||||||
</CustomMenu>
|
</CustomMenu>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
// next
|
||||||
|
import { useRouter } from "next/router";
|
||||||
// react-beautiful-dnd
|
// react-beautiful-dnd
|
||||||
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd";
|
||||||
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
import StrictModeDroppable from "components/dnd/StrictModeDroppable";
|
||||||
@ -7,6 +10,9 @@ import { useChart } from "./hooks";
|
|||||||
import { Loader } from "components/ui";
|
import { Loader } from "components/ui";
|
||||||
// icons
|
// icons
|
||||||
import { EllipsisVerticalIcon } from "@heroicons/react/24/outline";
|
import { EllipsisVerticalIcon } from "@heroicons/react/24/outline";
|
||||||
|
import { PlusIcon } from "lucide-react";
|
||||||
|
// components
|
||||||
|
import { GanttInlineCreateIssueForm } from "components/core/views/gantt-chart-view/inline-create-issue-form";
|
||||||
// types
|
// types
|
||||||
import { IBlockUpdateData, IGanttBlock } from "./types";
|
import { IBlockUpdateData, IGanttBlock } from "./types";
|
||||||
|
|
||||||
@ -18,15 +24,16 @@ type Props = {
|
|||||||
enableReorder: boolean;
|
enableReorder: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const GanttSidebar: React.FC<Props> = ({
|
export const GanttSidebar: React.FC<Props> = (props) => {
|
||||||
title,
|
const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props;
|
||||||
blockUpdateHandler,
|
|
||||||
blocks,
|
const router = useRouter();
|
||||||
SidebarBlockRender,
|
const { cycleId, moduleId } = router.query;
|
||||||
enableReorder,
|
|
||||||
}) => {
|
|
||||||
const { activeBlock, dispatch } = useChart();
|
const { activeBlock, dispatch } = useChart();
|
||||||
|
|
||||||
|
const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false);
|
||||||
|
|
||||||
// update the active block on hover
|
// update the active block on hover
|
||||||
const updateActiveBlock = (block: IGanttBlock | null) => {
|
const updateActiveBlock = (block: IGanttBlock | null) => {
|
||||||
dispatch({
|
dispatch({
|
||||||
@ -148,6 +155,28 @@ export const GanttSidebar: React.FC<Props> = ({
|
|||||||
)}
|
)}
|
||||||
{droppableProvided.placeholder}
|
{droppableProvided.placeholder}
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
<GanttInlineCreateIssueForm
|
||||||
|
isOpen={isCreateIssueFormOpen}
|
||||||
|
handleClose={() => setIsCreateIssueFormOpen(false)}
|
||||||
|
prePopulatedData={{
|
||||||
|
start_date: new Date(Date.now()).toISOString().split("T")[0],
|
||||||
|
target_date: new Date(Date.now() + 86400000).toISOString().split("T")[0],
|
||||||
|
...(cycleId && { cycle: cycleId.toString() }),
|
||||||
|
...(moduleId && { module: moduleId.toString() }),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!isCreateIssueFormOpen && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsCreateIssueFormOpen(true)}
|
||||||
|
className="flex items-center gap-x-[6px] text-custom-primary-100 px-2 py-1 rounded-md mt-3"
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
<span className="text-sm font-medium text-custom-primary-100">New Issue</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</StrictModeDroppable>
|
</StrictModeDroppable>
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
import {
|
||||||
|
CYCLE_ISSUES_WITH_PARAMS,
|
||||||
|
MODULE_ISSUES_WITH_PARAMS,
|
||||||
|
PROJECT_ISSUES_LIST_WITH_PARAMS,
|
||||||
|
VIEW_ISSUES,
|
||||||
|
} from "constants/fetch-keys";
|
||||||
|
|
||||||
export const addSpaceIfCamelCase = (str: string) => str.replace(/([a-z])([A-Z])/g, "$1 $2");
|
export const addSpaceIfCamelCase = (str: string) => str.replace(/([a-z])([A-Z])/g, "$1 $2");
|
||||||
|
|
||||||
export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
|
export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
|
||||||
@ -122,3 +129,65 @@ export const objToQueryParams = (obj: any) => {
|
|||||||
|
|
||||||
return params.toString();
|
return params.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getFetchKeysForIssueMutation = (options: {
|
||||||
|
cycleId?: string | string[];
|
||||||
|
moduleId?: string | string[];
|
||||||
|
viewId?: string | string[];
|
||||||
|
projectId: string;
|
||||||
|
calendarParams: any;
|
||||||
|
spreadsheetParams: any;
|
||||||
|
viewGanttParams: any;
|
||||||
|
ganttParams: any;
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
cycleId,
|
||||||
|
moduleId,
|
||||||
|
viewId,
|
||||||
|
projectId,
|
||||||
|
calendarParams,
|
||||||
|
spreadsheetParams,
|
||||||
|
viewGanttParams,
|
||||||
|
ganttParams,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const calendarFetchKey = cycleId
|
||||||
|
? { calendarFetchKey: CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), calendarParams) }
|
||||||
|
: moduleId
|
||||||
|
? { calendarFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), calendarParams) }
|
||||||
|
: viewId
|
||||||
|
? { calendarFetchKey: VIEW_ISSUES(viewId.toString(), calendarParams) }
|
||||||
|
: {
|
||||||
|
calendarFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(
|
||||||
|
projectId?.toString() ?? "",
|
||||||
|
calendarParams
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const spreadsheetFetchKey = cycleId
|
||||||
|
? { spreadsheetFetchKey: CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), spreadsheetParams) }
|
||||||
|
: moduleId
|
||||||
|
? { spreadsheetFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), spreadsheetParams) }
|
||||||
|
: viewId
|
||||||
|
? { spreadsheetFetchKey: VIEW_ISSUES(viewId.toString(), spreadsheetParams) }
|
||||||
|
: {
|
||||||
|
spreadsheetFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(
|
||||||
|
projectId?.toString() ?? "",
|
||||||
|
spreadsheetParams
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
const ganttFetchKey = cycleId
|
||||||
|
? { ganttFetchKey: CYCLE_ISSUES_WITH_PARAMS(cycleId.toString(), ganttParams) }
|
||||||
|
: moduleId
|
||||||
|
? { ganttFetchKey: MODULE_ISSUES_WITH_PARAMS(moduleId.toString(), ganttParams) }
|
||||||
|
: viewId
|
||||||
|
? { ganttFetchKey: VIEW_ISSUES(viewId.toString(), viewGanttParams) }
|
||||||
|
: { ganttFetchKey: PROJECT_ISSUES_LIST_WITH_PARAMS(projectId?.toString() ?? "", ganttParams) };
|
||||||
|
|
||||||
|
return {
|
||||||
|
...calendarFetchKey,
|
||||||
|
...spreadsheetFetchKey,
|
||||||
|
...ganttFetchKey,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -36,6 +36,7 @@ const useGanttChartIssues = (workspaceSlug: string | undefined, projectId: strin
|
|||||||
return {
|
return {
|
||||||
ganttIssues,
|
ganttIssues,
|
||||||
mutateGanttIssues,
|
mutateGanttIssues,
|
||||||
|
params,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
19
web/hooks/use-keypress.tsx
Normal file
19
web/hooks/use-keypress.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
const useKeypress = (key: string, callback: () => void) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeydown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === key) {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener("keydown", handleKeydown);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener("keydown", handleKeydown);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useKeypress;
|
Loading…
Reference in New Issue
Block a user