diff --git a/web/components/core/views/board-view/index.ts b/web/components/core/views/board-view/index.ts index 6e5cdf8bf..a5a6ee497 100644 --- a/web/components/core/views/board-view/index.ts +++ b/web/components/core/views/board-view/index.ts @@ -2,3 +2,4 @@ export * from "./all-boards"; export * from "./board-header"; export * from "./single-board"; export * from "./single-issue"; +export * from "./inline-create-issue-form"; diff --git a/web/components/core/views/board-view/inline-create-issue-form.tsx b/web/components/core/views/board-view/inline-create-issue-form.tsx new file mode 100644 index 000000000..f4810164d --- /dev/null +++ b/web/components/core/views/board-view/inline-create-issue-form.tsx @@ -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; + prePopulatedData?: Partial; +}; + +const InlineInput = () => { + const { projectDetails } = useProjectDetails(); + + const { register, setFocus } = useFormContext(); + + useEffect(() => { + setFocus("name"); + }, [setFocus]); + + return ( +
+

+ {projectDetails?.identifier ?? "..."} +

+ +
+ ); +}; + +export const BoardInlineCreateIssueForm: React.FC = (props) => ( + <> + + + + {props.isOpen && ( +

+ Press {"'"}Enter{"'"} to add another issue +

+ )} + +); diff --git a/web/components/core/views/board-view/single-board.tsx b/web/components/core/views/board-view/single-board.tsx index 1981e1f7c..3e174ead2 100644 --- a/web/components/core/views/board-view/single-board.tsx +++ b/web/components/core/views/board-view/single-board.tsx @@ -6,7 +6,7 @@ import { useRouter } from "next/router"; import StrictModeDroppable from "components/dnd/StrictModeDroppable"; import { Draggable } from "react-beautiful-dnd"; // components -import { BoardHeader, SingleBoardIssue } from "components/core"; +import { BoardHeader, SingleBoardIssue, BoardInlineCreateIssueForm } from "components/core"; // ui import { CustomMenu } from "components/ui"; // icons @@ -34,26 +34,30 @@ type Props = { viewProps: IIssueViewProps; }; -export const SingleBoard: React.FC = ({ - addIssueToGroup, - currentState, - groupTitle, - disableUserActions, - disableAddIssueOption = false, - dragDisabled, - handleIssueAction, - handleDraftIssueAction, - handleTrashBox, - openIssuesListModal, - handleMyIssueOpen, - removeIssue, - user, - userAuth, - viewProps, -}) => { +export const SingleBoard: React.FC = (props) => { + const { + addIssueToGroup, + currentState, + groupTitle, + disableUserActions, + disableAddIssueOption = false, + dragDisabled, + handleIssueAction, + handleDraftIssueAction, + handleTrashBox, + openIssuesListModal, + handleMyIssueOpen, + removeIssue, + user, + userAuth, + viewProps, + } = props; + // collapse/expand const [isCollapsed, setIsCollapsed] = useState(true); + const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false); + const { displayFilters, groupedIssues } = viewProps; const router = useRouter(); @@ -67,6 +71,24 @@ export const SingleBoard: React.FC = ({ 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 (
= ({ )}
= ({ > <>{provided.placeholder} + + 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, + }} + />
{displayFilters?.group_by !== "created_by" && (
@@ -177,7 +213,7 @@ export const SingleBoard: React.FC = ({ @@ -224,7 +230,9 @@ export const SingleList: React.FC = ({ position="right" noBorder > - Create new + setIsCreateIssueFormOpen(true)}> + Create new + {openIssuesListModal && ( Add an existing issue @@ -284,6 +292,29 @@ export const SingleList: React.FC = ({ ) : (
Loading...
)} + + setIsCreateIssueFormOpen(false)} + prePopulatedData={{ + ...(cycleId && { cycle: cycleId.toString() }), + ...(moduleId && { module: moduleId.toString() }), + [displayFilters?.group_by!]: groupTitle, + }} + /> + + {!isCreateIssueFormOpen && ( +
+ +
+ )}
diff --git a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx index 1076f30d0..0b4634b97 100644 --- a/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx +++ b/web/components/core/views/spreadsheet-view/spreadsheet-view.tsx @@ -4,7 +4,7 @@ import React, { useState } from "react"; import { useRouter } from "next/router"; // components -import { SpreadsheetColumns, SpreadsheetIssues } from "components/core"; +import { SpreadsheetColumns, SpreadsheetIssues, ListInlineCreateIssueForm } from "components/core"; import { CustomMenu, Spinner } from "components/ui"; import { IssuePeekOverview } from "components/issues"; // hooks @@ -33,6 +33,7 @@ export const SpreadsheetView: React.FC = ({ userAuth, }) => { const [expandedIssues, setExpandedIssues] = useState([]); + const [isInlineCreateIssueFormOpen, setIsInlineCreateIssueFormOpen] = useState(false); const router = useRouter(); const { workspaceSlug, projectId, cycleId, moduleId } = router.query; @@ -88,53 +89,59 @@ export const SpreadsheetView: React.FC = ({ userAuth={userAuth} /> ))} + + setIsInlineCreateIssueFormOpen(false)} + prePopulatedData={{ + ...(cycleId && { cycle: cycleId.toString() }), + ...(moduleId && { module: moduleId.toString() }), + }} + /> +
- {type === "issue" ? ( - - ) : ( - !disableUserActions && ( - - - Add Issue - - } - position="left" - optionsClassName="left-5 !w-36" - noBorder - > - { - const e = new KeyboardEvent("keydown", { key: "c" }); - document.dispatchEvent(e); - }} + {!isInlineCreateIssueFormOpen && ( + <> + {type === "issue" ? ( + + ) : ( + !disableUserActions && ( + + + Add Issue + + } + position="left" + optionsClassName="left-5 !w-36" + noBorder + > + setIsInlineCreateIssueFormOpen(true)}> + Create new + + {openIssuesListModal && ( + + Add an existing issue + + )} + + ) + )} + )}
diff --git a/web/components/gantt-chart/sidebar.tsx b/web/components/gantt-chart/sidebar.tsx index 92e7a603d..0d90ffdd0 100644 --- a/web/components/gantt-chart/sidebar.tsx +++ b/web/components/gantt-chart/sidebar.tsx @@ -1,3 +1,6 @@ +import { useState } from "react"; +// next +import { useRouter } from "next/router"; // react-beautiful-dnd import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd"; import StrictModeDroppable from "components/dnd/StrictModeDroppable"; @@ -7,6 +10,9 @@ import { useChart } from "./hooks"; import { Loader } from "components/ui"; // icons 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 import { IBlockUpdateData, IGanttBlock } from "./types"; @@ -18,15 +24,16 @@ type Props = { enableReorder: boolean; }; -export const GanttSidebar: React.FC = ({ - title, - blockUpdateHandler, - blocks, - SidebarBlockRender, - enableReorder, -}) => { +export const GanttSidebar: React.FC = (props) => { + const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props; + + const router = useRouter(); + const { cycleId, moduleId } = router.query; + const { activeBlock, dispatch } = useChart(); + const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false); + // update the active block on hover const updateActiveBlock = (block: IGanttBlock | null) => { dispatch({ @@ -148,6 +155,28 @@ export const GanttSidebar: React.FC = ({ )} {droppableProvided.placeholder} + + 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 && ( + + )} )} diff --git a/web/helpers/string.helper.ts b/web/helpers/string.helper.ts index 0fc84fda1..d1e0e98b7 100644 --- a/web/helpers/string.helper.ts +++ b/web/helpers/string.helper.ts @@ -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 replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " "); @@ -122,3 +129,65 @@ export const objToQueryParams = (obj: any) => { 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, + }; +}; diff --git a/web/hooks/gantt-chart/issue-view.tsx b/web/hooks/gantt-chart/issue-view.tsx index 8b24a566c..c2f6972fa 100644 --- a/web/hooks/gantt-chart/issue-view.tsx +++ b/web/hooks/gantt-chart/issue-view.tsx @@ -36,6 +36,7 @@ const useGanttChartIssues = (workspaceSlug: string | undefined, projectId: strin return { ganttIssues, mutateGanttIssues, + params, }; }; diff --git a/web/hooks/use-keypress.tsx b/web/hooks/use-keypress.tsx new file mode 100644 index 000000000..d04cd1445 --- /dev/null +++ b/web/hooks/use-keypress.tsx @@ -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;