diff --git a/.gitignore b/.gitignore index 8bf25a3f31..7568602d31 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,8 @@ node_modules # Production /build -dist +dist/ +out/ # Misc .DS_Store diff --git a/packages/editor/core/package.json b/packages/editor/core/package.json index 78bf59db76..2c35ead1c8 100644 --- a/packages/editor/core/package.json +++ b/packages/editor/core/package.json @@ -27,6 +27,7 @@ "next-themes": "^0.2.1" }, "dependencies": { + "react-moveable" : "^0.54.2", "@blueprintjs/popover2": "^2.0.10", "@tiptap/core": "^2.1.7", "@tiptap/extension-color": "^2.1.11", diff --git a/packages/ui/package.json b/packages/ui/package.json index ab5f6c451b..3a89a5c71d 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -16,6 +16,7 @@ "clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist" }, "devDependencies": { + "@types/react-color" : "^3.0.9", "@types/node": "^20.5.2", "@types/react": "18.2.0", "@types/react-dom": "18.2.0", diff --git a/space/next.config.js b/space/next.config.js index bd3749f102..7e98657847 100644 --- a/space/next.config.js +++ b/space/next.config.js @@ -8,6 +8,9 @@ const nextConfig = { experimental: { outputFileTracingRoot: path.join(__dirname, "../"), }, + images: { + unoptimized: true, + }, output: "standalone", }; diff --git a/space/package.json b/space/package.json index b2b064a534..5f3f60dc9c 100644 --- a/space/package.json +++ b/space/package.json @@ -7,7 +7,8 @@ "develop": "next dev -p 4000", "build": "next build", "start": "next start -p 4000", - "lint": "next lint" + "lint": "next lint", + "export": "next export" }, "dependencies": { "@blueprintjs/core": "^4.16.3", @@ -16,6 +17,8 @@ "@emotion/styled": "^11.11.0", "@headlessui/react": "^1.7.13", "@mui/material": "^5.14.1", + "@plane/ui": "*", + "@plane/lite-text-editor": "*", "@plane/rich-text-editor": "*", "axios": "^1.3.4", "clsx": "^2.0.0", diff --git a/web/components/estimates/estimate-select.tsx b/web/components/estimates/estimate-select.tsx new file mode 100644 index 0000000000..5ac283b838 --- /dev/null +++ b/web/components/estimates/estimate-select.tsx @@ -0,0 +1,160 @@ +import React, { useState } from "react"; +import { usePopper } from "react-popper"; +import { Combobox } from "@headlessui/react"; +import { Check, ChevronDown, Search, Triangle } from "lucide-react"; +// types +import { Tooltip } from "components/ui"; +import { Placement } from "@popperjs/core"; +// constants +import { IEstimatePoint } from "types"; + +type Props = { + value: number | null; + onChange: (value: number | null) => void; + estimatePoints: IEstimatePoint[] | undefined; + className?: string; + buttonClassName?: string; + optionsClassName?: string; + placement?: Placement; + hideDropdownArrow?: boolean; + disabled?: boolean; +}; + +export const EstimateSelect: React.FC = (props) => { + const { + value, + onChange, + estimatePoints, + className = "", + buttonClassName = "", + optionsClassName = "", + placement, + hideDropdownArrow = false, + disabled = false, + } = props; + + const [query, setQuery] = useState(""); + + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], + }); + + const options: { value: number | null; query: string; content: any }[] | undefined = estimatePoints?.map( + (estimate) => ({ + value: estimate.key, + query: estimate.value, + content: ( +
+ + {estimate.value} +
+ ), + }) + ); + options?.unshift({ + value: null, + query: "none", + content: ( +
+ + None +
+ ), + }); + + const filteredOptions = + query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); + + const selectedEstimate = estimatePoints?.find((e) => e.key === value); + const label = ( + +
+ + {selectedEstimate?.value ?? "None"} +
+
+ ); + + return ( + onChange(val as number | null)} + disabled={disabled} + > + + + + +
+
+ + setQuery(e.target.value)} + placeholder="Search" + displayValue={(assigned: any) => assigned?.name} + /> +
+
+ {filteredOptions ? ( + filteredOptions.length > 0 ? ( + filteredOptions.map((option) => ( + + `flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${ + active ? "bg-custom-background-80" : "" + } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` + } + > + {({ selected }) => ( + <> + {option.content} + {selected && } + + )} + + )) + ) : ( + +

No matching results

+
+ ) + ) : ( +

Loading...

+ )} +
+
+
+
+ ); +}; diff --git a/web/components/estimates/index.tsx b/web/components/estimates/index.ts similarity index 77% rename from web/components/estimates/index.tsx rename to web/components/estimates/index.ts index f20c747808..b88ceaf03a 100644 --- a/web/components/estimates/index.tsx +++ b/web/components/estimates/index.ts @@ -1,3 +1,4 @@ export * from "./create-update-estimate-modal"; -export * from "./single-estimate"; export * from "./delete-estimate-modal"; +export * from "./estimate-select"; +export * from "./single-estimate"; diff --git a/web/components/gantt-chart/chart/index.tsx b/web/components/gantt-chart/chart/index.tsx index bcee02ebc0..82a7ed6143 100644 --- a/web/components/gantt-chart/chart/index.tsx +++ b/web/components/gantt-chart/chart/index.tsx @@ -1,6 +1,4 @@ import { FC, useEffect, useState } from "react"; -// next -import { useRouter } from "next/router"; // icons // components import { GanttChartBlocks } from "components/gantt-chart"; @@ -13,7 +11,7 @@ import { MonthChartView } from "./month"; // import { QuarterChartView } from "./quarter"; // import { YearChartView } from "./year"; // icons -import { Expand, PlusIcon, Shrink } from "lucide-react"; +import { Expand, Shrink } from "lucide-react"; // views import { // generateHourChart, @@ -28,7 +26,6 @@ import { // getNumberOfDaysBetweenTwoDatesInYear, getMonthChartItemPositionWidthInMonth, } from "../views"; -// import { GanttInlineCreateIssueForm } from "components/core/views/gantt-chart-view/inline-create-issue-form"; // types import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types"; // data @@ -65,15 +62,9 @@ export const ChartViewRoot: FC = ({ enableReorder, bottomSpacing, }) => { - // router - const router = useRouter(); - const { cycleId, moduleId } = router.query; - const isCyclePage = router.pathname.split("/")[4] === "cycles" && !cycleId; - const isModulePage = router.pathname.split("/")[4] === "modules" && !moduleId; // states const [itemsContainerWidth, setItemsContainerWidth] = useState(0); const [fullScreenMode, setFullScreenMode] = useState(false); - const [isCreateIssueFormOpen, setIsCreateIssueFormOpen] = useState(false); const [chartBlocks, setChartBlocks] = useState(null); // blocks state management starts // hooks const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } = useChart(); @@ -297,44 +288,6 @@ export const ChartViewRoot: FC = ({ SidebarBlockRender={SidebarBlockRender} enableReorder={enableReorder} /> - {chartBlocks && !(isCyclePage || isModulePage) && ( -
- {/* setIsCreateIssueFormOpen(false)} - onSuccess={() => { - const ganttSidebar = document.getElementById(`gantt-sidebar-${cycleId}`); - - const timeoutId = setTimeout(() => { - if (ganttSidebar) - ganttSidebar.scrollBy({ - top: ganttSidebar.scrollHeight, - left: 0, - behavior: "smooth", - }); - clearTimeout(timeoutId); - }, 10); - }} - 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 && ( - - )} -
- )}
void; + blocks: IGanttBlock[] | null; + SidebarBlockRender: React.FC; + enableReorder: boolean; +}; + +export const GanttSidebar: React.FC = (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props; + + const router = useRouter(); + const { cycleId } = router.query; + + const { activeBlock, dispatch } = useChart(); + + // update the active block on hover + const updateActiveBlock = (block: IGanttBlock | null) => { + dispatch({ + type: "PARTIAL_UPDATE", + payload: { + activeBlock: block, + }, + }); + }; + + const handleOrderChange = (result: DropResult) => { + if (!blocks) return; + + const { source, destination } = result; + + // return if dropped outside the list + if (!destination) return; + + // return if dropped on the same index + if (source.index === destination.index) return; + + let updatedSortOrder = blocks[source.index].sort_order; + + // update the sort order to the lowest if dropped at the top + if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; + // update the sort order to the highest if dropped at the bottom + else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; + // update the sort order to the average of the two adjacent blocks if dropped in between + else { + const destinationSortingOrder = blocks[destination.index].sort_order; + const relativeDestinationSortingOrder = + source.index < destination.index + ? blocks[destination.index + 1].sort_order + : blocks[destination.index - 1].sort_order; + + updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; + } + + // extract the element from the source index and insert it at the destination index without updating the entire array + const removedElement = blocks.splice(source.index, 1)[0]; + blocks.splice(destination.index, 0, removedElement); + + // call the block update handler with the updated sort order, new and old index + blockUpdateHandler(removedElement.data, { + sort_order: { + destinationIndex: destination.index, + newSortOrder: updatedSortOrder, + sourceIndex: source.index, + }, + }); + }; + + return ( + + + {(droppableProvided) => ( +
+ <> + {blocks ? ( + blocks.map((block, index) => { + const duration = findTotalDaysInRange(block.start_date ?? "", block.target_date ?? "", true); + + return ( + + {(provided, snapshot) => ( +
updateActiveBlock(block)} + onMouseLeave={() => updateActiveBlock(null)} + ref={provided.innerRef} + {...provided.draggableProps} + > +
+ {enableReorder && ( + + )} +
+
+ +
+
+ {duration} day{duration > 1 ? "s" : ""} +
+
+
+
+ )} +
+ ); + }) + ) : ( + + + + + + + )} + {droppableProvided.placeholder} + +
+ )} +
+
+ ); +}; diff --git a/web/components/gantt-chart/module-sidebar.tsx b/web/components/gantt-chart/module-sidebar.tsx new file mode 100644 index 0000000000..4b0b654f2c --- /dev/null +++ b/web/components/gantt-chart/module-sidebar.tsx @@ -0,0 +1,158 @@ +import { useRouter } from "next/router"; +import { DragDropContext, Draggable, DropResult } from "react-beautiful-dnd"; +import StrictModeDroppable from "components/dnd/StrictModeDroppable"; +import { MoreVertical } from "lucide-react"; +// hooks +import { useChart } from "./hooks"; +// ui +import { Loader } from "@plane/ui"; +// helpers +import { findTotalDaysInRange } from "helpers/date-time.helper"; +// types +import { IBlockUpdateData, IGanttBlock } from "./types"; + +type Props = { + title: string; + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + blocks: IGanttBlock[] | null; + SidebarBlockRender: React.FC; + enableReorder: boolean; +}; + +export const GanttSidebar: React.FC = (props) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props; + + const router = useRouter(); + const { cycleId } = router.query; + + const { activeBlock, dispatch } = useChart(); + + // update the active block on hover + const updateActiveBlock = (block: IGanttBlock | null) => { + dispatch({ + type: "PARTIAL_UPDATE", + payload: { + activeBlock: block, + }, + }); + }; + + const handleOrderChange = (result: DropResult) => { + if (!blocks) return; + + const { source, destination } = result; + + // return if dropped outside the list + if (!destination) return; + + // return if dropped on the same index + if (source.index === destination.index) return; + + let updatedSortOrder = blocks[source.index].sort_order; + + // update the sort order to the lowest if dropped at the top + if (destination.index === 0) updatedSortOrder = blocks[0].sort_order - 1000; + // update the sort order to the highest if dropped at the bottom + else if (destination.index === blocks.length - 1) updatedSortOrder = blocks[blocks.length - 1].sort_order + 1000; + // update the sort order to the average of the two adjacent blocks if dropped in between + else { + const destinationSortingOrder = blocks[destination.index].sort_order; + const relativeDestinationSortingOrder = + source.index < destination.index + ? blocks[destination.index + 1].sort_order + : blocks[destination.index - 1].sort_order; + + updatedSortOrder = (destinationSortingOrder + relativeDestinationSortingOrder) / 2; + } + + // extract the element from the source index and insert it at the destination index without updating the entire array + const removedElement = blocks.splice(source.index, 1)[0]; + blocks.splice(destination.index, 0, removedElement); + + // call the block update handler with the updated sort order, new and old index + blockUpdateHandler(removedElement.data, { + sort_order: { + destinationIndex: destination.index, + newSortOrder: updatedSortOrder, + sourceIndex: source.index, + }, + }); + }; + + return ( + + + {(droppableProvided) => ( +
+ <> + {blocks ? ( + blocks.map((block, index) => { + const duration = findTotalDaysInRange(block.start_date ?? "", block.target_date ?? "", true); + + return ( + + {(provided, snapshot) => ( +
updateActiveBlock(block)} + onMouseLeave={() => updateActiveBlock(null)} + ref={provided.innerRef} + {...provided.draggableProps} + > +
+ {enableReorder && ( + + )} +
+
+ +
+
+ {duration} day{duration > 1 ? "s" : ""} +
+
+
+
+ )} +
+ ); + }) + ) : ( + + + + + + + )} + {droppableProvided.placeholder} + +
+ )} +
+
+ ); +}; diff --git a/web/components/gantt-chart/sidebar.tsx b/web/components/gantt-chart/sidebar.tsx index 4b0b654f2c..0e7dae0480 100644 --- a/web/components/gantt-chart/sidebar.tsx +++ b/web/components/gantt-chart/sidebar.tsx @@ -6,6 +6,8 @@ import { MoreVertical } from "lucide-react"; import { useChart } from "./hooks"; // ui import { Loader } from "@plane/ui"; +// components +import { GanttInlineCreateIssueForm } from "components/issues"; // helpers import { findTotalDaysInRange } from "helpers/date-time.helper"; // types @@ -17,11 +19,12 @@ type Props = { blocks: IGanttBlock[] | null; SidebarBlockRender: React.FC; enableReorder: boolean; + enableQuickIssueCreate?: boolean; }; export const GanttSidebar: React.FC = (props) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder } = props; + const { title, blockUpdateHandler, blocks, SidebarBlockRender, enableReorder, enableQuickIssueCreate } = props; const router = useRouter(); const { cycleId } = router.query; @@ -150,6 +153,7 @@ export const GanttSidebar: React.FC = (props) => { )} {droppableProvided.placeholder} +
)} diff --git a/web/components/headers/index.ts b/web/components/headers/index.ts index 0657b5ae30..1a52dd4de4 100644 --- a/web/components/headers/index.ts +++ b/web/components/headers/index.ts @@ -18,3 +18,4 @@ export * from "./project-draft-issues"; export * from "./project-archived-issue-details"; export * from "./project-archived-issues"; export * from "./project-issue-details"; +export * from "./user-profile"; diff --git a/web/components/headers/user-profile.tsx b/web/components/headers/user-profile.tsx new file mode 100644 index 0000000000..870aa1b54b --- /dev/null +++ b/web/components/headers/user-profile.tsx @@ -0,0 +1,25 @@ +import { FC } from "react"; + +import { useRouter } from "next/router"; + +// ui +import { BreadcrumbItem, Breadcrumbs } from "@plane/ui"; +// hooks +import { observer } from "mobx-react-lite"; + +export const UserProfileHeader: FC = observer(() => { + const router = useRouter(); + return ( +
+
+
+ router.back()}> + + +
+
+
+ ); +}); diff --git a/web/components/issues/issue-layouts/calendar/calendar.tsx b/web/components/issues/issue-layouts/calendar/calendar.tsx index 8b31c1eafa..9b7bfcaf95 100644 --- a/web/components/issues/issue-layouts/calendar/calendar.tsx +++ b/web/components/issues/issue-layouts/calendar/calendar.tsx @@ -44,12 +44,23 @@ export const CalendarChart: React.FC = observer((props) => {
{allWeeksOfActiveMonth && Object.values(allWeeksOfActiveMonth).map((week: ICalendarWeek, weekIndex) => ( - + ))}
)} {layout === "week" && ( - + )} diff --git a/web/components/issues/issue-layouts/calendar/day-tile.tsx b/web/components/issues/issue-layouts/calendar/day-tile.tsx index 7d07ce7f45..2dacc03eeb 100644 --- a/web/components/issues/issue-layouts/calendar/day-tile.tsx +++ b/web/components/issues/issue-layouts/calendar/day-tile.tsx @@ -4,7 +4,7 @@ import { Droppable } from "@hello-pangea/dnd"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { CalendarIssueBlocks, ICalendarDate } from "components/issues"; +import { CalendarIssueBlocks, ICalendarDate, CalendarInlineCreateIssueForm } from "components/issues"; // helpers import { renderDateFormat } from "helpers/date-time.helper"; // types @@ -17,10 +17,11 @@ type Props = { date: ICalendarDate; issues: IIssueGroupedStructure | null; quickActions: (issue: IIssue) => React.ReactNode; + enableQuickIssueCreate?: boolean; }; export const CalendarDayTile: React.FC = observer((props) => { - const { date, issues, quickActions } = props; + const { date, issues, quickActions, enableQuickIssueCreate } = props; const { issueFilter: issueFilterStore } = useMobxStore(); @@ -29,35 +30,56 @@ export const CalendarDayTile: React.FC = observer((props) => { const issuesList = issues ? (issues as IIssueGroupedStructure)[renderDateFormat(date.date)] : null; return ( - - {(provided, snapshot) => ( + <> +
+ {/* header */}
- <> -
- {date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "} - {date.date.getDate()} -
- - {provided.placeholder} - + {date.date.getDate() === 1 && MONTHS_LIST[date.date.getMonth() + 1].shortTitle + " "} + {date.date.getDate()}
- )} - + + {/* content */} +
+ + {(provided, snapshot) => ( +
+ + {enableQuickIssueCreate && ( +
+ +
+ )} + {provided.placeholder} +
+ )} +
+
+
+ ); }); diff --git a/web/components/issues/issue-layouts/calendar/index.ts b/web/components/issues/issue-layouts/calendar/index.ts index be43839547..a33cf557b1 100644 --- a/web/components/issues/issue-layouts/calendar/index.ts +++ b/web/components/issues/issue-layouts/calendar/index.ts @@ -7,3 +7,4 @@ export * from "./header"; export * from "./issue-blocks"; export * from "./week-days"; export * from "./week-header"; +export * from "./inline-create-issue-form"; diff --git a/web/components/issues/issue-layouts/calendar/inline-create-issue-form.tsx b/web/components/issues/issue-layouts/calendar/inline-create-issue-form.tsx new file mode 100644 index 0000000000..3ab74c368b --- /dev/null +++ b/web/components/issues/issue-layouts/calendar/inline-create-issue-form.tsx @@ -0,0 +1,234 @@ +import { useEffect, useRef, useState } from "react"; +import { useRouter } from "next/router"; +import { Transition } from "@headlessui/react"; +import { useForm } from "react-hook-form"; + +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; + +// hooks +import useToast from "hooks/use-toast"; +import useKeypress from "hooks/use-keypress"; +import useProjectDetails from "hooks/use-project-details"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; + +// constants +import { createIssuePayload } from "constants/issue"; + +// icons +import { PlusIcon } from "lucide-react"; + +// types +import { IIssue } from "types"; + +type Props = { + groupId?: string; + dependencies?: any[]; + prePopulatedData?: Partial; + onSuccess?: (data: IIssue) => Promise | void; +}; + +const useCheckIfThereIsSpaceOnRight = (ref: React.RefObject, deps: any[]) => { + const [isThereSpaceOnRight, setIsThereSpaceOnRight] = useState(true); + + const router = useRouter(); + const { moduleId, cycleId, viewId } = router.query; + + const container = document.getElementById(`calendar-view-${cycleId ?? moduleId ?? viewId}`); + + useEffect(() => { + if (!ref.current) return; + + const { right } = ref.current.getBoundingClientRect(); + + const width = right; + + const innerWidth = container?.getBoundingClientRect().width ?? window.innerWidth; + + if (width > innerWidth) setIsThereSpaceOnRight(false); + else setIsThereSpaceOnRight(true); + }, [ref, deps, container]); + + return isThereSpaceOnRight; +}; + +const defaultValues: Partial = { + name: "", +}; + +const Inputs = (props: any) => { + const { register, setFocus, projectDetails } = props; + + useEffect(() => { + setFocus("name"); + }, [setFocus]); + + return ( + <> +

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

+ + + ); +}; + +export const CalendarInlineCreateIssueForm: React.FC = observer((props) => { + const { prePopulatedData, dependencies = [], groupId } = props; + + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + // store + const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore(); + + // ref + const ref = useRef(null); + + // states + const [isOpen, setIsOpen] = useState(false); + + const { setToastAlert } = useToast(); + + const { projectDetails } = useProjectDetails(); + + const { + reset, + handleSubmit, + register, + setFocus, + formState: { errors, isSubmitting }, + } = useForm({ defaultValues }); + + const handleClose = () => { + setIsOpen(false); + }; + + useKeypress("Escape", handleClose); + useOutsideClickDetector(ref, handleClose); + + // derived values + const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!); + + useEffect(() => { + if (!isOpen) reset({ ...defaultValues }); + }, [isOpen, reset]); + + 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 isSpaceOnRight = useCheckIfThereIsSpaceOnRight(ref, dependencies); + + const onSubmitHandler = async (formData: IIssue) => { + if (isSubmitting || !workspaceSlug || !projectId) return; + + // resetting the form so that user can add another issue quickly + reset({ ...defaultValues, ...(prePopulatedData ?? {}) }); + + const payload = createIssuePayload(workspaceDetail!, projectDetails!, { + ...(prePopulatedData ?? {}), + ...formData, + labels_list: + formData.labels_list?.length !== 0 + ? formData.labels_list + : prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none" + ? [prePopulatedData.labels as any] + : [], + assignees_list: + formData.assignees_list?.length !== 0 + ? formData.assignees_list + : prePopulatedData?.assignees && prePopulatedData?.assignees.toString() !== "none" + ? [prePopulatedData.assignees as any] + : [], + }); + + try { + quickAddStore.createIssue( + workspaceSlug.toString(), + projectId.toString(), + { + group_id: groupId ?? null, + sub_group_id: null, + }, + payload + ); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue created successfully.", + }); + } catch (err: any) { + 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 ( + <> + +
+
+ + +
+
+ + {!isOpen && ( +
+ +
+ )} + + ); +}); diff --git a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx index de58b86224..cddc30c68f 100644 --- a/web/components/issues/issue-layouts/calendar/issue-blocks.tsx +++ b/web/components/issues/issue-layouts/calendar/issue-blocks.tsx @@ -17,42 +17,44 @@ export const CalendarIssueBlocks: React.FC = observer((props) => { const { workspaceSlug } = router.query; return ( -
+ <> {issues?.map((issue, index) => ( {(provided, snapshot) => ( - - - -
- {issue.project_detail.identifier}-{issue.sequence_id} -
-
{issue.name}
-
{quickActions(issue)}
- {/* handleIssues(issue.target_date ?? "", issue, "delete")} - handleUpdate={async (data) => handleIssues(issue.target_date ?? "", data, "update")} - /> */} -
- + + ); }); diff --git a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx index 0f0c19f06a..ec9e6b94f3 100644 --- a/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/cycle-root.tsx @@ -11,12 +11,16 @@ import { IIssueGroupedStructure } from "store/issue"; import { IIssue } from "types"; export const CycleCalendarLayout: React.FC = observer(() => { - const { cycleIssue: cycleIssueStore, issueFilter: issueFilterStore, issueDetail: issueDetailStore } = useMobxStore(); + const { + cycleIssue: cycleIssueStore, + issueFilter: issueFilterStore, + issueDetail: issueDetailStore, + cycleIssueCalendarView: cycleIssueCalendarViewStore, + } = useMobxStore(); const router = useRouter(); const { workspaceSlug, cycleId } = router.query; - // TODO: add drag and drop functionality const onDragEnd = (result: DropResult) => { if (!result) return; @@ -26,7 +30,7 @@ export const CycleCalendarLayout: React.FC = observer(() => { // return if dropped on the same date if (result.destination.droppableId === result.source.droppableId) return; - // issueKanBanViewStore?.handleDragDrop(result.source, result.destination); + cycleIssueCalendarViewStore?.handleDragDrop(result.source, result.destination); }; const issues = cycleIssueStore.getIssues; diff --git a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx index c0afd5a0a7..1f0219d551 100644 --- a/web/components/issues/issue-layouts/calendar/roots/module-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/module-root.tsx @@ -15,12 +15,12 @@ export const ModuleCalendarLayout: React.FC = observer(() => { moduleIssue: moduleIssueStore, issueFilter: issueFilterStore, issueDetail: issueDetailStore, + moduleIssueCalendarView: moduleIssueCalendarViewStore, } = useMobxStore(); const router = useRouter(); const { workspaceSlug, moduleId } = router.query; - // TODO: add drag and drop functionality const onDragEnd = (result: DropResult) => { if (!result) return; @@ -30,7 +30,7 @@ export const ModuleCalendarLayout: React.FC = observer(() => { // return if dropped on the same date if (result.destination.droppableId === result.source.droppableId) return; - // issueKanBanViewStore?.handleDragDrop(result.source, result.destination); + moduleIssueCalendarViewStore?.handleDragDrop(result.source, result.destination); }; const issues = moduleIssueStore.getIssues; diff --git a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx index 1d7c1cea32..96459a350a 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-root.tsx @@ -11,12 +11,16 @@ import { IIssueGroupedStructure } from "store/issue"; import { IIssue } from "types"; export const CalendarLayout: React.FC = observer(() => { - const { issue: issueStore, issueFilter: issueFilterStore, issueDetail: issueDetailStore } = useMobxStore(); + const { + issue: issueStore, + issueFilter: issueFilterStore, + issueDetail: issueDetailStore, + issueCalendarView: issueCalendarViewStore, + } = useMobxStore(); const router = useRouter(); const { workspaceSlug } = router.query; - // TODO: add drag and drop functionality const onDragEnd = (result: DropResult) => { if (!result) return; @@ -26,7 +30,7 @@ export const CalendarLayout: React.FC = observer(() => { // return if dropped on the same date if (result.destination.droppableId === result.source.droppableId) return; - // issueKanBanViewStore?.handleDragDrop(result.source, result.destination); + issueCalendarViewStore?.handleDragDrop(result.source, result.destination); }; const issues = issueStore.getIssues; diff --git a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx index 5aa9e1545b..6ea847609c 100644 --- a/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx +++ b/web/components/issues/issue-layouts/calendar/roots/project-view-root.tsx @@ -15,12 +15,12 @@ export const ProjectViewCalendarLayout: React.FC = observer(() => { projectViewIssues: projectViewIssuesStore, issueFilter: issueFilterStore, issueDetail: issueDetailStore, + projectViewIssueCalendarView: projectViewIssueCalendarViewStore, } = useMobxStore(); const router = useRouter(); const { workspaceSlug } = router.query; - // TODO: add drag and drop functionality const onDragEnd = (result: DropResult) => { if (!result) return; @@ -30,7 +30,7 @@ export const ProjectViewCalendarLayout: React.FC = observer(() => { // return if dropped on the same date if (result.destination.droppableId === result.source.droppableId) return; - // issueKanBanViewStore?.handleDragDrop(result.source, result.destination); + projectViewIssueCalendarViewStore?.handleDragDrop(result.source, result.destination); }; const issues = projectViewIssuesStore.getIssues; diff --git a/web/components/issues/issue-layouts/calendar/week-days.tsx b/web/components/issues/issue-layouts/calendar/week-days.tsx index 2d6d5d09a9..3923acb62a 100644 --- a/web/components/issues/issue-layouts/calendar/week-days.tsx +++ b/web/components/issues/issue-layouts/calendar/week-days.tsx @@ -15,10 +15,11 @@ type Props = { issues: IIssueGroupedStructure | null; week: ICalendarWeek | undefined; quickActions: (issue: IIssue) => React.ReactNode; + enableQuickIssueCreate?: boolean; }; export const CalendarWeekDays: React.FC = observer((props) => { - const { issues, week, quickActions } = props; + const { issues, week, quickActions, enableQuickIssueCreate } = props; const { issueFilter: issueFilterStore } = useMobxStore(); @@ -37,7 +38,13 @@ export const CalendarWeekDays: React.FC = observer((props) => { if (!showWeekends && (date.date.getDay() === 0 || date.date.getDay() === 6)) return null; return ( - + ); })}
diff --git a/web/components/issues/issue-layouts/gantt/index.ts b/web/components/issues/issue-layouts/gantt/index.ts index d8cfadd487..87899ae83f 100644 --- a/web/components/issues/issue-layouts/gantt/index.ts +++ b/web/components/issues/issue-layouts/gantt/index.ts @@ -3,3 +3,4 @@ export * from "./cycle-root"; export * from "./module-root"; export * from "./project-view-root"; export * from "./root"; +export * from "./inline-create-issue-form"; diff --git a/web/components/issues/issue-layouts/gantt/inline-create-issue-form.tsx b/web/components/issues/issue-layouts/gantt/inline-create-issue-form.tsx new file mode 100644 index 0000000000..b4edddbd13 --- /dev/null +++ b/web/components/issues/issue-layouts/gantt/inline-create-issue-form.tsx @@ -0,0 +1,196 @@ +import { useEffect, useState, useRef } from "react"; +import { useRouter } from "next/router"; +import { useForm } from "react-hook-form"; +import { Transition } from "@headlessui/react"; +import { PlusIcon } from "lucide-react"; + +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; + +// constants +import { createIssuePayload } from "constants/issue"; + +// hooks +import useToast from "hooks/use-toast"; +import useKeypress from "hooks/use-keypress"; +import useProjectDetails from "hooks/use-project-details"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; + +// types +import { IIssue } from "types"; +import { renderDateFormat } from "helpers/date-time.helper"; + +type Props = { + prePopulatedData?: Partial; + onSuccess?: (data: IIssue) => Promise | void; +}; + +const defaultValues: Partial = { + name: "", +}; + +const Inputs = (props: any) => { + const { register, setFocus } = props; + + useEffect(() => { + setFocus("name"); + }, [setFocus]); + + return ( + + ); +}; + +export const GanttInlineCreateIssueForm: React.FC = observer((props) => { + const { prePopulatedData } = props; + + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + // store + const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore(); + + const { projectDetails } = useProjectDetails(); + + const { + reset, + handleSubmit, + setFocus, + register, + formState: { errors, isSubmitting }, + } = useForm({ defaultValues }); + + // ref + const ref = useRef(null); + + // states + const [isOpen, setIsOpen] = useState(false); + + const handleClose = () => setIsOpen(false); + + // hooks + useKeypress("Escape", handleClose); + useOutsideClickDetector(ref, handleClose); + const { setToastAlert } = useToast(); + + // derived values + const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!); + + useEffect(() => { + if (!isOpen) reset({ ...defaultValues }); + }, [isOpen, reset]); + + 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 onSubmitHandler = async (formData: IIssue) => { + if (isSubmitting || !workspaceSlug || !projectId) return; + + // resetting the form so that user can add another issue quickly + reset({ ...defaultValues, ...(prePopulatedData ?? {}) }); + + const payload = createIssuePayload(workspaceDetail!, projectDetails!, { + ...(prePopulatedData ?? {}), + ...formData, + labels_list: + formData.labels_list?.length !== 0 + ? formData.labels_list + : prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none" + ? [prePopulatedData.labels as any] + : [], + start_date: renderDateFormat(new Date()), + target_date: renderDateFormat(new Date(new Date().getTime() + 24 * 60 * 60 * 1000)), + }); + + try { + quickAddStore.createIssue( + workspaceSlug.toString(), + projectId.toString(), + { + group_id: null, + sub_group_id: null, + }, + payload + ); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue created successfully.", + }); + } catch (err: any) { + 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 ( + <> + +
+
+

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

+ + + + + {isOpen && ( +

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

+ )} + + {!isOpen && ( + + )} + + ); +}); diff --git a/web/components/issues/issue-layouts/kanban/block.tsx b/web/components/issues/issue-layouts/kanban/block.tsx index 34f0a550e6..3cbb4e7d51 100644 --- a/web/components/issues/issue-layouts/kanban/block.tsx +++ b/web/components/issues/issue-layouts/kanban/block.tsx @@ -2,7 +2,7 @@ import { Draggable } from "@hello-pangea/dnd"; // components import { KanBanProperties } from "./properties"; // types -import { IIssue } from "types"; +import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types"; interface IssueBlockProps { sub_group_id: string; @@ -18,10 +18,27 @@ interface IssueBlockProps { ) => void; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; displayProperties: any; + states: IState[] | null; + labels: IIssueLabels[] | null; + members: IUserLite[] | null; + estimates: IEstimatePoint[] | null; } export const KanbanIssueBlock: React.FC = (props) => { - const { sub_group_id, columnId, index, issue, isDragDisabled, handleIssues, quickActions, displayProperties } = props; + const { + sub_group_id, + columnId, + index, + issue, + isDragDisabled, + handleIssues, + quickActions, + displayProperties, + states, + labels, + members, + estimates, + } = props; const updateIssue = (sub_group_by: string | null, group_by: string | null, issueToUpdate: IIssue) => { if (issueToUpdate) handleIssues(sub_group_by, group_by, issueToUpdate, "update"); @@ -37,6 +54,9 @@ export const KanbanIssueBlock: React.FC = (props) => { {...provided.dragHandleProps} ref={provided.innerRef} > + {issue.tempId !== undefined && ( +
+ )}
{quickActions( !sub_group_id && sub_group_id === "null" ? null : sub_group_id, @@ -54,7 +74,7 @@ export const KanbanIssueBlock: React.FC = (props) => { {issue.project_detail.identifier}-{issue.sequence_id}
)} -
{issue.name}
+
{issue.name}
= (props) => { issue={issue} handleIssues={updateIssue} display_properties={displayProperties} + states={states} + labels={labels} + members={members} + estimates={estimates} />
diff --git a/web/components/issues/issue-layouts/kanban/blocks-list.tsx b/web/components/issues/issue-layouts/kanban/blocks-list.tsx index aeee5c2fc8..0e921638a2 100644 --- a/web/components/issues/issue-layouts/kanban/blocks-list.tsx +++ b/web/components/issues/issue-layouts/kanban/blocks-list.tsx @@ -1,6 +1,6 @@ // components import { KanbanIssueBlock } from "components/issues"; -import { IIssue } from "types"; +import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types"; interface IssueBlocksListProps { sub_group_id: string; @@ -15,10 +15,26 @@ interface IssueBlocksListProps { ) => void; quickActions: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => React.ReactNode; display_properties: any; + states: IState[] | null; + labels: IIssueLabels[] | null; + members: IUserLite[] | null; + estimates: IEstimatePoint[] | null; } export const KanbanIssueBlocksList: React.FC = (props) => { - const { sub_group_id, columnId, issues, isDragDisabled, handleIssues, quickActions, display_properties } = props; + const { + sub_group_id, + columnId, + issues, + isDragDisabled, + handleIssues, + quickActions, + display_properties, + states, + labels, + members, + estimates, + } = props; return ( <> @@ -35,6 +51,10 @@ export const KanbanIssueBlocksList: React.FC = (props) => columnId={columnId} sub_group_id={sub_group_id} isDragDisabled={isDragDisabled} + states={states} + labels={labels} + members={members} + estimates={estimates} /> ))} diff --git a/web/components/issues/issue-layouts/kanban/default.tsx b/web/components/issues/issue-layouts/kanban/default.tsx index 691e50fb9e..354cf011fa 100644 --- a/web/components/issues/issue-layouts/kanban/default.tsx +++ b/web/components/issues/issue-layouts/kanban/default.tsx @@ -5,9 +5,9 @@ import { Droppable } from "@hello-pangea/dnd"; import { useMobxStore } from "lib/mobx/store-provider"; // components import { KanBanGroupByHeaderRoot } from "./headers/group-by-root"; -import { KanbanIssueBlocksList } from "components/issues"; +import { KanbanIssueBlocksList, BoardInlineCreateIssueForm } from "components/issues"; // types -import { IIssue } from "types"; +import { IEstimatePoint, IIssue, IIssueLabels, IProject, IState, IUserLite } from "types"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue"; @@ -29,6 +29,12 @@ export interface IGroupByKanBan { display_properties: any; kanBanToggle: any; handleKanBanToggle: any; + enableQuickIssueCreate?: boolean; + states: IState[] | null; + labels: IIssueLabels[] | null; + members: IUserLite[] | null; + priorities: any; + estimates: IEstimatePoint[] | null; } const GroupByKanBan: React.FC = observer((props) => { @@ -45,6 +51,12 @@ const GroupByKanBan: React.FC = observer((props) => { display_properties, kanBanToggle, handleKanBanToggle, + states, + labels, + members, + priorities, + estimates, + enableQuickIssueCreate, } = props; const verticalAlignPosition = (_list: any) => @@ -93,6 +105,10 @@ const GroupByKanBan: React.FC = observer((props) => { handleIssues={handleIssues} quickActions={quickActions} display_properties={display_properties} + states={states} + labels={labels} + members={members} + estimates={estimates} /> ) : ( isDragDisabled && ( @@ -106,6 +122,16 @@ const GroupByKanBan: React.FC = observer((props) => { )}
+ {enableQuickIssueCreate && ( + + )} ))} @@ -128,14 +154,14 @@ export interface IKanBan { display_properties: any; kanBanToggle: any; handleKanBanToggle: any; - - states: any; + states: IState[] | null; stateGroups: any; priorities: any; - labels: any; - members: any; - projects: any; - estimates: any; + labels: IIssueLabels[] | null; + members: IUserLite[] | null; + projects: IProject[] | null; + estimates: IEstimatePoint[] | null; + enableQuickIssueCreate?: boolean; } export const KanBan: React.FC = observer((props) => { @@ -156,6 +182,7 @@ export const KanBan: React.FC = observer((props) => { members, projects, estimates, + enableQuickIssueCreate, } = props; const { project: projectStore, issueKanBanView: issueKanBanViewStore } = useMobxStore(); @@ -176,6 +203,12 @@ export const KanBan: React.FC = observer((props) => { display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} + enableQuickIssueCreate={enableQuickIssueCreate} + states={states} + labels={labels} + members={members} + priorities={priorities} + estimates={estimates} /> )} @@ -193,6 +226,12 @@ export const KanBan: React.FC = observer((props) => { display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} + enableQuickIssueCreate={enableQuickIssueCreate} + states={states} + labels={labels} + members={members} + priorities={priorities} + estimates={estimates} /> )} @@ -210,6 +249,12 @@ export const KanBan: React.FC = observer((props) => { display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} + enableQuickIssueCreate={enableQuickIssueCreate} + states={states} + labels={labels} + members={members} + priorities={priorities} + estimates={estimates} /> )} @@ -227,6 +272,12 @@ export const KanBan: React.FC = observer((props) => { display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} + enableQuickIssueCreate={enableQuickIssueCreate} + states={states} + labels={labels} + members={members} + priorities={priorities} + estimates={estimates} /> )} @@ -244,6 +295,12 @@ export const KanBan: React.FC = observer((props) => { display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} + enableQuickIssueCreate={enableQuickIssueCreate} + states={states} + labels={labels} + members={members} + priorities={priorities} + estimates={estimates} /> )} @@ -261,6 +318,12 @@ export const KanBan: React.FC = observer((props) => { display_properties={display_properties} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} + enableQuickIssueCreate={enableQuickIssueCreate} + states={states} + labels={labels} + members={members} + priorities={priorities} + estimates={estimates} /> )} diff --git a/web/components/issues/issue-layouts/kanban/index.ts b/web/components/issues/issue-layouts/kanban/index.ts index 3adfe5c26f..761f32a773 100644 --- a/web/components/issues/issue-layouts/kanban/index.ts +++ b/web/components/issues/issue-layouts/kanban/index.ts @@ -1,5 +1,4 @@ export * from "./block"; +export * from "./roots"; export * from "./blocks-list"; -export * from "./cycle-root"; -export * from "./module-root"; -export * from "./root"; +export * from "./inline-create-issue-form"; diff --git a/web/components/issues/issue-layouts/kanban/inline-create-issue-form.tsx b/web/components/issues/issue-layouts/kanban/inline-create-issue-form.tsx new file mode 100644 index 0000000000..cad0814b8f --- /dev/null +++ b/web/components/issues/issue-layouts/kanban/inline-create-issue-form.tsx @@ -0,0 +1,202 @@ +import { useEffect, useState, useRef } from "react"; +import { useRouter } from "next/router"; +import { useForm } from "react-hook-form"; +import { Transition } from "@headlessui/react"; +import { PlusIcon } from "lucide-react"; +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; + +// hooks +import useToast from "hooks/use-toast"; +import useKeypress from "hooks/use-keypress"; +import useProjectDetails from "hooks/use-project-details"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; + +// constants +import { createIssuePayload } from "constants/issue"; + +// types +import { IIssue } from "types"; + +type Props = { + groupId?: string; + subGroupId?: string; + prePopulatedData?: Partial; + onSuccess?: (data: IIssue) => Promise | void; +}; + +const defaultValues: Partial = { + name: "", +}; + +const Inputs = (props: any) => { + const { register, setFocus, projectDetails } = props; + + useEffect(() => { + setFocus("name"); + }, [setFocus]); + + return ( +
+

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

+ +
+ ); +}; + +export const BoardInlineCreateIssueForm: React.FC = observer((props) => { + const { prePopulatedData, groupId, subGroupId } = props; + + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + // store + const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore(); + + // ref + const ref = useRef(null); + + // states + const [isOpen, setIsOpen] = useState(false); + + const { setToastAlert } = useToast(); + + const { projectDetails } = useProjectDetails(); + + const { + reset, + handleSubmit, + register, + setFocus, + formState: { errors, isSubmitting }, + } = useForm({ defaultValues }); + + const handleClose = () => { + setIsOpen(false); + }; + + useKeypress("Escape", handleClose); + useOutsideClickDetector(ref, handleClose); + + // derived values + const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!); + + useEffect(() => { + if (!isOpen) reset({ ...defaultValues }); + }, [isOpen, reset]); + + 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 onSubmitHandler = async (formData: IIssue) => { + if (isSubmitting || !workspaceSlug || !projectId) return; + + // resetting the form so that user can add another issue quickly + reset({ ...defaultValues, ...(prePopulatedData ?? {}) }); + + const payload = createIssuePayload(workspaceDetail!, projectDetails!, { + ...(prePopulatedData ?? {}), + ...formData, + labels_list: + formData.labels_list && formData.labels_list.length !== 0 + ? formData.labels_list + : prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none" + ? [prePopulatedData.labels as any] + : [], + assignees_list: + formData.assignees_list && formData.assignees_list.length !== 0 + ? formData.assignees_list + : prePopulatedData?.assignees && prePopulatedData?.assignees.toString() !== "none" + ? [prePopulatedData.assignees as any] + : [], + }); + + try { + quickAddStore.createIssue( + workspaceSlug.toString(), + projectId.toString(), + { + group_id: groupId ?? null, + sub_group_id: subGroupId ?? null, + }, + payload + ); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue created successfully.", + }); + } catch (err: any) { + 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 ( +
+ +
+ + +
+ + {isOpen && ( +

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

+ )} + + {!isOpen && ( + + )} +
+ ); +}); diff --git a/web/components/issues/issue-layouts/kanban/properties.tsx b/web/components/issues/issue-layouts/kanban/properties.tsx index 979ead23b6..e321094d4c 100644 --- a/web/components/issues/issue-layouts/kanban/properties.tsx +++ b/web/components/issues/issue-layouts/kanban/properties.tsx @@ -10,190 +10,196 @@ import { IssuePropertyAssignee } from "../properties/assignee"; import { IssuePropertyEstimates } from "../properties/estimates"; import { IssuePropertyDate } from "../properties/date"; import { Tooltip } from "@plane/ui"; +import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite, TIssuePriorities } from "types"; export interface IKanBanProperties { sub_group_id: string; columnId: string; - issue: any; - handleIssues?: (sub_group_by: string | null, group_by: string | null, issue: any) => void; + issue: IIssue; + handleIssues: (sub_group_by: string | null, group_by: string | null, issue: IIssue) => void; display_properties: any; + states: IState[] | null; + labels: IIssueLabels[] | null; + members: IUserLite[] | null; + estimates: IEstimatePoint[] | null; } -export const KanBanProperties: React.FC = observer( - ({ sub_group_id, columnId: group_id, issue, handleIssues, display_properties }) => { - const handleState = (id: string) => { - if (handleIssues) - handleIssues( - !sub_group_id && sub_group_id === "null" ? null : sub_group_id, - !group_id && group_id === "null" ? null : group_id, - { ...issue, state: id } - ); - }; +export const KanBanProperties: React.FC = observer((props) => { + const { + sub_group_id, + columnId: group_id, + issue, + handleIssues, + display_properties, + states, + labels, + members, + estimates, + } = props; - const handlePriority = (id: string) => { - if (handleIssues) - handleIssues( - !sub_group_id && sub_group_id === "null" ? null : sub_group_id, - !group_id && group_id === "null" ? null : group_id, - { ...issue, priority: id } - ); - }; - - const handleLabel = (ids: string[]) => { - if (handleIssues) - handleIssues( - !sub_group_id && sub_group_id === "null" ? null : sub_group_id, - !group_id && group_id === "null" ? null : group_id, - { ...issue, labels: ids } - ); - }; - - const handleAssignee = (ids: string[]) => { - if (handleIssues) - handleIssues( - !sub_group_id && sub_group_id === "null" ? null : sub_group_id, - !group_id && group_id === "null" ? null : group_id, - { ...issue, assignees: ids } - ); - }; - - const handleStartDate = (date: string) => { - if (handleIssues) - handleIssues( - !sub_group_id && sub_group_id === "null" ? null : sub_group_id, - !group_id && group_id === "null" ? null : group_id, - { ...issue, start_date: date } - ); - }; - - const handleTargetDate = (date: string) => { - if (handleIssues) - handleIssues( - !sub_group_id && sub_group_id === "null" ? null : sub_group_id, - !group_id && group_id === "null" ? null : group_id, - { ...issue, target_date: date } - ); - }; - - const handleEstimate = (id: string) => { - if (handleIssues) - handleIssues( - !sub_group_id && sub_group_id === "null" ? null : sub_group_id, - !group_id && group_id === "null" ? null : group_id, - { ...issue, estimate_point: id } - ); - }; - - return ( -
- {/* basic properties */} - {/* state */} - {display_properties && display_properties?.state && ( - handleState(id)} - disabled={false} - /> - )} - - {/* priority */} - {display_properties && display_properties?.priority && ( - handlePriority(id)} - disabled={false} - /> - )} - - {/* label */} - {display_properties && display_properties?.labels && ( - handleLabel(ids)} - disabled={false} - /> - )} - - {/* assignee */} - {display_properties && display_properties?.assignee && ( - handleAssignee(ids)} - disabled={false} - /> - )} - - {/* start date */} - {display_properties && display_properties?.start_date && ( - handleStartDate(date)} - disabled={false} - /> - )} - - {/* target/due date */} - {display_properties && display_properties?.due_date && ( - handleTargetDate(date)} - disabled={false} - /> - )} - - {/* estimates */} - {display_properties && display_properties?.estimate && ( - handleEstimate(id)} - disabled={false} - workspaceSlug={issue?.workspace_detail?.slug || null} - projectId={issue?.project_detail?.id || null} - /> - )} - - {/* extra render properties */} - {/* sub-issues */} - {display_properties && display_properties?.sub_issue_count && ( - -
-
- -
-
{issue.sub_issues_count}
-
-
- )} - - {/* attachments */} - {display_properties && display_properties?.attachment_count && ( - -
-
- -
-
{issue.attachment_count}
-
-
- )} - - {/* link */} - {display_properties && display_properties?.link && ( - -
-
- -
-
{issue.link_count}
-
-
- )} -
+ const handleState = (state: IState) => { + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, state: state.id } ); - } -); + }; + + const handlePriority = (value: TIssuePriorities) => { + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, priority: value } + ); + }; + + const handleLabel = (ids: string[]) => { + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, labels_list: ids } + ); + }; + + const handleAssignee = (ids: string[]) => { + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, assignees_list: ids } + ); + }; + + const handleStartDate = (date: string) => { + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, start_date: date } + ); + }; + + const handleTargetDate = (date: string) => { + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, target_date: date } + ); + }; + + const handleEstimate = (value: number | null) => { + handleIssues( + !sub_group_id && sub_group_id === "null" ? null : sub_group_id, + !group_id && group_id === "null" ? null : group_id, + { ...issue, estimate_point: value } + ); + }; + + return ( +
+ {/* basic properties */} + {/* state */} + {display_properties && display_properties?.state && ( + + )} + + {/* priority */} + {display_properties && display_properties?.priority && ( + + )} + + {/* label */} + {display_properties && display_properties?.labels && ( + + )} + + {/* assignee */} + {display_properties && display_properties?.assignee && ( + + )} + + {/* start date */} + {display_properties && display_properties?.start_date && ( + handleStartDate(date)} + disabled={false} + placeHolder="Start date" + /> + )} + + {/* target/due date */} + {display_properties && display_properties?.due_date && ( + handleTargetDate(date)} + disabled={false} + placeHolder="Target date" + /> + )} + + {/* estimates */} + {display_properties && display_properties?.estimate && ( + + )} + + {/* extra render properties */} + {/* sub-issues */} + {display_properties && display_properties?.sub_issue_count && ( + +
+ +
{issue.sub_issues_count}
+
+
+ )} + + {/* attachments */} + {display_properties && display_properties?.attachment_count && ( + +
+ +
{issue.attachment_count}
+
+
+ )} + + {/* link */} + {display_properties && display_properties?.link && ( + +
+ +
{issue.link_count}
+
+
+ )} +
+ ); +}); diff --git a/web/components/issues/issue-layouts/kanban/cycle-root.tsx b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx similarity index 83% rename from web/components/issues/issue-layouts/kanban/cycle-root.tsx rename to web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx index f4d09aadac..188e27a68c 100644 --- a/web/components/issues/issue-layouts/kanban/cycle-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/cycle-root.tsx @@ -5,9 +5,11 @@ import { DragDropContext } from "@hello-pangea/dnd"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { KanBanSwimLanes } from "./swimlanes"; -import { KanBan } from "./default"; +import { KanBanSwimLanes } from "../swimlanes"; +import { KanBan } from "../default"; import { CycleIssueQuickActions } from "components/issues"; +// helpers +import { orderArrayBy } from "helpers/array.helper"; // types import { IIssue } from "types"; // constants @@ -25,7 +27,7 @@ export const CycleKanBanLayout: React.FC = observer(() => { } = useMobxStore(); const router = useRouter(); - const { workspaceSlug, cycleId } = router.query; + const { workspaceSlug, projectId, cycleId } = router.query; const issues = cycleIssueStore?.getIssues; @@ -60,12 +62,12 @@ export const CycleKanBanLayout: React.FC = observer(() => { if (!workspaceSlug || !cycleId) return; if (action === "update") { - cycleIssueStore.updateIssueStructure(group_by, null, issue); + cycleIssueStore.updateIssueStructure(group_by, sub_group_by, issue); issueDetailStore.updateIssue(workspaceSlug.toString(), issue.project, issue.id, issue); } - if (action === "delete") cycleIssueStore.deleteIssue(group_by, null, issue); + if (action === "delete") cycleIssueStore.deleteIssue(group_by, sub_group_by, issue); if (action === "remove" && issue.bridge_id) { - cycleIssueStore.deleteIssue(group_by, null, issue); + cycleIssueStore.deleteIssue(group_by, sub_group_by, issue); cycleIssueStore.removeIssueFromCycle( workspaceSlug.toString(), issue.project, @@ -81,13 +83,18 @@ export const CycleKanBanLayout: React.FC = observer(() => { cycleIssueKanBanViewStore.handleKanBanToggle(toggle, value); }; + const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null; + const states = projectStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = projectStore?.projectLabels || null; const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = projectStore?.projectStates || null; - const estimates = null; + const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; + const estimates = + projectDetails?.estimate !== null + ? projectStore.projectEstimates?.find((e) => e.id === projectDetails?.estimate) || null + : null; return (
@@ -113,9 +120,9 @@ export const CycleKanBanLayout: React.FC = observer(() => { stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members} + members={members?.map((m) => m.member) ?? null} projects={projects} - estimates={estimates} + estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} /> ) : ( { stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members} + members={members?.map((m) => m.member) ?? null} projects={projects} - estimates={estimates} + estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} /> )} diff --git a/web/components/issues/issue-layouts/kanban/roots/index.ts b/web/components/issues/issue-layouts/kanban/roots/index.ts new file mode 100644 index 0000000000..139c09a7ae --- /dev/null +++ b/web/components/issues/issue-layouts/kanban/roots/index.ts @@ -0,0 +1,5 @@ +export * from "./cycle-root"; +export * from "./module-root"; +export * from "./profile-issues-root"; +export * from "./project-root"; +export * from "./project-view-root"; diff --git a/web/components/issues/issue-layouts/kanban/module-root.tsx b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx similarity index 85% rename from web/components/issues/issue-layouts/kanban/module-root.tsx rename to web/components/issues/issue-layouts/kanban/roots/module-root.tsx index 594f15757e..754693d11a 100644 --- a/web/components/issues/issue-layouts/kanban/module-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/module-root.tsx @@ -5,9 +5,11 @@ import { DragDropContext } from "@hello-pangea/dnd"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { KanBanSwimLanes } from "./swimlanes"; -import { KanBan } from "./default"; +import { KanBanSwimLanes } from "../swimlanes"; +import { KanBan } from "../default"; import { ModuleIssueQuickActions } from "components/issues"; +// helpers +import { orderArrayBy } from "helpers/array.helper"; // types import { IIssue } from "types"; // constants @@ -25,7 +27,7 @@ export const ModuleKanBanLayout: React.FC = observer(() => { } = useMobxStore(); const router = useRouter(); - const { workspaceSlug, moduleId } = router.query; + const { workspaceSlug, projectId, moduleId } = router.query; const issues = moduleIssueStore?.getIssues; @@ -81,13 +83,18 @@ export const ModuleKanBanLayout: React.FC = observer(() => { moduleIssueKanBanViewStore.handleKanBanToggle(toggle, value); }; + const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null; + const states = projectStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = projectStore?.projectLabels || null; const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = projectStore?.projectStates || null; - const estimates = null; + const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; + const estimates = + projectDetails?.estimate !== null + ? projectStore.projectEstimates?.find((e) => e.id === projectDetails?.estimate) || null + : null; return (
@@ -113,9 +120,9 @@ export const ModuleKanBanLayout: React.FC = observer(() => { stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members} + members={members?.map((m) => m.member) ?? null} projects={projects} - estimates={estimates} + estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} /> ) : ( { stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members} + members={members?.map((m) => m.member) ?? null} projects={projects} - estimates={estimates} + estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} /> )} diff --git a/web/components/issues/issue-layouts/kanban/profile-issues-root.tsx b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx similarity index 94% rename from web/components/issues/issue-layouts/kanban/profile-issues-root.tsx rename to web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx index badcd04aa1..d2346120a2 100644 --- a/web/components/issues/issue-layouts/kanban/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/profile-issues-root.tsx @@ -5,8 +5,8 @@ import { DragDropContext } from "@hello-pangea/dnd"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { KanBanSwimLanes } from "./swimlanes"; -import { KanBan } from "./default"; +import { KanBanSwimLanes } from "../swimlanes"; +import { KanBan } from "../default"; import { ProjectIssueQuickActions } from "components/issues"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; @@ -79,7 +79,6 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; const projects = projectStore?.workspaceProjects || null; - const estimates = null; return (
@@ -104,9 +103,9 @@ export const ProfileIssuesKanBanLayout: FC = observer(() => { stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members} + members={members?.map((m) => m.member) ?? null} projects={projects} - estimates={estimates} + estimates={null} /> ) : ( { stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members} + members={members?.map((m) => m.member) ?? null} projects={projects} - estimates={estimates} + estimates={null} /> )} diff --git a/web/components/issues/issue-layouts/kanban/root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx similarity index 82% rename from web/components/issues/issue-layouts/kanban/root.tsx rename to web/components/issues/issue-layouts/kanban/roots/project-root.tsx index 741e878a0c..ab087e5c8e 100644 --- a/web/components/issues/issue-layouts/kanban/root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-root.tsx @@ -1,13 +1,15 @@ -import { FC, useCallback } from "react"; +import { useCallback } from "react"; import { useRouter } from "next/router"; import { DragDropContext } from "@hello-pangea/dnd"; import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { KanBanSwimLanes } from "./swimlanes"; -import { KanBan } from "./default"; +import { KanBanSwimLanes } from "../swimlanes"; +import { KanBan } from "../default"; import { ProjectIssueQuickActions } from "components/issues"; +// helpers +import { orderArrayBy } from "helpers/array.helper"; // types import { IIssue } from "types"; // constants @@ -15,9 +17,9 @@ import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; export interface IKanBanLayout {} -export const KanBanLayout: FC = observer(() => { +export const KanBanLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, projectId } = router.query; const { project: projectStore, @@ -72,13 +74,18 @@ export const KanBanLayout: FC = observer(() => { issueKanBanViewStore.handleKanBanToggle(toggle, value); }; + const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null; + const states = projectStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = projectStore?.projectLabels || null; const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = projectStore?.projectStates || null; - const estimates = null; + const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; + const estimates = + projectDetails?.estimate !== null + ? projectStore.projectEstimates?.find((e) => e.id === projectDetails?.estimate) || null + : null; return (
@@ -103,9 +110,10 @@ export const KanBanLayout: FC = observer(() => { stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members} + members={members?.map((m) => m.member) ?? null} projects={projects} - estimates={estimates} + enableQuickIssueCreate + estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} /> ) : ( { stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members} + members={members?.map((m) => m.member) ?? null} projects={projects} - estimates={estimates} + estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} /> )} diff --git a/web/components/issues/issue-layouts/kanban/view-root.tsx b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx similarity index 95% rename from web/components/issues/issue-layouts/kanban/view-root.tsx rename to web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx index 78117da807..583835ba34 100644 --- a/web/components/issues/issue-layouts/kanban/view-root.tsx +++ b/web/components/issues/issue-layouts/kanban/roots/project-view-root.tsx @@ -4,8 +4,8 @@ import { DragDropContext } from "@hello-pangea/dnd"; // mobx import { observer } from "mobx-react-lite"; // components -import { KanBanSwimLanes } from "./swimlanes"; -import { KanBan } from "./default"; +import { KanBanSwimLanes } from "../swimlanes"; +import { KanBan } from "../default"; // store import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; @@ -14,7 +14,7 @@ import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; export interface IViewKanBanLayout {} -export const ViewKanBanLayout: React.FC = observer(() => { +export const ProjectViewKanBanLayout: React.FC = observer(() => { const { project: projectStore, issue: issueStore, diff --git a/web/components/issues/issue-layouts/kanban/swimlanes.tsx b/web/components/issues/issue-layouts/kanban/swimlanes.tsx index 9090162c0e..7a3126cb27 100644 --- a/web/components/issues/issue-layouts/kanban/swimlanes.tsx +++ b/web/components/issues/issue-layouts/kanban/swimlanes.tsx @@ -7,7 +7,7 @@ import { KanBanGroupByHeaderRoot } from "./headers/group-by-root"; import { KanBanSubGroupByHeaderRoot } from "./headers/sub-group-by-root"; import { KanBan } from "./default"; // types -import { IIssue } from "types"; +import { IEstimatePoint, IIssue, IIssueLabels, IProject, IState, IUserLite } from "types"; // constants import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES, getValueFromObject } from "constants/issue"; @@ -19,6 +19,11 @@ interface ISubGroupSwimlaneHeader { listKey: string; kanBanToggle: any; handleKanBanToggle: any; + states: IState[] | null; + labels: IIssueLabels[] | null; + members: IUserLite[] | null; + projects: IProject[] | null; + estimates: IEstimatePoint[] | null; } const SubGroupSwimlaneHeader: React.FC = ({ issues, @@ -71,13 +76,13 @@ interface ISubGroupSwimlane extends ISubGroupSwimlaneHeader { display_properties: any; kanBanToggle: any; handleKanBanToggle: any; - states: any; + states: IState[] | null; stateGroups: any; priorities: any; - labels: any; - members: any; - projects: any; - estimates: any; + labels: IIssueLabels[] | null; + members: IUserLite[] | null; + projects: IProject[] | null; + estimates: IEstimatePoint[] | null; } const SubGroupSwimlane: React.FC = observer((props) => { const { @@ -148,6 +153,7 @@ const SubGroupSwimlane: React.FC = observer((props) => { members={members} projects={projects} estimates={estimates} + enableQuickIssueCreate />
)} @@ -171,13 +177,13 @@ export interface IKanBanSwimLanes { display_properties: any; kanBanToggle: any; handleKanBanToggle: any; - states: any; + states: IState[] | null; stateGroups: any; priorities: any; - labels: any; - members: any; - projects: any; - estimates: any; + labels: IIssueLabels[] | null; + members: IUserLite[] | null; + projects: IProject[] | null; + estimates: IEstimatePoint[] | null; } export const KanBanSwimLanes: React.FC = observer((props) => { @@ -213,6 +219,11 @@ export const KanBanSwimLanes: React.FC = observer((props) => { listKey={`id`} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} + states={states} + labels={labels} + members={members} + projects={projects} + estimates={estimates} /> )} @@ -225,6 +236,11 @@ export const KanBanSwimLanes: React.FC = observer((props) => { listKey={`key`} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} + states={states} + labels={labels} + members={members} + projects={projects} + estimates={estimates} /> )} @@ -237,6 +253,11 @@ export const KanBanSwimLanes: React.FC = observer((props) => { listKey={`key`} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} + states={states} + labels={labels} + members={members} + projects={projects} + estimates={estimates} /> )} @@ -249,6 +270,11 @@ export const KanBanSwimLanes: React.FC = observer((props) => { listKey={`id`} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} + states={states} + labels={labels} + members={members} + projects={projects} + estimates={estimates} /> )} @@ -261,6 +287,11 @@ export const KanBanSwimLanes: React.FC = observer((props) => { listKey={`member.id`} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} + states={states} + labels={labels} + members={members} + projects={projects} + estimates={estimates} /> )} @@ -273,6 +304,11 @@ export const KanBanSwimLanes: React.FC = observer((props) => { listKey={`member.id`} kanBanToggle={kanBanToggle} handleKanBanToggle={handleKanBanToggle} + states={states} + labels={labels} + members={members} + projects={projects} + estimates={estimates} /> )}
diff --git a/web/components/issues/issue-layouts/list/block.tsx b/web/components/issues/issue-layouts/list/block.tsx index 47a1a38f0a..5a84c5f9eb 100644 --- a/web/components/issues/issue-layouts/list/block.tsx +++ b/web/components/issues/issue-layouts/list/block.tsx @@ -4,7 +4,7 @@ import { IssuePeekOverview } from "components/issues/issue-peek-overview"; // ui import { Tooltip } from "@plane/ui"; // types -import { IIssue } from "types"; +import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types"; interface IssueBlockProps { columnId: string; @@ -12,28 +12,30 @@ interface IssueBlockProps { handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; display_properties: any; - states: any; - labels: any; - members: any; - priorities: any; + states: IState[] | null; + labels: IIssueLabels[] | null; + members: IUserLite[] | null; + estimates: IEstimatePoint[] | null; } export const IssueBlock: React.FC = (props) => { - const { columnId, issue, handleIssues, quickActions, display_properties, states, labels, members, priorities } = - props; + const { columnId, issue, handleIssues, quickActions, display_properties, states, labels, members, estimates } = props; const updateIssue = (group_by: string | null, issueToUpdate: IIssue) => { - if (issueToUpdate && handleIssues) handleIssues(group_by, issueToUpdate, "update"); + handleIssues(group_by, issueToUpdate, "update"); }; return ( <> -
+
{display_properties && display_properties?.key && (
{issue?.project_detail?.identifier}-{issue.sequence_id}
)} + {issue?.tempId !== undefined && ( +
+ )} = (props) => { states={states} labels={labels} members={members} - priorities={priorities} + estimates={estimates} /> {quickActions(!columnId && columnId === "null" ? null : columnId, issue)}
diff --git a/web/components/issues/issue-layouts/list/blocks-list.tsx b/web/components/issues/issue-layouts/list/blocks-list.tsx index 33618505f1..3267e221c2 100644 --- a/web/components/issues/issue-layouts/list/blocks-list.tsx +++ b/web/components/issues/issue-layouts/list/blocks-list.tsx @@ -2,7 +2,7 @@ import { FC } from "react"; // components import { IssueBlock } from "components/issues"; // types -import { IIssue } from "types"; +import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite } from "types"; interface Props { columnId: string; @@ -10,14 +10,14 @@ interface Props { handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; display_properties: any; - states: any; - labels: any; - members: any; - priorities: any; + states: IState[] | null; + labels: IIssueLabels[] | null; + members: IUserLite[] | null; + estimates: IEstimatePoint[] | null; } export const IssueBlocksList: FC = (props) => { - const { columnId, issues, handleIssues, quickActions, display_properties, states, labels, members, priorities } = + const { columnId, issues, handleIssues, quickActions, display_properties, states, labels, members, estimates } = props; return ( @@ -35,7 +35,7 @@ export const IssueBlocksList: FC = (props) => { states={states} labels={labels} members={members} - priorities={priorities} + estimates={estimates} /> ))} diff --git a/web/components/issues/issue-layouts/list/default.tsx b/web/components/issues/issue-layouts/list/default.tsx index 008e625aec..114b308eb0 100644 --- a/web/components/issues/issue-layouts/list/default.tsx +++ b/web/components/issues/issue-layouts/list/default.tsx @@ -2,11 +2,11 @@ import React from "react"; import { observer } from "mobx-react-lite"; // components import { ListGroupByHeaderRoot } from "./headers/group-by-root"; -import { IssueBlock } from "./block"; +import { IssueBlocksList, ListInlineCreateIssueForm } from "components/issues"; +// types +import { IEstimatePoint, IIssue, IIssueLabels, IProject, IState, IUserLite } from "types"; // constants import { getValueFromObject } from "constants/issue"; -import { IIssue } from "types"; -import { IssueBlocksList } from "./blocks-list"; export interface IGroupByList { issues: any; @@ -17,13 +17,14 @@ export interface IGroupByList { quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; display_properties: any; is_list?: boolean; - states: any; - labels: any; - members: any; - projects: any; + states: IState[] | null; + labels: IIssueLabels[] | null; + members: IUserLite[] | null; + projects: IProject[] | null; stateGroups: any; priorities: any; - estimates: any; + enableQuickIssueCreate?: boolean; + estimates: IEstimatePoint[] | null; } const GroupByList: React.FC = observer((props) => { @@ -43,6 +44,7 @@ const GroupByList: React.FC = observer((props) => { stateGroups, priorities, estimates, + enableQuickIssueCreate, } = props; return ( @@ -72,10 +74,18 @@ const GroupByList: React.FC = observer((props) => { states={states} labels={labels} members={members} - priorities={priorities} + estimates={estimates} /> )}
+ {enableQuickIssueCreate && ( + + )}
))}
@@ -90,13 +100,14 @@ export interface IList { handleIssues: (group_by: string | null, issue: IIssue, action: "update" | "delete") => void; quickActions: (group_by: string | null, issue: IIssue) => React.ReactNode; display_properties: any; - states: any; - labels: any; - members: any; - projects: any; + states: IState[] | null; + labels: IIssueLabels[] | null; + members: IUserLite[] | null; + projects: IProject[] | null; stateGroups: any; priorities: any; - estimates: any; + enableQuickIssueCreate?: boolean; + estimates: IEstimatePoint[] | null; } export const List: React.FC = observer((props) => { @@ -113,6 +124,7 @@ export const List: React.FC = observer((props) => { stateGroups, priorities, estimates, + enableQuickIssueCreate, } = props; return ( @@ -134,6 +146,7 @@ export const List: React.FC = observer((props) => { stateGroups={stateGroups} priorities={priorities} estimates={estimates} + enableQuickIssueCreate={enableQuickIssueCreate} /> )} @@ -153,6 +166,7 @@ export const List: React.FC = observer((props) => { stateGroups={stateGroups} priorities={priorities} estimates={estimates} + enableQuickIssueCreate={enableQuickIssueCreate} /> )} @@ -172,6 +186,7 @@ export const List: React.FC = observer((props) => { stateGroups={stateGroups} priorities={priorities} estimates={estimates} + enableQuickIssueCreate={enableQuickIssueCreate} /> )} @@ -191,6 +206,7 @@ export const List: React.FC = observer((props) => { stateGroups={stateGroups} priorities={priorities} estimates={estimates} + enableQuickIssueCreate={enableQuickIssueCreate} /> )} @@ -210,6 +226,7 @@ export const List: React.FC = observer((props) => { stateGroups={stateGroups} priorities={priorities} estimates={estimates} + enableQuickIssueCreate={enableQuickIssueCreate} /> )} @@ -229,6 +246,7 @@ export const List: React.FC = observer((props) => { stateGroups={stateGroups} priorities={priorities} estimates={estimates} + enableQuickIssueCreate={enableQuickIssueCreate} /> )} @@ -248,6 +266,7 @@ export const List: React.FC = observer((props) => { stateGroups={stateGroups} priorities={priorities} estimates={estimates} + enableQuickIssueCreate={enableQuickIssueCreate} /> )} @@ -267,6 +286,7 @@ export const List: React.FC = observer((props) => { stateGroups={stateGroups} priorities={priorities} estimates={estimates} + enableQuickIssueCreate={enableQuickIssueCreate} /> )}
diff --git a/web/components/issues/issue-layouts/list/index.ts b/web/components/issues/issue-layouts/list/index.ts index 3adfe5c26f..e557fe0229 100644 --- a/web/components/issues/issue-layouts/list/index.ts +++ b/web/components/issues/issue-layouts/list/index.ts @@ -1,5 +1,4 @@ +export * from "./roots"; export * from "./block"; export * from "./blocks-list"; -export * from "./cycle-root"; -export * from "./module-root"; -export * from "./root"; +export * from "./inline-create-issue-form"; diff --git a/web/components/issues/issue-layouts/list/inline-create-issue-form.tsx b/web/components/issues/issue-layouts/list/inline-create-issue-form.tsx new file mode 100644 index 0000000000..a6ac218e7f --- /dev/null +++ b/web/components/issues/issue-layouts/list/inline-create-issue-form.tsx @@ -0,0 +1,201 @@ +import { useEffect, useState, useRef } from "react"; +import { useRouter } from "next/router"; +import { useForm } from "react-hook-form"; +import { Transition } from "@headlessui/react"; + +// hooks +import useToast from "hooks/use-toast"; +import useKeypress from "hooks/use-keypress"; +import useProjectDetails from "hooks/use-project-details"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; + +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; + +// constants +import { createIssuePayload } from "constants/issue"; + +// types +import { IIssue } from "types"; +import { PlusIcon } from "lucide-react"; + +type Props = { + groupId?: string; + prePopulatedData?: Partial; + onSuccess?: (data: IIssue) => Promise | void; +}; + +const defaultValues: Partial = { + name: "", +}; + +const Inputs = (props: any) => { + const { register, setFocus, projectDetails } = props; + + useEffect(() => { + setFocus("name"); + }, [setFocus]); + + return ( + <> +

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

+ + + ); +}; + +export const ListInlineCreateIssueForm: React.FC = observer((props) => { + const { prePopulatedData, groupId } = props; + + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + // store + const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore(); + + const { projectDetails } = useProjectDetails(); + + const { + reset, + handleSubmit, + setFocus, + register, + formState: { errors, isSubmitting }, + } = useForm({ defaultValues }); + + // ref + const ref = useRef(null); + + // states + const [isOpen, setIsOpen] = useState(false); + + const handleClose = () => setIsOpen(false); + + // hooks + useKeypress("Escape", handleClose); + useOutsideClickDetector(ref, handleClose); + const { setToastAlert } = useToast(); + + // derived values + const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!); + + useEffect(() => { + if (!isOpen) reset({ ...defaultValues }); + }, [isOpen, reset]); + + 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 onSubmitHandler = async (formData: IIssue) => { + if (isSubmitting || !workspaceSlug || !projectId) return; + + // resetting the form so that user can add another issue quickly + reset({ ...defaultValues }); + + const payload = createIssuePayload(workspaceDetail!, projectDetails!, { + ...(prePopulatedData ?? {}), + ...formData, + labels_list: + formData.labels_list?.length !== 0 + ? formData.labels_list + : prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none" + ? [prePopulatedData.labels as any] + : [], + assignees_list: + formData.assignees_list?.length !== 0 + ? formData.assignees_list + : prePopulatedData?.assignees && prePopulatedData?.assignees.toString() !== "none" + ? [prePopulatedData.assignees as any] + : [], + }); + + try { + quickAddStore.createIssue( + workspaceSlug.toString(), + projectId.toString(), + { + group_id: groupId ?? null, + sub_group_id: null, + }, + payload + ); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue created successfully.", + }); + } catch (err: any) { + 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 ( +
+ +
+ + +
+ + {isOpen && ( +

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

+ )} + + {!isOpen && ( + + )} +
+ ); +}); diff --git a/web/components/issues/issue-layouts/list/properties.tsx b/web/components/issues/issue-layouts/list/properties.tsx index 86af82656c..9c70f9fdd3 100644 --- a/web/components/issues/issue-layouts/list/properties.tsx +++ b/web/components/issues/issue-layouts/list/properties.tsx @@ -11,49 +11,48 @@ import { IssuePropertyDate } from "../properties/date"; // ui import { Tooltip } from "@plane/ui"; // types -import { IIssue } from "types"; +import { IEstimatePoint, IIssue, IIssueLabels, IState, IUserLite, TIssuePriorities } from "types"; export interface IKanBanProperties { columnId: string; - issue: any; - handleIssues?: (group_by: string | null, issue: IIssue) => void; + issue: IIssue; + handleIssues: (group_by: string | null, issue: IIssue) => void; display_properties: any; - states: any; - labels: any; - members: any; - priorities: any; + states: IState[] | null; + labels: IIssueLabels[] | null; + members: IUserLite[] | null; + estimates: IEstimatePoint[] | null; } export const KanBanProperties: FC = observer((props) => { - const { columnId: group_id, issue, handleIssues, display_properties, states, labels, members, priorities } = props; + const { columnId: group_id, issue, handleIssues, display_properties, states, labels, members, estimates } = props; - const handleState = (id: string) => { - if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, state: id }); + const handleState = (state: IState) => { + handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, state: state.id }); }; - const handlePriority = (id: string) => { - if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, priority: id }); + const handlePriority = (value: TIssuePriorities) => { + handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, priority: value }); }; const handleLabel = (ids: string[]) => { - if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, labels: ids }); + handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, labels_list: ids }); }; const handleAssignee = (ids: string[]) => { - if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, assignees: ids }); + handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, assignees_list: ids }); }; const handleStartDate = (date: string) => { - if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, start_date: date }); + handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, start_date: date }); }; const handleTargetDate = (date: string) => { - if (handleIssues) handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, target_date: date }); + handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, target_date: date }); }; - const handleEstimate = (id: string) => { - if (handleIssues) - handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, estimate_point: id }); + const handleEstimate = (value: number | null) => { + handleIssues(!group_id && group_id === "null" ? null : group_id, { ...issue, estimate_point: value }); }; return ( @@ -62,22 +61,21 @@ export const KanBanProperties: FC = observer((props) => { {/* state */} {display_properties && display_properties?.state && states && ( handleState(id)} + value={issue?.state_detail || null} + hideDropdownArrow={true} + onChange={handleState} disabled={false} - list={states} + states={states} /> )} {/* priority */} - {display_properties && display_properties?.priority && priorities && ( + {display_properties && display_properties?.priority && ( handlePriority(id)} + onChange={handlePriority} disabled={false} - list={priorities} + hideDropdownArrow={true} /> )} @@ -85,10 +83,10 @@ export const KanBanProperties: FC = observer((props) => { {display_properties && display_properties?.labels && labels && ( handleLabel(ids)} + onChange={handleLabel} + labels={labels} disabled={false} - list={labels} + hideDropdownArrow={true} /> )} @@ -96,10 +94,10 @@ export const KanBanProperties: FC = observer((props) => { {display_properties && display_properties?.assignee && members && ( handleAssignee(ids)} + hideDropdownArrow={true} + onChange={handleAssignee} disabled={false} - list={members} + members={members} /> )} @@ -109,7 +107,7 @@ export const KanBanProperties: FC = observer((props) => { value={issue?.start_date || null} onChange={(date: string) => handleStartDate(date)} disabled={false} - placeHolder={`Start date`} + placeHolder="Start date" /> )} @@ -119,31 +117,28 @@ export const KanBanProperties: FC = observer((props) => { value={issue?.target_date || null} onChange={(date: string) => handleTargetDate(date)} disabled={false} - placeHolder={`Target date`} + placeHolder="Target date" /> )} {/* estimates */} {display_properties && display_properties?.estimate && ( handleEstimate(id)} + value={issue?.estimate_point || null} + estimatePoints={estimates} + hideDropdownArrow={true} + onChange={handleEstimate} disabled={false} - workspaceSlug={issue?.workspace_detail?.slug || null} - projectId={issue?.project_detail?.id || null} /> )} {/* extra render properties */} {/* sub-issues */} {display_properties && display_properties?.sub_issue_count && ( - -
-
- -
-
{issue.sub_issues_count}
+ +
+ +
{issue.sub_issues_count}
)} @@ -151,11 +146,9 @@ export const KanBanProperties: FC = observer((props) => { {/* attachments */} {display_properties && display_properties?.attachment_count && ( -
-
- -
-
{issue.attachment_count}
+
+ +
{issue.attachment_count}
)} @@ -163,11 +156,9 @@ export const KanBanProperties: FC = observer((props) => { {/* link */} {display_properties && display_properties?.link && ( -
-
- -
-
{issue.link_count}
+
+ +
{issue.link_count}
)} diff --git a/web/components/issues/issue-layouts/list/cycle-root.tsx b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx similarity index 80% rename from web/components/issues/issue-layouts/list/cycle-root.tsx rename to web/components/issues/issue-layouts/list/roots/cycle-root.tsx index 5113365311..771186c842 100644 --- a/web/components/issues/issue-layouts/list/cycle-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/cycle-root.tsx @@ -4,8 +4,10 @@ import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { List } from "./default"; +import { List } from "../default"; import { CycleIssueQuickActions } from "components/issues"; +// helpers +import { orderArrayBy } from "helpers/array.helper"; // types import { IIssue } from "types"; // constants @@ -15,7 +17,7 @@ export interface ICycleListLayout {} export const CycleListLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, cycleId } = router.query; + const { workspaceSlug, projectId, cycleId } = router.query; const { project: projectStore, @@ -52,13 +54,18 @@ export const CycleListLayout: React.FC = observer(() => { [cycleIssueStore, issueDetailStore, cycleId, workspaceSlug] ); + const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null; + const states = projectStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = projectStore?.projectLabels || null; const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = projectStore?.projectStates || null; - const estimates = null; + const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; + const estimates = + projectDetails?.estimate !== null + ? projectStore.projectEstimates?.find((e) => e.id === projectDetails?.estimate) || null + : null; return (
@@ -79,9 +86,9 @@ export const CycleListLayout: React.FC = observer(() => { stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members} + members={members?.map((m) => m.member) ?? null} projects={projects} - estimates={estimates} + estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} />
); diff --git a/web/components/issues/issue-layouts/list/roots/index.ts b/web/components/issues/issue-layouts/list/roots/index.ts new file mode 100644 index 0000000000..139c09a7ae --- /dev/null +++ b/web/components/issues/issue-layouts/list/roots/index.ts @@ -0,0 +1,5 @@ +export * from "./cycle-root"; +export * from "./module-root"; +export * from "./profile-issues-root"; +export * from "./project-root"; +export * from "./project-view-root"; diff --git a/web/components/issues/issue-layouts/list/module-root.tsx b/web/components/issues/issue-layouts/list/roots/module-root.tsx similarity index 80% rename from web/components/issues/issue-layouts/list/module-root.tsx rename to web/components/issues/issue-layouts/list/roots/module-root.tsx index 485e4e908e..daa12e64a2 100644 --- a/web/components/issues/issue-layouts/list/module-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/module-root.tsx @@ -4,8 +4,10 @@ import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { List } from "./default"; +import { List } from "../default"; import { ModuleIssueQuickActions } from "components/issues"; +// helpers +import { orderArrayBy } from "helpers/array.helper"; // types import { IIssue } from "types"; // constants @@ -15,7 +17,7 @@ export interface IModuleListLayout {} export const ModuleListLayout: React.FC = observer(() => { const router = useRouter(); - const { workspaceSlug, moduleId } = router.query; + const { workspaceSlug, projectId, moduleId } = router.query; const { project: projectStore, @@ -52,13 +54,18 @@ export const ModuleListLayout: React.FC = observer(() => { [moduleIssueStore, issueDetailStore, moduleId, workspaceSlug] ); + const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null; + const states = projectStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = projectStore?.projectLabels || null; const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = projectStore?.projectStates || null; - const estimates = null; + const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; + const estimates = + projectDetails?.estimate !== null + ? projectStore.projectEstimates?.find((e) => e.id === projectDetails?.estimate) || null + : null; return (
@@ -79,9 +86,9 @@ export const ModuleListLayout: React.FC = observer(() => { stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members} + members={members?.map((m) => m.member) ?? null} projects={projects} - estimates={estimates} + estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} />
); diff --git a/web/components/issues/issue-layouts/list/profile-issues-root.tsx b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx similarity index 95% rename from web/components/issues/issue-layouts/list/profile-issues-root.tsx rename to web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx index b1fb86c6a9..9e4937ffdb 100644 --- a/web/components/issues/issue-layouts/list/profile-issues-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/profile-issues-root.tsx @@ -4,7 +4,7 @@ import { observer } from "mobx-react-lite"; // mobx store import { useMobxStore } from "lib/mobx/store-provider"; // components -import { List } from "./default"; +import { List } from "../default"; import { ProjectIssueQuickActions } from "components/issues"; // types import { IIssue } from "types"; @@ -50,7 +50,6 @@ export const ProfileIssuesListLayout: FC = observer(() => { const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; const projects = projectStore?.workspaceProjects || null; - const estimates = null; return (
@@ -70,9 +69,9 @@ export const ProfileIssuesListLayout: FC = observer(() => { stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members} + members={members?.map((m) => m.member) ?? null} projects={projects} - estimates={estimates} + estimates={null} />
); diff --git a/web/components/issues/issue-layouts/list/root.tsx b/web/components/issues/issue-layouts/list/roots/project-root.tsx similarity index 76% rename from web/components/issues/issue-layouts/list/root.tsx rename to web/components/issues/issue-layouts/list/roots/project-root.tsx index a5b1baa646..47c1753c6c 100644 --- a/web/components/issues/issue-layouts/list/root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-root.tsx @@ -4,8 +4,10 @@ import { observer } from "mobx-react-lite"; // hooks import { useMobxStore } from "lib/mobx/store-provider"; // components -import { List } from "./default"; +import { List } from "../default"; import { ProjectIssueQuickActions } from "components/issues"; +// helpers +import { orderArrayBy } from "helpers/array.helper"; // types import { IIssue } from "types"; // constants @@ -13,7 +15,7 @@ import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; export const ListLayout: FC = observer(() => { const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, projectId } = router.query; const { project: projectStore, @@ -41,13 +43,18 @@ export const ListLayout: FC = observer(() => { [issueStore, issueDetailStore, workspaceSlug] ); + const projectDetails = projectId ? projectStore.project_details[projectId.toString()] : null; + const states = projectStore?.projectStates || null; const priorities = ISSUE_PRIORITIES || null; const labels = projectStore?.projectLabels || null; const members = projectStore?.projectMembers || null; const stateGroups = ISSUE_STATE_GROUPS || null; - const projects = projectStore?.projectStates || null; - const estimates = null; + const projects = workspaceSlug ? projectStore?.projects[workspaceSlug.toString()] || null : null; + const estimates = + projectDetails?.estimate !== null + ? projectStore.projectEstimates?.find((e) => e.id === projectDetails?.estimate) || null + : null; return (
@@ -67,9 +74,10 @@ export const ListLayout: FC = observer(() => { stateGroups={stateGroups} priorities={priorities} labels={labels} - members={members} + members={members?.map((m) => m.member) ?? null} projects={projects} - estimates={estimates} + enableQuickIssueCreate + estimates={estimates?.points ? orderArrayBy(estimates.points, "key") : null} />
); diff --git a/web/components/issues/issue-layouts/list/view-root.tsx b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx similarity index 94% rename from web/components/issues/issue-layouts/list/view-root.tsx rename to web/components/issues/issue-layouts/list/roots/project-view-root.tsx index aa7ab563ac..85b8177b3d 100644 --- a/web/components/issues/issue-layouts/list/view-root.tsx +++ b/web/components/issues/issue-layouts/list/roots/project-view-root.tsx @@ -1,7 +1,7 @@ import React from "react"; import { observer } from "mobx-react-lite"; // components -import { List } from "./default"; +import { List } from "../default"; // store import { useMobxStore } from "lib/mobx/store-provider"; import { RootStore } from "store/root"; @@ -10,7 +10,7 @@ import { ISSUE_STATE_GROUPS, ISSUE_PRIORITIES } from "constants/issue"; export interface IViewListLayout {} -export const ViewListLayout: React.FC = observer(() => { +export const ProjectViewListLayout: React.FC = observer(() => { const { project: projectStore, issue: issueStore, issueFilter: issueFilterStore }: RootStore = useMobxStore(); const issues = issueStore?.getIssues; diff --git a/web/components/issues/issue-layouts/properties/assignee.tsx b/web/components/issues/issue-layouts/properties/assignee.tsx index a235ec5a28..6a209f3b63 100644 --- a/web/components/issues/issue-layouts/properties/assignee.tsx +++ b/web/components/issues/issue-layouts/properties/assignee.tsx @@ -1,252 +1,28 @@ -import { FC, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; -import { ChevronDown, Search, X, Check } from "lucide-react"; import { observer } from "mobx-react-lite"; // components -import { Tooltip } from "@plane/ui"; -// hooks -import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; - -interface IFiltersOption { - id: string; - title: string; - avatar: string; -} +import { MembersSelect } from "components/project"; +// types +import { IUserLite } from "types"; export interface IIssuePropertyAssignee { - value?: any; - onChange?: (id: any, data: any) => void; + value: string[]; + onChange: (data: string[]) => void; + members: IUserLite[] | null; disabled?: boolean; - list?: any; - - className?: string; - buttonClassName?: string; - optionsClassName?: string; - dropdownArrow?: boolean; + hideDropdownArrow?: boolean; } -export const IssuePropertyAssignee: FC = observer((props) => { - const { value, onChange, disabled, list, className, buttonClassName, optionsClassName, dropdownArrow = true } = props; - - const dropdownBtn = useRef(null); - const dropdownOptions = useRef(null); - - const [isOpen, setIsOpen] = useState(false); - const [search, setSearch] = useState(""); - - const options: IFiltersOption[] | [] = - (list && - list?.length > 0 && - list.map((_member: any) => ({ - id: _member?.member?.id, - title: _member?.member?.display_name, - avatar: _member?.member?.avatar && _member?.member?.avatar !== "" ? _member?.member?.avatar : null, - }))) || - []; - - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); - - const selectedOption: IFiltersOption[] = - (value && value?.length > 0 && options.filter((_member: IFiltersOption) => value.includes(_member.id))) || []; - - const filteredOptions: IFiltersOption[] = - search === "" - ? options && options.length > 0 - ? options - : [] - : options && options.length > 0 - ? options.filter((_member: IFiltersOption) => - _member.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) - ) - : []; - - const assigneeRenderLength = 5; +export const IssuePropertyAssignee: React.FC = observer((props) => { + const { value, onChange, members, disabled = false, hideDropdownArrow = false } = props; return ( - _member.id) as string[]} - onChange={(data: string[]) => { - if (onChange && selectedOption) onChange(data, selectedOption); - }} + - {({ open }: { open: boolean }) => { - if (open) { - if (!isOpen) setIsOpen(true); - } else if (isOpen) setIsOpen(false); - - return ( - <> - - {selectedOption && selectedOption?.length > 0 ? ( - <> - {selectedOption?.length > 1 ? ( - _label.title) || []).join(", ")} - > -
- {selectedOption.slice(0, assigneeRenderLength).map((_assignee) => ( -
- {_assignee && _assignee.avatar ? ( - {_assignee.title} - ) : ( - _assignee.title[0] - )} -
- ))} - {selectedOption.length > assigneeRenderLength && ( -
- +{selectedOption?.length - assigneeRenderLength} -
- )} -
-
- ) : ( - _label.title) || []).join(", ")} - > -
-
- {selectedOption[0] && selectedOption[0].avatar ? ( - {selectedOption[0].title} - ) : ( -
- {selectedOption[0].title[0]} -
- )} -
-
{selectedOption[0].title}
-
-
- )} - - ) : ( - -
Select Assignees
-
- )} - - {dropdownArrow && !disabled && ( -
- -
- )} -
- -
- - {options && options.length > 0 ? ( - <> -
-
- -
- -
- setSearch(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
- - {search && search.length > 0 && ( -
setSearch("")} - className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" - > - -
- )} -
- -
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `cursor-pointer select-none truncate rounded px-1 py-1.5 ${ - active || (value && value.length > 0 && value.includes(option?.id)) - ? "bg-custom-background-80" - : "" - } ${ - value && value.length > 0 && value.includes(option?.id) - ? "text-custom-text-100" - : "text-custom-text-200" - }` - } - > -
-
- {option && option.avatar ? ( - {option.title} - ) : ( -
- {option.title[0]} -
- )} -
-
{option.title}
- {value && value.length > 0 && value.includes(option?.id) && ( -
- -
- )} -
-
- )) - ) : ( - -

No matching results

-
- ) - ) : ( -

Loading...

- )} -
- - ) : ( -

No options available.

- )} -
-
- - ); - }} -
+ hideDropdownArrow={hideDropdownArrow} + multiple + /> ); }); diff --git a/web/components/issues/issue-layouts/properties/date.tsx b/web/components/issues/issue-layouts/properties/date.tsx index 48b6de5072..dbcbc0eac6 100644 --- a/web/components/issues/issue-layouts/properties/date.tsx +++ b/web/components/issues/issue-layouts/properties/date.tsx @@ -15,85 +15,81 @@ import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; import { renderDateFormat } from "helpers/date-time.helper"; export interface IIssuePropertyDate { - value?: any; - onChange?: (date: any) => void; + value: any; + onChange: (date: any) => void; disabled?: boolean; placeHolder?: string; } -export const IssuePropertyDate: React.FC = observer( - ({ value, onChange, disabled, placeHolder }) => { - const dropdownBtn = React.useRef(null); - const dropdownOptions = React.useRef(null); +export const IssuePropertyDate: React.FC = observer((props) => { + const { value, onChange, disabled, placeHolder } = props; - const [isOpen, setIsOpen] = React.useState(false); + const dropdownBtn = React.useRef(null); + const dropdownOptions = React.useRef(null); - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); + const [isOpen, setIsOpen] = React.useState(false); - return ( - - {({ open }) => { - if (open) { - if (!isOpen) setIsOpen(true); - } else if (isOpen) setIsOpen(false); + useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); - return ( - <> - - -
-
- -
- {value ? ( - <> -
{value}
-
{ - if (onChange) onChange(null); - }} - > - -
- - ) : ( -
{placeHolder ? placeHolder : `Select date`}
- )} -
-
-
+ return ( + + {({ open }) => { + if (open) { + if (!isOpen) setIsOpen(true); + } else if (isOpen) setIsOpen(false); -
- - {({ close }) => ( - { - if (onChange && val) { - onChange(renderDateFormat(val)); - close(); - } - }} - dateFormat="dd-MM-yyyy" - calendarClassName="h-full" - inline - /> + return ( + <> + + +
+ + {value && ( + <> +
{value}
+
{ + if (onChange) onChange(null); + }} + > + +
+ )} - -
- - ); - }} - - ); - } -); +
+ + + +
+ + {({ close }) => ( + { + if (onChange && val) { + onChange(renderDateFormat(val)); + close(); + } + }} + dateFormat="dd-MM-yyyy" + calendarClassName="h-full" + inline + /> + )} + +
+ + ); + }} +
+ ); +}); diff --git a/web/components/issues/issue-layouts/properties/estimates.tsx b/web/components/issues/issue-layouts/properties/estimates.tsx index 80b3d96156..83de934cb4 100644 --- a/web/components/issues/issue-layouts/properties/estimates.tsx +++ b/web/components/issues/issue-layouts/properties/estimates.tsx @@ -1,217 +1,28 @@ -import React from "react"; -// headless ui -import { Combobox } from "@headlessui/react"; -// lucide icons -import { ChevronDown, Search, X, Check, Triangle } from "lucide-react"; -// mobx import { observer } from "mobx-react-lite"; // components -import { Tooltip } from "@plane/ui"; -// hooks -import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; -// mobx -import { useMobxStore } from "lib/mobx/store-provider"; -import { RootStore } from "store/root"; - -interface IFiltersOption { - id: string; - title: string; - key: string; -} +import { EstimateSelect } from "components/estimates"; +// types +import { IEstimatePoint } from "types"; export interface IIssuePropertyEstimates { - value?: any; - onChange?: (id: any) => void; + value: number | null; + onChange: (value: number | null) => void; + estimatePoints: IEstimatePoint[] | null; disabled?: boolean; - - workspaceSlug?: string; - projectId?: string; - - className?: string; - buttonClassName?: string; - optionsClassName?: string; - dropdownArrow?: boolean; + hideDropdownArrow?: boolean; } -export const IssuePropertyEstimates: React.FC = observer( - ({ - value, - onChange, - disabled, +export const IssuePropertyEstimates: React.FC = observer((props) => { + const { value, onChange, estimatePoints, disabled, hideDropdownArrow = false } = props; - workspaceSlug, - projectId, - - className, - buttonClassName, - optionsClassName, - dropdownArrow = true, - }) => { - const { project: projectStore }: RootStore = useMobxStore(); - - const dropdownBtn = React.useRef(null); - const dropdownOptions = React.useRef(null); - - const [isOpen, setIsOpen] = React.useState(false); - const [search, setSearch] = React.useState(""); - - const projectDetail = - (workspaceSlug && projectId && projectStore?.getProjectById(workspaceSlug, projectId)) || null; - const projectEstimateId = (projectDetail && projectDetail?.estimate) || null; - const estimates = (projectEstimateId && projectStore?.getProjectEstimateById(projectEstimateId)) || null; - - const options: IFiltersOption[] | [] = - (estimates && - estimates.points && - estimates.points.length > 0 && - estimates.points.map((_estimate) => ({ - id: _estimate?.id, - title: _estimate?.value, - key: _estimate?.key.toString(), - }))) || - []; - - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); - - const selectedOption: IFiltersOption | null | undefined = - (value && options.find((_estimate: IFiltersOption) => _estimate.key === value)) || null; - - const filteredOptions: IFiltersOption[] = - search === "" - ? options && options.length > 0 - ? options - : [] - : options && options.length > 0 - ? options.filter((_estimate: IFiltersOption) => - _estimate.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) - ) - : []; - - return ( - { - if (onChange) onChange(data); - }} - disabled={disabled} - > - {({ open }: { open: boolean }) => { - if (open) { - if (!isOpen) setIsOpen(true); - } else if (isOpen) setIsOpen(false); - - return ( - <> - - {selectedOption ? ( - -
-
- -
-
{selectedOption?.title}
-
-
- ) : ( - -
Select Estimates
-
- )} - - {dropdownArrow && !disabled && ( -
- -
- )} -
- -
- - {options && options.length > 0 ? ( - <> -
-
- -
- -
- setSearch(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
- - {search && search.length > 0 && ( -
setSearch("")} - className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" - > - -
- )} -
- -
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `cursor-pointer select-none truncate rounded px-1 py-1.5 ${ - active || selected ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( -
-
- -
-
{option.title}
- {selected && ( -
- -
- )} -
- )} -
- )) - ) : ( - -

No matching results

-
- ) - ) : ( -

Loading...

- )} -
- - ) : ( -

No options available.

- )} -
-
- - ); - }} -
- ); - } -); + return ( + + ); +}); diff --git a/web/components/issues/issue-layouts/properties/labels.tsx b/web/components/issues/issue-layouts/properties/labels.tsx index bc9930c72c..dcc884b193 100644 --- a/web/components/issues/issue-layouts/properties/labels.tsx +++ b/web/components/issues/issue-layouts/properties/labels.tsx @@ -1,230 +1,28 @@ -import { FC, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; -import { ChevronDown, Search, X, Check } from "lucide-react"; import { observer } from "mobx-react-lite"; // components -import { Tooltip } from "@plane/ui"; -// hooks -import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; - -interface IFiltersOption { - id: string; - title: string; - color: string | null; -} +import { LabelSelect } from "components/project"; +// types +import { IIssueLabels } from "types"; export interface IIssuePropertyLabels { - value?: any; - onChange?: (id: any, data: any) => void; + value: string[]; + onChange: (data: string[]) => void; + labels: IIssueLabels[] | null; disabled?: boolean; - list?: any; - - className?: string; - buttonClassName?: string; - optionsClassName?: string; - dropdownArrow?: boolean; + hideDropdownArrow?: boolean; } -export const IssuePropertyLabels: FC = observer((props) => { - const { - value, - onChange, - disabled, - list, - - className, - buttonClassName, - optionsClassName, - dropdownArrow = true, - } = props; - - const dropdownBtn = useRef(null); - const dropdownOptions = useRef(null); - - const [isOpen, setIsOpen] = useState(false); - const [search, setSearch] = useState(""); - - const options: IFiltersOption[] | [] = - (list && - list?.length > 0 && - list.map((_label: any) => ({ - id: _label?.id, - title: _label?.name, - color: _label?.color || null, - }))) || - []; - - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); - - const selectedOption: IFiltersOption[] = - (value && value?.length > 0 && options.filter((_label: IFiltersOption) => value.includes(_label.id))) || []; - - const filteredOptions: IFiltersOption[] = - search === "" - ? options && options.length > 0 - ? options - : [] - : options && options.length > 0 - ? options.filter((_label: IFiltersOption) => - _label.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) - ) - : []; +export const IssuePropertyLabels: React.FC = observer((props) => { + const { value, onChange, labels, disabled, hideDropdownArrow = false } = props; return ( - _label.id) as string[]} - onChange={(data: string[]) => { - if (onChange && selectedOption) onChange(data, selectedOption); - }} + - {({ open }: { open: boolean }) => { - if (open) { - if (!isOpen) setIsOpen(true); - } else if (isOpen) setIsOpen(false); - - return ( - <> - - {selectedOption && selectedOption?.length > 0 ? ( - <> - {selectedOption?.length === 1 ? ( - _label.title) || []).join(", ")} - > -
-
-
{selectedOption[0]?.title}
-
- - ) : ( - _label.title) || []).join(", ")} - > -
-
-
{selectedOption?.length} Labels
-
- - )} - - ) : ( - -
Select Labels
-
- )} - - {dropdownArrow && !disabled && ( -
- -
- )} - - -
- - {options && options.length > 0 ? ( - <> -
-
- -
- -
- setSearch(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
- - {search && search.length > 0 && ( -
setSearch("")} - className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" - > - -
- )} -
- -
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `cursor-pointer select-none truncate rounded px-1 py-1.5 ${ - active || (value && value.length > 0 && value.includes(option?.id)) - ? "bg-custom-background-80" - : "" - } ${ - value && value.length > 0 && value.includes(option?.id) - ? "text-custom-text-100" - : "text-custom-text-200" - }` - } - > -
-
-
{option.title}
- {value && value.length > 0 && value.includes(option?.id) && ( -
- -
- )} -
- - )) - ) : ( - -

No matching results

-
- ) - ) : ( -

Loading...

- )} -
- - ) : ( -

No options available.

- )} - -
- - ); - }} - + hideDropdownArrow={hideDropdownArrow} + /> ); }); diff --git a/web/components/issues/issue-layouts/properties/priority.tsx b/web/components/issues/issue-layouts/properties/priority.tsx index 37404c11b3..cbf6602eb1 100644 --- a/web/components/issues/issue-layouts/properties/priority.tsx +++ b/web/components/issues/issue-layouts/properties/priority.tsx @@ -1,223 +1,25 @@ -import { FC, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; -import { ChevronDown, Search, X, Check, AlertCircle, SignalHigh, SignalMedium, SignalLow, Ban } from "lucide-react"; +import { PrioritySelect } from "components/project"; import { observer } from "mobx-react-lite"; -// components -import { Tooltip } from "@plane/ui"; -// hooks -import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; - -interface IFiltersOption { - id: string; - title: string; -} +// types +import { TIssuePriorities } from "types"; export interface IIssuePropertyPriority { - value?: any; - onChange?: (id: any, data: IFiltersOption) => void; + value: TIssuePriorities; + onChange: (value: TIssuePriorities) => void; disabled?: boolean; - list?: any; - - className?: string; - buttonClassName?: string; - optionsClassName?: string; - dropdownArrow?: boolean; + hideDropdownArrow?: boolean; } -const Icon = ({ priority }: any) => ( -
- {priority === "urgent" ? ( -
- -
- ) : priority === "high" ? ( -
- -
- ) : priority === "medium" ? ( -
- -
- ) : priority === "low" ? ( -
- -
- ) : ( -
- -
- )} -
-); - -export const IssuePropertyPriority: FC = observer((props) => { - const { - value, - onChange, - disabled, - list, - - className, - buttonClassName, - optionsClassName, - dropdownArrow = true, - } = props; - - const dropdownBtn = useRef(null); - const dropdownOptions = useRef(null); - - const [isOpen, setIsOpen] = useState(false); - const [search, setSearch] = useState(""); - - const options: IFiltersOption[] | [] = - (list && - list?.length > 0 && - list.map((_priority: any) => ({ - id: _priority?.key, - title: _priority?.title, - }))) || - []; - - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); - - const selectedOption: IFiltersOption | null | undefined = - (value && options.find((_priority: IFiltersOption) => _priority.id === value)) || null; - - const filteredOptions: IFiltersOption[] = - search === "" - ? options && options.length > 0 - ? options - : [] - : options && options.length > 0 - ? options.filter((_priority: IFiltersOption) => - _priority.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) - ) - : []; +export const IssuePropertyPriority: React.FC = observer((props) => { + const { value, onChange, disabled, hideDropdownArrow = false } = props; return ( - { - if (onChange && selectedOption) onChange(data, selectedOption); - }} + - {({ open }: { open: boolean }) => { - if (open) { - if (!isOpen) setIsOpen(true); - } else if (isOpen) setIsOpen(false); - - return ( - <> - - {selectedOption ? ( - -
-
- -
-
{selectedOption?.title}
-
-
- ) : ( - -
Select Priority
-
- )} - - {dropdownArrow && !disabled && ( -
- -
- )} -
- -
- - {options && options.length > 0 ? ( - <> -
-
- -
- -
- setSearch(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
- - {search && search.length > 0 && ( -
setSearch("")} - className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" - > - -
- )} -
- -
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `cursor-pointer select-none truncate rounded px-1 py-1.5 ${ - active || selected ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( -
-
- -
-
{option.title}
- {selected && ( -
- -
- )} -
- )} -
- )) - ) : ( - -

No matching results

-
- ) - ) : ( -

Loading...

- )} -
- - ) : ( -

No options available.

- )} -
-
- - ); - }} -
+ hideDropdownArrow={hideDropdownArrow} + /> ); }); diff --git a/web/components/issues/issue-layouts/properties/state.tsx b/web/components/issues/issue-layouts/properties/state.tsx index 05cb375ad4..9264b3084d 100644 --- a/web/components/issues/issue-layouts/properties/state.tsx +++ b/web/components/issues/issue-layouts/properties/state.tsx @@ -1,214 +1,28 @@ -import { FC, useRef, useState } from "react"; -import { Combobox } from "@headlessui/react"; -import { ChevronDown, Search, X, Check } from "lucide-react"; import { observer } from "mobx-react-lite"; // components -import { Tooltip, StateGroupIcon } from "@plane/ui"; -// hooks -import useDynamicDropdownPosition from "hooks/use-dynamic-dropdown"; - +import { StateSelect } from "components/states"; // types import { IState } from "types"; -interface IFiltersOption { - id: string; - title: string; - group: string; - color: string | null; -} - export interface IIssuePropertyState { - value?: any; - onChange?: (id: any, data: IFiltersOption) => void; + value: IState; + onChange: (state: IState) => void; + states: IState[] | null; disabled?: boolean; - list?: any; - - className?: string; - buttonClassName?: string; - optionsClassName?: string; - dropdownArrow?: boolean; + hideDropdownArrow?: boolean; } -export const IssuePropertyState: FC = observer((props) => { - const { - value, - onChange, - disabled, - list, - - className, - buttonClassName, - optionsClassName, - dropdownArrow = true, - } = props; - - const dropdownBtn = useRef(null); - const dropdownOptions = useRef(null); - - const [isOpen, setIsOpen] = useState(false); - const [search, setSearch] = useState(""); - - const options: IFiltersOption[] | [] = - (list && - list?.length > 0 && - list.map((_state: IState) => ({ - id: _state?.id, - title: _state?.name, - group: _state?.group, - color: _state?.color || null, - }))) || - []; - - useDynamicDropdownPosition(isOpen, () => setIsOpen(false), dropdownBtn, dropdownOptions); - - const selectedOption: IFiltersOption | null | undefined = - (value && options.find((_state: IFiltersOption) => _state.id === value)) || null; - - const filteredOptions: IFiltersOption[] = - search === "" - ? options && options.length > 0 - ? options - : [] - : options && options.length > 0 - ? options.filter((_state: IFiltersOption) => - _state.title.toLowerCase().replace(/\s+/g, "").includes(search.toLowerCase().replace(/\s+/g, "")) - ) - : []; +export const IssuePropertyState: React.FC = observer((props) => { + const { value, onChange, states, disabled, hideDropdownArrow = false } = props; return ( - { - if (onChange && selectedOption) onChange(data, selectedOption); - }} + - {({ open }: { open: boolean }) => { - if (open) { - if (!isOpen) setIsOpen(true); - } else if (isOpen) setIsOpen(false); - - return ( - <> - - {selectedOption ? ( - -
-
- -
-
{selectedOption?.title}
-
-
- ) : ( - -
Select State
-
- )} - - {dropdownArrow && !disabled && ( -
- -
- )} -
- -
- - {options && options.length > 0 ? ( - <> -
-
- -
- -
- setSearch(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
- - {search && search.length > 0 && ( -
setSearch("")} - className="flex-shrink-0 flex justify-center items-center w-[16px] h-[16px] rounded-sm cursor-pointer hover:bg-custom-background-80" - > - -
- )} -
- -
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `cursor-pointer select-none truncate rounded px-1 py-1.5 ${ - active || selected ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( -
-
- -
-
{option.title}
- {selected && ( -
- -
- )} -
- )} -
- )) - ) : ( - -

No matching results

-
- ) - ) : ( -

Loading...

- )} -
- - ) : ( -

No options available.

- )} -
-
- - ); - }} -
+ hideDropdownArrow={hideDropdownArrow} + /> ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx index 7873ebe020..81f45d04f8 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/state-column.tsx @@ -4,6 +4,8 @@ import React from "react"; import { StateSelect } from "components/states"; // hooks import useSubIssue from "hooks/use-sub-issue"; +// helpers +import { getStatesList } from "helpers/state.helper"; // types import { IIssue, IStateResponse } from "types"; @@ -22,12 +24,14 @@ export const SpreadsheetStateColumn: React.FC = (props) => { const { subIssues, isLoading } = useSubIssue(issue.project_detail.id, issue.id, isExpanded); + const statesList = getStatesList(states); + return ( <> onChange({ state: data.id, state_detail: data })} - stateGroups={states} + states={statesList} buttonClassName="!shadow-none !border-0" hideDropdownArrow disabled={disabled} diff --git a/web/components/issues/issue-layouts/spreadsheet/index.ts b/web/components/issues/issue-layouts/spreadsheet/index.ts index 64b02a7661..5b14a2dab1 100644 --- a/web/components/issues/issue-layouts/spreadsheet/index.ts +++ b/web/components/issues/issue-layouts/spreadsheet/index.ts @@ -2,3 +2,4 @@ export * from "./columns"; export * from "./roots"; export * from "./spreadsheet-column"; export * from "./spreadsheet-view"; +export * from "./inline-create-issue-form"; diff --git a/web/components/issues/issue-layouts/spreadsheet/inline-create-issue-form.tsx b/web/components/issues/issue-layouts/spreadsheet/inline-create-issue-form.tsx new file mode 100644 index 0000000000..20105a67eb --- /dev/null +++ b/web/components/issues/issue-layouts/spreadsheet/inline-create-issue-form.tsx @@ -0,0 +1,207 @@ +import { useEffect, useState, useRef } from "react"; +import { useRouter } from "next/router"; +import { useForm } from "react-hook-form"; +import { Transition } from "@headlessui/react"; + +// hooks +import useToast from "hooks/use-toast"; +import useKeypress from "hooks/use-keypress"; +import useProjectDetails from "hooks/use-project-details"; +import useOutsideClickDetector from "hooks/use-outside-click-detector"; + +// store +import { observer } from "mobx-react-lite"; +import { useMobxStore } from "lib/mobx/store-provider"; + +// constants +import { createIssuePayload } from "constants/issue"; + +// types +import { IIssue } from "types"; +import { PlusIcon } from "lucide-react"; + +type Props = { + groupId?: string; + prePopulatedData?: Partial; + onSuccess?: (data: IIssue) => Promise | void; +}; + +const defaultValues: Partial = { + name: "", +}; + +const Inputs = (props: any) => { + const { register, setFocus, projectDetails } = props; + + useEffect(() => { + setFocus("name"); + }, [setFocus]); + + return ( + <> +

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

+ + + ); +}; + +export const SpreadsheetInlineCreateIssueForm: React.FC = observer((props) => { + const { prePopulatedData, groupId } = props; + + // router + const router = useRouter(); + const { workspaceSlug, projectId } = router.query; + + // store + const { workspace: workspaceStore, quickAddIssue: quickAddStore } = useMobxStore(); + + const { projectDetails } = useProjectDetails(); + + const { + reset, + handleSubmit, + setFocus, + register, + formState: { errors, isSubmitting }, + } = useForm({ defaultValues }); + + // ref + const ref = useRef(null); + + // states + const [isOpen, setIsOpen] = useState(false); + + const handleClose = () => setIsOpen(false); + + // hooks + useKeypress("Escape", handleClose); + useOutsideClickDetector(ref, handleClose); + const { setToastAlert } = useToast(); + + // derived values + const workspaceDetail = workspaceStore.getWorkspaceBySlug(workspaceSlug?.toString()!); + + useEffect(() => { + setFocus("name"); + }, [setFocus, isOpen]); + + useEffect(() => { + if (!isOpen) reset({ ...defaultValues }); + }, [isOpen, reset]); + + 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 onSubmitHandler = async (formData: IIssue) => { + if (isSubmitting || !workspaceSlug || !projectId) return; + + // resetting the form so that user can add another issue quickly + reset({ ...defaultValues }); + + const payload = createIssuePayload(workspaceDetail!, projectDetails!, { + ...(prePopulatedData ?? {}), + ...formData, + labels_list: + formData.labels_list && formData.labels_list?.length !== 0 + ? formData.labels_list + : prePopulatedData?.labels && prePopulatedData?.labels.toString() !== "none" + ? [prePopulatedData.labels as any] + : [], + assignees_list: + formData.assignees_list && formData.assignees_list?.length !== 0 + ? formData.assignees_list + : prePopulatedData?.assignees && prePopulatedData?.assignees.toString() !== "none" + ? [prePopulatedData.assignees as any] + : [], + }); + + try { + quickAddStore.createIssue( + workspaceSlug.toString(), + projectId.toString(), + { + group_id: groupId ?? null, + sub_group_id: null, + }, + payload + ); + + setToastAlert({ + type: "success", + title: "Success!", + message: "Issue created successfully.", + }); + } catch (err: any) { + 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 ( +
+ +
+
+ + +
+
+ + {isOpen && ( +

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

+ )} + + {!isOpen && ( + + )} +
+ ); +}); diff --git a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx index 78940ceafd..a3856f5fb2 100644 --- a/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/roots/project-root.tsx @@ -66,6 +66,7 @@ export const ProjectSpreadsheetLayout: React.FC = observer(() => { handleIssueAction={() => {}} handleUpdateIssue={handleUpdateIssue} disableUserActions={false} + enableQuickCreateIssue /> ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx index 521773f6bc..4d2a7cd048 100644 --- a/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/spreadsheet-view.tsx @@ -3,11 +3,7 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; import { PlusIcon } from "lucide-react"; // components -import { - SpreadsheetColumnsList, - // ListInlineCreateIssueForm, - SpreadsheetIssuesColumn, -} from "components/issues"; +import { SpreadsheetColumnsList, SpreadsheetIssuesColumn, SpreadsheetInlineCreateIssueForm } from "components/issues"; import { CustomMenu, Spinner } from "@plane/ui"; // types import { @@ -31,6 +27,7 @@ type Props = { handleUpdateIssue: (issue: IIssue, data: Partial) => void; openIssuesListModal?: (() => void) | null; disableUserActions: boolean; + enableQuickCreateIssue?: boolean; }; export const SpreadsheetView: React.FC = observer((props) => { @@ -46,6 +43,7 @@ export const SpreadsheetView: React.FC = observer((props) => { handleUpdateIssue, openIssuesListModal, disableUserActions, + enableQuickCreateIssue, } = props; const [expandedIssues, setExpandedIssues] = useState([]); @@ -138,17 +136,10 @@ export const SpreadsheetView: React.FC = observer((props) => {
- {/* setIsInlineCreateIssueFormOpen(false)} - prePopulatedData={{ - ...(cycleId && { cycle: cycleId.toString() }), - ...(moduleId && { module: moduleId.toString() }), - }} - /> */} + {enableQuickCreateIssue && }
- {!disableUserActions && + {/* {!disableUserActions && !isInlineCreateIssueFormOpen && (type === "issue" ? (
diff --git a/web/components/issues/issue-peek-overview/activity/card.tsx b/web/components/issues/issue-peek-overview/activity/card.tsx index b66b5754f1..8fbf8605ad 100644 --- a/web/components/issues/issue-peek-overview/activity/card.tsx +++ b/web/components/issues/issue-peek-overview/activity/card.tsx @@ -12,6 +12,7 @@ import { render24HourFormatTime, renderLongDateFormat, timeAgo } from "helpers/d interface IssueActivityCard { workspaceSlug: string; projectId: string; + issueId: string; user: any; issueComments: any; issueCommentUpdate: (comment: any) => void; @@ -24,6 +25,7 @@ export const IssueActivityCard: FC = (props) => { const { workspaceSlug, projectId, + issueId, user, issueComments, issueCommentUpdate, @@ -118,6 +120,7 @@ export const IssueActivityCard: FC = (props) => { void; issueCommentReactionRemove: (commentId: string, reaction: string) => void; @@ -36,6 +37,7 @@ export const IssueCommentCard: React.FC = (props) => { showAccessSpecifier = false, workspaceSlug, projectId, + issueId, user, issueCommentReactionCreate, issueCommentReactionRemove, @@ -157,6 +159,7 @@ export const IssueCommentCard: React.FC = (props) => { = observer((props) => { - const { workspaceSlug, projectId, user, comment, issueCommentReactionCreate, issueCommentReactionRemove } = props; + const { workspaceSlug, projectId, issueId, user, comment, issueCommentReactionCreate, issueCommentReactionRemove } = + props; const { issueDetail: issueDetailStore }: RootStore = useMobxStore(); @@ -32,15 +34,18 @@ export const IssueCommentReaction: FC = observer((props) }; useSWR( - workspaceSlug && projectId && comment && comment?.id ? `ISSUE+PEEK_OVERVIEW_COMMENT_${comment?.id}` : null, + workspaceSlug && projectId && issueId && comment && comment?.id + ? `ISSUE+PEEK_OVERVIEW_COMMENT_${comment?.id}` + : null, () => { - if (workspaceSlug && projectId && comment && comment.id) { - issueDetailStore.fetchIssueCommentReactions(workspaceSlug, projectId, comment?.id); + if (workspaceSlug && projectId && issueId && comment && comment.id) { + issueDetailStore.fetchIssueCommentReactions(workspaceSlug, projectId, issueId, comment?.id); } } ); - const issueReactions = issueDetailStore?.getIssueCommentReactionsByCommentId(comment.id) || []; + let issueReactions = issueDetailStore?.getIssueCommentReactions || null; + issueReactions = issueReactions && comment.id ? issueReactions?.[comment.id] : []; return (
diff --git a/web/components/issues/issue-peek-overview/activity/view.tsx b/web/components/issues/issue-peek-overview/activity/view.tsx index f5db0f2978..d7f9bcf92e 100644 --- a/web/components/issues/issue-peek-overview/activity/view.tsx +++ b/web/components/issues/issue-peek-overview/activity/view.tsx @@ -6,6 +6,7 @@ import { IssueCommentEditor } from "./comment-editor"; interface IIssueComment { workspaceSlug: string; projectId: string; + issueId: string; user: any; issueComments: any; issueCommentCreate: (comment: any) => void; @@ -19,6 +20,7 @@ export const IssueComment: FC = (props) => { const { workspaceSlug, projectId, + issueId, user, issueComments, issueCommentCreate, @@ -46,6 +48,7 @@ export const IssueComment: FC = (props) => { ) => void; - - states: any; - members: any; + states: IState[] | null; + members: IUserLite[] | null; priorities: any; } export const PeekOverviewProperties: FC = (props) => { const { issue, issueUpdate, states, members, priorities } = props; - const handleState = (_state: string) => { - if (issueUpdate) issueUpdate({ ...issue, state: _state }); + const handleState = (_state: IState) => { + issueUpdate({ ...issue, state: _state.id }); }; - const handlePriority = (_priority: any) => { - if (issueUpdate) issueUpdate({ ...issue, priority: _priority }); + const handlePriority = (_priority: TIssuePriorities) => { + issueUpdate({ ...issue, priority: _priority }); }; const handleAssignee = (_assignees: string[]) => { - if (issueUpdate) issueUpdate({ ...issue, assignees: _assignees }); + issueUpdate({ ...issue, assignees: _assignees }); }; const handleStartDate = (_startDate: string) => { - if (issueUpdate) issueUpdate({ ...issue, start_date: _startDate }); + issueUpdate({ ...issue, start_date: _startDate }); }; const handleTargetDate = (_targetDate: string) => { - if (issueUpdate) issueUpdate({ ...issue, target_date: _targetDate }); + issueUpdate({ ...issue, target_date: _targetDate }); }; return ( @@ -54,11 +53,11 @@ export const PeekOverviewProperties: FC = (props) => {
handleState(id)} + value={issue?.state_detail || null} + onChange={handleState} + states={states} disabled={false} - list={states} + hideDropdownArrow={true} />
@@ -74,10 +73,10 @@ export const PeekOverviewProperties: FC = (props) => {
handleAssignee(ids)} disabled={false} - list={members} + hideDropdownArrow={true} + members={members} />
@@ -93,10 +92,9 @@ export const PeekOverviewProperties: FC = (props) => {
handlePriority(id)} + onChange={handlePriority} disabled={false} - list={priorities} + hideDropdownArrow={true} />
diff --git a/web/components/issues/issue-peek-overview/reactions/root.tsx b/web/components/issues/issue-peek-overview/reactions/root.tsx index 645ac6aab8..efa2e488c1 100644 --- a/web/components/issues/issue-peek-overview/reactions/root.tsx +++ b/web/components/issues/issue-peek-overview/reactions/root.tsx @@ -15,7 +15,7 @@ export const IssueReaction: FC = (props) => { const handleReaction = (reaction: string) => { const isReactionAvailable = - issueReactions[reaction].find((_reaction: any) => _reaction.actor === user?.id) ?? false; + issueReactions?.[reaction].find((_reaction: any) => _reaction.actor === user?.id) ?? false; if (isReactionAvailable) issueReactionRemove(reaction); else issueReactionCreate(reaction); diff --git a/web/components/issues/issue-peek-overview/root.tsx b/web/components/issues/issue-peek-overview/root.tsx index 8b96a31007..e94b728c08 100644 --- a/web/components/issues/issue-peek-overview/root.tsx +++ b/web/components/issues/issue-peek-overview/root.tsx @@ -50,10 +50,10 @@ export const IssuePeekOverview: FC = observer((props) => { issueDetailStore.removeIssueComment(workspaceSlug, projectId, issueId, commentId); const issueCommentReactionCreate = (commentId: string, reaction: string) => - issueDetailStore.creationIssueCommentReaction(workspaceSlug, projectId, commentId, reaction); + issueDetailStore.creationIssueCommentReaction(workspaceSlug, projectId, issueId, commentId, reaction); const issueCommentReactionRemove = (commentId: string, reaction: string) => - issueDetailStore.removeIssueCommentReaction(workspaceSlug, projectId, commentId, reaction); + issueDetailStore.removeIssueCommentReaction(workspaceSlug, projectId, issueId, commentId, reaction); return ( = observer((props) => { = observer((props) => { = observer((props) => { ); }; + const statesList = getStatesList(projectStore.states?.[issue.project]); + return (
{displayProperties.priority && ( @@ -132,7 +136,7 @@ export const IssueProperty: React.FC = observer((props) => {
handleStateChange(data)} hideDropdownArrow disabled={!editable} diff --git a/web/components/profile/navbar.tsx b/web/components/profile/navbar.tsx index cd43cc974f..4334525367 100644 --- a/web/components/profile/navbar.tsx +++ b/web/components/profile/navbar.tsx @@ -5,11 +5,9 @@ import Link from "next/link"; // components import { ProfileIssuesFilter } from "components/profile"; -// types -import { UserAuth } from "types"; type Props = { - memberRole: UserAuth; + isAuthorized: boolean; }; const viewerTabs = [ @@ -38,12 +36,11 @@ const adminTabs = [ }, ]; -export const ProfileNavbar: React.FC = ({ memberRole }) => { +export const ProfileNavbar: React.FC = ({ isAuthorized }) => { const router = useRouter(); const { workspaceSlug, userId } = router.query; - const tabsList = - memberRole.isOwner || memberRole.isMember || memberRole.isViewer ? [...viewerTabs, ...adminTabs] : viewerTabs; + const tabsList = isAuthorized ? [...viewerTabs, ...adminTabs] : viewerTabs; return (
diff --git a/web/components/project/delete-project-modal.tsx b/web/components/project/delete-project-modal.tsx index 0103416888..a40e374039 100644 --- a/web/components/project/delete-project-modal.tsx +++ b/web/components/project/delete-project-modal.tsx @@ -30,7 +30,7 @@ export const DeleteProjectModal: React.FC = (props) => { const { project: projectStore } = useMobxStore(); // router const router = useRouter(); - const { workspaceSlug } = router.query; + const { workspaceSlug, projectId } = router.query; // toast const { setToastAlert } = useToast(); // form info @@ -59,6 +59,8 @@ export const DeleteProjectModal: React.FC = (props) => { await projectStore .deleteProject(workspaceSlug.toString(), project.id) .then(() => { + if (projectId && projectId.toString() === project.id) router.push(`/${workspaceSlug}/projects`); + handleClose(); }) .catch(() => { diff --git a/web/components/project/index.ts b/web/components/project/index.ts index ff0213d524..040a0f3dff 100644 --- a/web/components/project/index.ts +++ b/web/components/project/index.ts @@ -1,18 +1,18 @@ +export * from "./publish-project"; +export * from "./settings"; +export * from "./card-list"; +export * from "./card"; export * from "./create-project-modal"; export * from "./delete-project-modal"; -export * from "./sidebar-list"; -export * from "./settings-sidebar"; -export * from "./single-integration-card"; -export * from "./sidebar-list-item"; +export * from "./delete-project-section"; +export * from "./form-loader"; +export * from "./form"; +export * from "./join-project-modal"; +export * from "./label-select"; export * from "./leave-project-modal"; export * from "./member-select"; export * from "./members-select"; -export * from "./label-select"; export * from "./priority-select"; -export * from "./card-list"; -export * from "./card"; -export * from "./join-project-modal"; -export * from "./form"; -export * from "./form-loader"; -export * from "./delete-project-section"; -export * from "./publish-project"; +export * from "./sidebar-list-item"; +export * from "./sidebar-list"; +export * from "./single-integration-card"; diff --git a/web/components/project/label-select.tsx b/web/components/project/label-select.tsx index f715793cbc..ebf7f47768 100644 --- a/web/components/project/label-select.tsx +++ b/web/components/project/label-select.tsx @@ -3,9 +3,6 @@ import { usePopper } from "react-popper"; import { Placement } from "@popperjs/core"; import { Combobox } from "@headlessui/react"; import { Check, ChevronDown, PlusIcon, Search } from "lucide-react"; - -// components -import { CreateLabelModal } from "components/labels"; // ui import { Tooltip } from "components/ui"; // types @@ -14,7 +11,7 @@ import { IIssueLabels } from "types"; type Props = { value: string[]; onChange: (data: string[]) => void; - labels: IIssueLabels[]; + labels: IIssueLabels[] | undefined; className?: string; buttonClassName?: string; optionsClassName?: string; @@ -41,10 +38,16 @@ export const LabelSelect: React.FC = ({ const [referenceElement, setReferenceElement] = useState(null); const [popperElement, setPopperElement] = useState(null); - const [labelModal, setLabelModal] = useState(false); - const { styles, attributes } = usePopper(referenceElement, popperElement, { placement: placement ?? "bottom-start", + modifiers: [ + { + name: "preventOverflow", + options: { + padding: 12, + }, + }, + ], }); const options = labels?.map((label) => ({ @@ -66,149 +69,126 @@ export const LabelSelect: React.FC = ({ const filteredOptions = query === "" ? options : options?.filter((option) => option.query.toLowerCase().includes(query.toLowerCase())); - const footerOption = ( - - ); - return ( - <> - {/* TODO: update this logic */} - {/* {projectId && ( - setLabelModal(false)} - projectId={projectId} - user={user} - /> - )} */} - - - - - - -
-
- - setQuery(e.target.value)} - placeholder="Search" - displayValue={(assigned: any) => assigned?.name} - /> -
-
- {filteredOptions ? ( - filteredOptions.length > 0 ? ( - filteredOptions.map((option) => ( - - `flex items-center justify-between gap-2 cursor-pointer select-none truncate rounded px-1 py-1.5 ${ - active && !selected ? "bg-custom-background-80" : "" - } ${selected ? "text-custom-text-100" : "text-custom-text-200"}` - } - > - {({ selected }) => ( - <> - {option.content} - {selected && } - - )} - - )) - ) : ( - -

No matching results

-
- ) - ) : ( -

Loading...

- )} -
- {footerOption} +
+ value.includes(l.id)) + .map((l) => l.name) + .join(", ")} + > +
+ + {`${value.length} Labels`} +
+
+
+ ) + ) : ( +
+ Select labels +
+ )}
-
-
- + {!hideDropdownArrow && !disabled &&