diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index e3c1cd69d..7c465a010 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -67,6 +67,7 @@ from .issue import ( IssueRelationSerializer, RelatedIssueSerializer, IssuePublicSerializer, + IssueDetailSerializer, ) from .module import ( diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index be98bc312..90069bd41 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -586,7 +586,6 @@ class IssueSerializer(DynamicBaseSerializer): "id", "name", "state_id", - "description_html", "sort_order", "completed_at", "estimate_point", @@ -618,6 +617,13 @@ class IssueSerializer(DynamicBaseSerializer): return [module for module in obj.issue_module.values_list("module_id", flat=True)] +class IssueDetailSerializer(IssueSerializer): + description_html = serializers.CharField() + + class Meta(IssueSerializer.Meta): + fields = IssueSerializer.Meta.fields + ['description_html'] + + class IssueLiteSerializer(DynamicBaseSerializer): workspace_detail = WorkspaceLiteSerializer( read_only=True, source="workspace" diff --git a/apiserver/plane/app/views/issue.py b/apiserver/plane/app/views/issue.py index 34bce8a0a..c8845150a 100644 --- a/apiserver/plane/app/views/issue.py +++ b/apiserver/plane/app/views/issue.py @@ -50,6 +50,7 @@ from plane.app.serializers import ( CommentReactionSerializer, IssueRelationSerializer, RelatedIssueSerializer, + IssueDetailSerializer, ) from plane.app.permissions import ( ProjectEntityPermission, @@ -267,7 +268,7 @@ class IssueViewSet(WebhookMixin, BaseViewSet): def retrieve(self, request, slug, project_id, pk=None): issue = self.get_queryset().filter(pk=pk).first() return Response( - IssueSerializer( + IssueDetailSerializer( issue, fields=self.fields, expand=self.expand ).data, status=status.HTTP_200_OK, diff --git a/apiserver/requirements/base.txt b/apiserver/requirements/base.txt index 0e7a18fa8..194bf8d90 100644 --- a/apiserver/requirements/base.txt +++ b/apiserver/requirements/base.txt @@ -1,6 +1,6 @@ # base requirements -Django==4.2.7 +Django==4.2.10 psycopg==3.1.12 djangorestframework==3.14.0 redis==4.6.0 @@ -30,7 +30,7 @@ openpyxl==3.1.2 beautifulsoup4==4.12.2 dj-database-url==2.1.0 posthog==3.0.2 -cryptography==41.0.6 +cryptography==42.0.0 lxml==4.9.3 boto3==1.28.40 diff --git a/packages/editor/document-editor/src/ui/components/editor-header.tsx b/packages/editor/document-editor/src/ui/components/editor-header.tsx index 3501785a7..a322ddddc 100644 --- a/packages/editor/document-editor/src/ui/components/editor-header.tsx +++ b/packages/editor/document-editor/src/ui/components/editor-header.tsx @@ -42,8 +42,8 @@ export const EditorHeader = (props: IEditorHeader) => { } = props; return ( -
-
+
+
{ />
-
+
{!readonly && uploadFile && ( )} diff --git a/packages/editor/document-editor/src/ui/components/page-renderer.tsx b/packages/editor/document-editor/src/ui/components/page-renderer.tsx index c60ac0e7a..06b9e70ff 100644 --- a/packages/editor/document-editor/src/ui/components/page-renderer.tsx +++ b/packages/editor/document-editor/src/ui/components/page-renderer.tsx @@ -152,7 +152,7 @@ export const PageRenderer = (props: IPageRenderer) => { ); return ( -
+
{!readonly ? ( handlePageTitleChange(e.target.value)} diff --git a/packages/editor/document-editor/src/ui/components/summary-popover.tsx b/packages/editor/document-editor/src/ui/components/summary-popover.tsx index d3ec64f1c..12903bb3d 100644 --- a/packages/editor/document-editor/src/ui/components/summary-popover.tsx +++ b/packages/editor/document-editor/src/ui/components/summary-popover.tsx @@ -40,16 +40,30 @@ export const SummaryPopover: React.FC = (props) => { > - {!sidePeekVisible && ( -
- -
- )} +
+ {sidePeekVisible && ( +
+ +
+ )} +
+
+ {!sidePeekVisible && ( +
+ +
+ )} +
); }; diff --git a/packages/editor/document-editor/src/ui/index.tsx b/packages/editor/document-editor/src/ui/index.tsx index d1bdbc935..2491e04c7 100644 --- a/packages/editor/document-editor/src/ui/index.tsx +++ b/packages/editor/document-editor/src/ui/index.tsx @@ -10,6 +10,7 @@ import { DocumentDetails } from "src/types/editor-types"; import { PageRenderer } from "src/ui/components/page-renderer"; import { getMenuOptions } from "src/utils/menu-options"; import { useRouter } from "next/router"; +import { FixedMenu } from "src"; interface IDocumentEditor { // document info @@ -149,11 +150,14 @@ const DocumentEditor = ({ documentDetails={documentDetails} isSubmitting={isSubmitting} /> +
+ {uploadFile && } +
-
+
-
+
{ } return ( -
+
{basicMarkItems.map((item) => (
-
diff --git a/web/components/cycles/gantt-chart/blocks.tsx b/web/components/cycles/gantt-chart/blocks.tsx index 46bc04039..beb239d87 100644 --- a/web/components/cycles/gantt-chart/blocks.tsx +++ b/web/components/cycles/gantt-chart/blocks.tsx @@ -1,16 +1,30 @@ import { useRouter } from "next/router"; +import { observer } from "mobx-react"; +// hooks +import { useApplication, useCycle } from "hooks/store"; // ui import { Tooltip, ContrastIcon } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; -// types -import { ICycle } from "@plane/types"; -export const CycleGanttBlock = ({ data }: { data: ICycle }) => { +type Props = { + cycleId: string; +}; + +export const CycleGanttBlock: React.FC = observer((props) => { + const { cycleId } = props; + // router const router = useRouter(); - const { workspaceSlug } = router.query; + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { getCycleById } = useCycle(); + // derived values + const cycleDetails = getCycleById(cycleId); + + const cycleStatus = cycleDetails?.status.toLocaleLowerCase(); - const cycleStatus = data.status.toLocaleLowerCase(); return (
{ ? "rgb(var(--color-text-200))" : "", }} - onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/cycles/${data?.id}`)} + onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project}/cycles/${cycleDetails?.id}`)} >
-
{data?.name}
+
{cycleDetails?.name}
- {renderFormattedDate(data?.start_date ?? "")} to {renderFormattedDate(data?.end_date ?? "")} + {renderFormattedDate(cycleDetails?.start_date ?? "")} to{" "} + {renderFormattedDate(cycleDetails?.end_date ?? "")}
} position="top-left" > -
{data?.name}
+
{cycleDetails?.name}
); -}; +}); -export const CycleGanttSidebarBlock = ({ data }: { data: ICycle }) => { +export const CycleGanttSidebarBlock: React.FC = observer((props) => { + const { cycleId } = props; + // router const router = useRouter(); - const { workspaceSlug } = router.query; + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { getCycleById } = useCycle(); + // derived values + const cycleDetails = getCycleById(cycleId); - const cycleStatus = data.status.toLocaleLowerCase(); + const cycleStatus = cycleDetails?.status.toLocaleLowerCase(); return (
router.push(`/${workspaceSlug}/projects/${data?.project}/cycles/${data?.id}`)} + onClick={() => router.push(`/${workspaceSlug}/projects/${cycleDetails?.project}/cycles/${cycleDetails?.id}`)} > { : "" }`} /> -
{data?.name}
+
{cycleDetails?.name}
); -}; +}); diff --git a/web/components/cycles/gantt-chart/cycles-list-layout.tsx b/web/components/cycles/gantt-chart/cycles-list-layout.tsx index 797fc9e39..421a73a4a 100644 --- a/web/components/cycles/gantt-chart/cycles-list-layout.tsx +++ b/web/components/cycles/gantt-chart/cycles-list-layout.tsx @@ -63,7 +63,7 @@ export const CyclesListGanttChartView: FC = observer((props) => { blocks={cycleIds ? blockFormat(cycleIds.map((c) => getCycleById(c))) : null} blockUpdateHandler={(block, payload) => handleCycleUpdate(block, payload)} sidebarToRender={(props) => } - blockToRender={(data: ICycle) => } + blockToRender={(data: ICycle) => } enableBlockLeftResize={false} enableBlockRightResize={false} enableBlockMove={false} diff --git a/web/components/cycles/sidebar.tsx b/web/components/cycles/sidebar.tsx index 27182247b..8335baf06 100644 --- a/web/components/cycles/sidebar.tsx +++ b/web/components/cycles/sidebar.tsx @@ -100,7 +100,7 @@ export const CycleDetailsSidebar: React.FC = observer((props) => { }); }) - .catch((_) => { + .catch(() => { captureCycleEvent({ eventName: CYCLE_UPDATED, payload: { diff --git a/web/components/empty-state/empty-state.tsx b/web/components/empty-state/empty-state.tsx index adc11b546..4a5aeca02 100644 --- a/web/components/empty-state/empty-state.tsx +++ b/web/components/empty-state/empty-state.tsx @@ -65,14 +65,14 @@ export const EmptyState: React.FC = ({ ); return ( -
+
-
{emptyStateHeader}
+
{emptyStateHeader}
= (props) => { +export const GanttChartBlocksList: FC = observer((props) => { const { itemsContainerWidth, blocks, @@ -31,9 +34,10 @@ export const GanttChartBlocks: FC = (props) => { enableBlockMove, showAllBlocks, } = props; - - const { activeBlock, dispatch } = useChart(); + // store hooks const { peekIssue } = useIssueDetail(); + // chart hook + const { activeBlock, dispatch } = useChart(); // update the active block on hover const updateActiveBlock = (block: IGanttBlock | null) => { @@ -77,43 +81,51 @@ export const GanttChartBlocks: FC = (props) => { return (
- {blocks && - blocks.length > 0 && - blocks.map((block) => { - // hide the block if it doesn't have start and target dates and showAllBlocks is false - if (!showAllBlocks && !(block.start_date && block.target_date)) return; + {blocks?.map((block) => { + // hide the block if it doesn't have start and target dates and showAllBlocks is false + if (!showAllBlocks && !(block.start_date && block.target_date)) return; - const isBlockVisibleOnChart = block.start_date && block.target_date; + const isBlockVisibleOnChart = block.start_date && block.target_date; - return ( + return ( +
updateActiveBlock(block)} onMouseLeave={() => updateActiveBlock(null)} > - {!isBlockVisibleOnChart && } - handleChartBlockPosition(block, ...args)} - enableBlockLeftResize={enableBlockLeftResize} - enableBlockRightResize={enableBlockRightResize} - enableBlockMove={enableBlockMove} - /> + {isBlockVisibleOnChart ? ( + handleChartBlockPosition(block, ...args)} + enableBlockLeftResize={enableBlockLeftResize} + enableBlockRightResize={enableBlockRightResize} + enableBlockMove={enableBlockMove} + /> + ) : ( + + )}
- ); - })} +
+ ); + })}
); -}; +}); diff --git a/web/components/gantt-chart/blocks/index.ts b/web/components/gantt-chart/blocks/index.ts index 18ca5da9e..c99f8af32 100644 --- a/web/components/gantt-chart/blocks/index.ts +++ b/web/components/gantt-chart/blocks/index.ts @@ -1 +1 @@ -export * from "./blocks-display"; +export * from "./blocks-list"; diff --git a/web/components/gantt-chart/chart/header.tsx b/web/components/gantt-chart/chart/header.tsx new file mode 100644 index 000000000..6dcfdc36f --- /dev/null +++ b/web/components/gantt-chart/chart/header.tsx @@ -0,0 +1,59 @@ +import { Expand, Shrink } from "lucide-react"; +// hooks +import { useChart } from "../hooks"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { IGanttBlock, TGanttViews } from "../types"; + +type Props = { + blocks: IGanttBlock[] | null; + fullScreenMode: boolean; + handleChartView: (view: TGanttViews) => void; + handleToday: () => void; + loaderTitle: string; + title: string; + toggleFullScreenMode: () => void; +}; + +export const GanttChartHeader: React.FC = (props) => { + const { blocks, fullScreenMode, handleChartView, handleToday, loaderTitle, title, toggleFullScreenMode } = props; + // chart hook + const { currentView, allViews } = useChart(); + + return ( +
+
{title}
+
+
{blocks ? `${blocks.length} ${loaderTitle}` : "Loading..."}
+
+ +
+ {allViews?.map((chartView: any) => ( +
handleChartView(chartView?.key)} + > + {chartView?.title} +
+ ))} +
+ + + + +
+ ); +}; diff --git a/web/components/gantt-chart/chart/index.ts b/web/components/gantt-chart/chart/index.ts new file mode 100644 index 000000000..68b20b89a --- /dev/null +++ b/web/components/gantt-chart/chart/index.ts @@ -0,0 +1,4 @@ +export * from "./views"; +export * from "./header"; +export * from "./main-content"; +export * from "./root"; diff --git a/web/components/gantt-chart/chart/index.tsx b/web/components/gantt-chart/chart/index.tsx deleted file mode 100644 index 4592bfb5b..000000000 --- a/web/components/gantt-chart/chart/index.tsx +++ /dev/null @@ -1,324 +0,0 @@ -import { FC, useEffect, useState } from "react"; -// icons -// components -import { GanttChartBlocks } from "components/gantt-chart"; -// import { GanttSidebar } from "../sidebar"; -// import { HourChartView } from "./hours"; -// import { DayChartView } from "./day"; -// import { WeekChartView } from "./week"; -// import { BiWeekChartView } from "./bi-week"; -import { MonthChartView } from "./month"; -// import { QuarterChartView } from "./quarter"; -// import { YearChartView } from "./year"; -// icons -import { Expand, Shrink } from "lucide-react"; -// views -import { - // generateHourChart, - // generateDayChart, - // generateWeekChart, - // generateBiWeekChart, - generateMonthChart, - // generateQuarterChart, - // generateYearChart, - getNumberOfDaysBetweenTwoDatesInMonth, - // getNumberOfDaysBetweenTwoDatesInQuarter, - // getNumberOfDaysBetweenTwoDatesInYear, - getMonthChartItemPositionWidthInMonth, -} from "../views"; -// types -import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types"; -// data -import { currentViewDataWithView } from "../data"; -// context -import { useChart } from "../hooks"; - -type ChartViewRootProps = { - border: boolean; - title: string; - loaderTitle: string; - blocks: IGanttBlock[] | null; - blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; - blockToRender: (data: any) => React.ReactNode; - sidebarToRender: (props: any) => React.ReactNode; - enableBlockLeftResize: boolean; - enableBlockRightResize: boolean; - enableBlockMove: boolean; - enableReorder: boolean; - bottomSpacing: boolean; - showAllBlocks: boolean; -}; - -export const ChartViewRoot: FC = (props) => { - const { - border, - title, - blocks = null, - loaderTitle, - blockUpdateHandler, - sidebarToRender, - blockToRender, - enableBlockLeftResize, - enableBlockRightResize, - enableBlockMove, - enableReorder, - bottomSpacing, - showAllBlocks, - } = props; - // states - const [itemsContainerWidth, setItemsContainerWidth] = useState(0); - const [fullScreenMode, setFullScreenMode] = useState(false); - const [chartBlocks, setChartBlocks] = useState(null); // blocks state management starts - // hooks - const { currentView, currentViewData, renderView, dispatch, allViews, updateScrollLeft } = useChart(); - - const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) => - blocks && blocks.length > 0 - ? blocks.map((block: any) => ({ - ...block, - position: getMonthChartItemPositionWidthInMonth(view, block), - })) - : []; - - useEffect(() => { - if (currentViewData && blocks) setChartBlocks(() => renderBlockStructure(currentViewData, blocks)); - }, [currentViewData, blocks]); - - // blocks state management ends - - const handleChartView = (key: TGanttViews) => updateCurrentViewRenderPayload(null, key); - - const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => { - const selectedCurrentView: TGanttViews = view; - const selectedCurrentViewData: ChartDataType | undefined = - selectedCurrentView && selectedCurrentView === currentViewData?.key - ? currentViewData - : currentViewDataWithView(view); - - if (selectedCurrentViewData === undefined) return; - - let currentRender: any; - - // if (view === "hours") currentRender = generateHourChart(selectedCurrentViewData, side); - // if (view === "day") currentRender = generateDayChart(selectedCurrentViewData, side); - // if (view === "week") currentRender = generateWeekChart(selectedCurrentViewData, side); - // if (view === "bi_week") currentRender = generateBiWeekChart(selectedCurrentViewData, side); - if (selectedCurrentView === "month") currentRender = generateMonthChart(selectedCurrentViewData, side); - // if (view === "quarter") currentRender = generateQuarterChart(selectedCurrentViewData, side); - // if (selectedCurrentView === "year") - // currentRender = generateYearChart(selectedCurrentViewData, side); - - // updating the prevData, currentData and nextData - if (currentRender.payload.length > 0) { - if (side === "left") { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - currentView: selectedCurrentView, - currentViewData: currentRender.state, - renderView: [...currentRender.payload, ...renderView], - }, - }); - updatingCurrentLeftScrollPosition(currentRender.scrollWidth); - setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth); - } else if (side === "right") { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - currentView: view, - currentViewData: currentRender.state, - renderView: [...renderView, ...currentRender.payload], - }, - }); - setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth); - } else { - dispatch({ - type: "PARTIAL_UPDATE", - payload: { - currentView: view, - currentViewData: currentRender.state, - renderView: [...currentRender.payload], - }, - }); - setItemsContainerWidth(currentRender.scrollWidth); - setTimeout(() => { - handleScrollToCurrentSelectedDate(currentRender.state, currentRender.state.data.currentDate); - }, 50); - } - } - }; - - const handleToday = () => updateCurrentViewRenderPayload(null, currentView); - - // handling the scroll positioning from left and right - useEffect(() => { - handleToday(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const updatingCurrentLeftScrollPosition = (width: number) => { - const scrollContainer = document.getElementById("scroll-container") as HTMLElement; - - if (!scrollContainer) return; - - scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft; - setItemsContainerWidth(width + scrollContainer?.scrollLeft); - }; - - const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => { - const scrollContainer = document.getElementById("scroll-container") as HTMLElement; - - if (!scrollContainer) return; - - const clientVisibleWidth: number = scrollContainer?.clientWidth; - let scrollWidth: number = 0; - let daysDifference: number = 0; - - // if (currentView === "hours") - // daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date); - // if (currentView === "day") - // daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date); - // if (currentView === "week") - // daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date); - // if (currentView === "bi_week") - // daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date); - if (currentView === "month") - daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date); - // if (currentView === "quarter") - // daysDifference = getNumberOfDaysBetweenTwoDatesInQuarter(currentState.data.startDate, date); - // if (currentView === "year") - // daysDifference = getNumberOfDaysBetweenTwoDatesInYear(currentState.data.startDate, date); - - scrollWidth = daysDifference * currentState.data.width - (clientVisibleWidth / 2 - currentState.data.width); - - scrollContainer.scrollLeft = scrollWidth; - }; - - // handling scroll functionality - const onScroll = () => { - const scrollContainer = document.getElementById("scroll-container") as HTMLElement; - - if (!scrollContainer) return; - - const scrollWidth: number = scrollContainer?.scrollWidth; - const clientVisibleWidth: number = scrollContainer?.clientWidth; - const currentScrollPosition: number = scrollContainer?.scrollLeft; - - updateScrollLeft(currentScrollPosition); - - const approxRangeLeft: number = scrollWidth >= clientVisibleWidth + 1000 ? 1000 : scrollWidth - clientVisibleWidth; - const approxRangeRight: number = scrollWidth - (approxRangeLeft + clientVisibleWidth); - - if (currentScrollPosition >= approxRangeRight) updateCurrentViewRenderPayload("right", currentView); - if (currentScrollPosition <= approxRangeLeft) updateCurrentViewRenderPayload("left", currentView); - }; - - return ( -
- {/* chart header */} -
- {title && ( -
-
{title}
- {/*
- Gantt View Beta -
*/} -
- )} - -
- {blocks === null ? ( -
Loading...
- ) : ( -
- {blocks.length} {loaderTitle} -
- )} -
- -
- {allViews && - allViews.length > 0 && - // eslint-disable-next-line @typescript-eslint/no-unused-vars - allViews.map((_chatView: any, _idx: any) => ( -
handleChartView(_chatView?.key)} - > - {_chatView?.title} -
- ))} -
- -
-
- Today -
-
- -
setFullScreenMode((prevData) => !prevData)} - > - {fullScreenMode ? : } -
-
- - {/* content */} -
-
-
-
{title}
-
Duration
-
- - {sidebarToRender && sidebarToRender({ title, blockUpdateHandler, blocks, enableReorder })} -
-
- {/* {currentView && currentView === "hours" && } */} - {/* {currentView && currentView === "day" && } */} - {/* {currentView && currentView === "week" && } */} - {/* {currentView && currentView === "bi_week" && } */} - {currentView && currentView === "month" && } - {/* {currentView && currentView === "quarter" && } */} - {/* {currentView && currentView === "year" && } */} - - {/* blocks */} - {currentView && currentViewData && ( - - )} -
-
-
- ); -}; diff --git a/web/components/gantt-chart/chart/main-content.tsx b/web/components/gantt-chart/chart/main-content.tsx new file mode 100644 index 000000000..950f3550c --- /dev/null +++ b/web/components/gantt-chart/chart/main-content.tsx @@ -0,0 +1,120 @@ +// components +import { + BiWeekChartView, + DayChartView, + GanttChartBlocksList, + GanttChartSidebar, + HourChartView, + IBlockUpdateData, + IGanttBlock, + MonthChartView, + QuarterChartView, + TGanttViews, + WeekChartView, + YearChartView, + useChart, +} from "components/gantt-chart"; +// helpers +import { cn } from "helpers/common.helper"; + +type Props = { + blocks: IGanttBlock[] | null; + blockToRender: (data: any) => React.ReactNode; + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + bottomSpacing: boolean; + chartBlocks: IGanttBlock[] | null; + enableBlockLeftResize: boolean; + enableBlockMove: boolean; + enableBlockRightResize: boolean; + enableReorder: boolean; + itemsContainerWidth: number; + showAllBlocks: boolean; + sidebarToRender: (props: any) => React.ReactNode; + title: string; + updateCurrentViewRenderPayload: (direction: "left" | "right", currentView: TGanttViews) => void; +}; + +export const GanttChartMainContent: React.FC = (props) => { + const { + blocks, + blockToRender, + blockUpdateHandler, + bottomSpacing, + chartBlocks, + enableBlockLeftResize, + enableBlockMove, + enableBlockRightResize, + enableReorder, + itemsContainerWidth, + showAllBlocks, + sidebarToRender, + title, + updateCurrentViewRenderPayload, + } = props; + // chart hook + const { currentView, currentViewData, updateScrollLeft } = useChart(); + // handling scroll functionality + const onScroll = (e: React.UIEvent) => { + const { clientWidth, scrollLeft, scrollWidth } = e.currentTarget; + + updateScrollLeft(scrollLeft); + + const approxRangeLeft = scrollLeft >= clientWidth + 1000 ? 1000 : scrollLeft - clientWidth; + const approxRangeRight = scrollWidth - (scrollLeft + clientWidth); + + if (approxRangeRight < 1000) updateCurrentViewRenderPayload("right", currentView); + if (approxRangeLeft < 1000) updateCurrentViewRenderPayload("left", currentView); + }; + + const CHART_VIEW_COMPONENTS: { + [key in TGanttViews]: React.FC; + } = { + hours: HourChartView, + day: DayChartView, + week: WeekChartView, + bi_week: BiWeekChartView, + month: MonthChartView, + quarter: QuarterChartView, + year: YearChartView, + }; + + if (!currentView) return null; + const ActiveChartView = CHART_VIEW_COMPONENTS[currentView]; + + return ( +
+ +
+ + {currentViewData && ( + + )} +
+
+ ); +}; diff --git a/web/components/gantt-chart/chart/month.tsx b/web/components/gantt-chart/chart/month.tsx deleted file mode 100644 index 0b7a4c452..000000000 --- a/web/components/gantt-chart/chart/month.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { FC } from "react"; -// hooks -import { useChart } from "../hooks"; -// types -import { IMonthBlock } from "../views"; - -export const MonthChartView: FC = () => { - const { currentViewData, renderView } = useChart(); - - const monthBlocks: IMonthBlock[] = renderView; - - return ( - <> -
- {monthBlocks && - monthBlocks.length > 0 && - monthBlocks.map((block, _idxRoot) => ( -
-
-
-
- {block?.title} -
-
- -
- {block?.children && - block?.children.length > 0 && - block?.children.map((monthDay, _idx) => ( -
-
- {monthDay.dayData.shortTitle[0]}{" "} - - {monthDay.day} - -
-
- ))} -
-
- -
- {block?.children && - block?.children.length > 0 && - block?.children.map((monthDay, _idx) => ( -
-
- {/* {monthDay?.today && ( -
- )} */} -
-
- ))} -
-
- ))} -
- - ); -}; diff --git a/web/components/gantt-chart/chart/root.tsx b/web/components/gantt-chart/chart/root.tsx new file mode 100644 index 000000000..961cb2fee --- /dev/null +++ b/web/components/gantt-chart/chart/root.tsx @@ -0,0 +1,203 @@ +import { FC, useEffect, useState } from "react"; +// components +import { GanttChartHeader, useChart, GanttChartMainContent } from "components/gantt-chart"; +// views +import { + generateMonthChart, + getNumberOfDaysBetweenTwoDatesInMonth, + getMonthChartItemPositionWidthInMonth, +} from "../views"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { ChartDataType, IBlockUpdateData, IGanttBlock, TGanttViews } from "../types"; +// data +import { currentViewDataWithView } from "../data"; +// constants +import { SIDEBAR_WIDTH } from "../constants"; + +type ChartViewRootProps = { + border: boolean; + title: string; + loaderTitle: string; + blocks: IGanttBlock[] | null; + blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; + blockToRender: (data: any) => React.ReactNode; + sidebarToRender: (props: any) => React.ReactNode; + enableBlockLeftResize: boolean; + enableBlockRightResize: boolean; + enableBlockMove: boolean; + enableReorder: boolean; + bottomSpacing: boolean; + showAllBlocks: boolean; +}; + +export const ChartViewRoot: FC = (props) => { + const { + border, + title, + blocks = null, + loaderTitle, + blockUpdateHandler, + sidebarToRender, + blockToRender, + enableBlockLeftResize, + enableBlockRightResize, + enableBlockMove, + enableReorder, + bottomSpacing, + showAllBlocks, + } = props; + // states + const [itemsContainerWidth, setItemsContainerWidth] = useState(0); + const [fullScreenMode, setFullScreenMode] = useState(false); + const [chartBlocks, setChartBlocks] = useState(null); + // hooks + const { currentView, currentViewData, renderView, dispatch } = useChart(); + + // rendering the block structure + const renderBlockStructure = (view: any, blocks: IGanttBlock[] | null) => + blocks + ? blocks.map((block: any) => ({ + ...block, + position: getMonthChartItemPositionWidthInMonth(view, block), + })) + : []; + + useEffect(() => { + if (!currentViewData || !blocks) return; + setChartBlocks(() => renderBlockStructure(currentViewData, blocks)); + }, [currentViewData, blocks]); + + const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => { + const selectedCurrentView: TGanttViews = view; + const selectedCurrentViewData: ChartDataType | undefined = + selectedCurrentView && selectedCurrentView === currentViewData?.key + ? currentViewData + : currentViewDataWithView(view); + + if (selectedCurrentViewData === undefined) return; + + let currentRender: any; + if (selectedCurrentView === "month") currentRender = generateMonthChart(selectedCurrentViewData, side); + + // updating the prevData, currentData and nextData + if (currentRender.payload.length > 0) { + if (side === "left") { + dispatch({ + type: "PARTIAL_UPDATE", + payload: { + currentView: selectedCurrentView, + currentViewData: currentRender.state, + renderView: [...currentRender.payload, ...renderView], + }, + }); + updatingCurrentLeftScrollPosition(currentRender.scrollWidth); + setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth); + } else if (side === "right") { + dispatch({ + type: "PARTIAL_UPDATE", + payload: { + currentView: view, + currentViewData: currentRender.state, + renderView: [...renderView, ...currentRender.payload], + }, + }); + setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth); + } else { + dispatch({ + type: "PARTIAL_UPDATE", + payload: { + currentView: view, + currentViewData: currentRender.state, + renderView: [...currentRender.payload], + }, + }); + setItemsContainerWidth(currentRender.scrollWidth); + setTimeout(() => { + handleScrollToCurrentSelectedDate(currentRender.state, currentRender.state.data.currentDate); + }, 50); + } + } + }; + + const handleToday = () => updateCurrentViewRenderPayload(null, currentView); + + // handling the scroll positioning from left and right + useEffect(() => { + handleToday(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const updatingCurrentLeftScrollPosition = (width: number) => { + const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement; + if (!scrollContainer) return; + + scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft; + setItemsContainerWidth(width + scrollContainer?.scrollLeft); + }; + + const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => { + const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement; + if (!scrollContainer) return; + + const clientVisibleWidth: number = scrollContainer?.clientWidth; + let scrollWidth: number = 0; + let daysDifference: number = 0; + + // if (currentView === "hours") + // daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date); + // if (currentView === "day") + // daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date); + // if (currentView === "week") + // daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date); + // if (currentView === "bi_week") + // daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date); + if (currentView === "month") + daysDifference = getNumberOfDaysBetweenTwoDatesInMonth(currentState.data.startDate, date); + // if (currentView === "quarter") + // daysDifference = getNumberOfDaysBetweenTwoDatesInQuarter(currentState.data.startDate, date); + // if (currentView === "year") + // daysDifference = getNumberOfDaysBetweenTwoDatesInYear(currentState.data.startDate, date); + + scrollWidth = + daysDifference * currentState.data.width - (clientVisibleWidth / 2 - currentState.data.width) + SIDEBAR_WIDTH / 2; + + scrollContainer.scrollLeft = scrollWidth; + }; + + return ( +
+ setFullScreenMode((prevData) => !prevData)} + handleChartView={(key) => updateCurrentViewRenderPayload(null, key)} + handleToday={handleToday} + loaderTitle={loaderTitle} + title={title} + /> + +
+ ); +}; diff --git a/web/components/gantt-chart/chart/bi-week.tsx b/web/components/gantt-chart/chart/views/bi-week.tsx similarity index 97% rename from web/components/gantt-chart/chart/bi-week.tsx rename to web/components/gantt-chart/chart/views/bi-week.tsx index f4a6080cd..6e53d5390 100644 --- a/web/components/gantt-chart/chart/bi-week.tsx +++ b/web/components/gantt-chart/chart/views/bi-week.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; // context -import { useChart } from "../hooks"; +import { useChart } from "components/gantt-chart"; export const BiWeekChartView: FC = () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/web/components/gantt-chart/chart/day.tsx b/web/components/gantt-chart/chart/views/day.tsx similarity index 98% rename from web/components/gantt-chart/chart/day.tsx rename to web/components/gantt-chart/chart/views/day.tsx index 32b3caca0..a50b7748a 100644 --- a/web/components/gantt-chart/chart/day.tsx +++ b/web/components/gantt-chart/chart/views/day.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; // context -import { useChart } from "../hooks"; +import { useChart } from "../../hooks"; export const DayChartView: FC = () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/web/components/gantt-chart/chart/hours.tsx b/web/components/gantt-chart/chart/views/hours.tsx similarity index 97% rename from web/components/gantt-chart/chart/hours.tsx rename to web/components/gantt-chart/chart/views/hours.tsx index 5693b38b8..e1fd02e3f 100644 --- a/web/components/gantt-chart/chart/hours.tsx +++ b/web/components/gantt-chart/chart/views/hours.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; // context -import { useChart } from "../hooks"; +import { useChart } from "components/gantt-chart"; export const HourChartView: FC = () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/web/components/gantt-chart/chart/views/index.ts b/web/components/gantt-chart/chart/views/index.ts new file mode 100644 index 000000000..8936623c2 --- /dev/null +++ b/web/components/gantt-chart/chart/views/index.ts @@ -0,0 +1,7 @@ +export * from "./bi-week"; +export * from "./day"; +export * from "./hours"; +export * from "./month"; +export * from "./quarter"; +export * from "./week"; +export * from "./year"; diff --git a/web/components/gantt-chart/chart/views/month.tsx b/web/components/gantt-chart/chart/views/month.tsx new file mode 100644 index 000000000..13f60c2b4 --- /dev/null +++ b/web/components/gantt-chart/chart/views/month.tsx @@ -0,0 +1,76 @@ +import { FC } from "react"; +// hooks +import { useChart } from "components/gantt-chart"; +// helpers +import { cn } from "helpers/common.helper"; +// types +import { IMonthBlock } from "../../views"; +// constants +import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "components/gantt-chart/constants"; + +export const MonthChartView: FC = () => { + // chart hook + const { currentViewData, renderView } = useChart(); + const monthBlocks: IMonthBlock[] = renderView; + + return ( + <> +
+ {monthBlocks?.map((block, rootIndex) => ( +
+
+
+
+ {block?.title} +
+
+
+ {block?.children?.map((monthDay, index) => ( +
+
+ {monthDay.dayData.shortTitle[0]}{" "} + + {monthDay.day} + +
+
+ ))} +
+
+
+ {block?.children?.map((monthDay, index) => ( +
+ {["sat", "sun"].includes(monthDay?.dayData?.shortTitle) && ( +
+ )} +
+ ))} +
+
+ ))} +
+ + ); +}; diff --git a/web/components/gantt-chart/chart/quarter.tsx b/web/components/gantt-chart/chart/views/quarter.tsx similarity index 98% rename from web/components/gantt-chart/chart/quarter.tsx rename to web/components/gantt-chart/chart/views/quarter.tsx index a15f6f34d..ffbc1cbfe 100644 --- a/web/components/gantt-chart/chart/quarter.tsx +++ b/web/components/gantt-chart/chart/views/quarter.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; // context -import { useChart } from "../hooks"; +import { useChart } from "../../hooks"; export const QuarterChartView: FC = () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/web/components/gantt-chart/chart/week.tsx b/web/components/gantt-chart/chart/views/week.tsx similarity index 98% rename from web/components/gantt-chart/chart/week.tsx rename to web/components/gantt-chart/chart/views/week.tsx index b90caf8b7..8170affa4 100644 --- a/web/components/gantt-chart/chart/week.tsx +++ b/web/components/gantt-chart/chart/views/week.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; // context -import { useChart } from "../hooks"; +import { useChart } from "../../hooks"; export const WeekChartView: FC = () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/web/components/gantt-chart/chart/year.tsx b/web/components/gantt-chart/chart/views/year.tsx similarity index 98% rename from web/components/gantt-chart/chart/year.tsx rename to web/components/gantt-chart/chart/views/year.tsx index 7c3a34b53..9dbeedece 100644 --- a/web/components/gantt-chart/chart/year.tsx +++ b/web/components/gantt-chart/chart/views/year.tsx @@ -1,6 +1,6 @@ import { FC } from "react"; // context -import { useChart } from "../hooks"; +import { useChart } from "../../hooks"; export const YearChartView: FC = () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/web/components/gantt-chart/constants.ts b/web/components/gantt-chart/constants.ts new file mode 100644 index 000000000..958985cf1 --- /dev/null +++ b/web/components/gantt-chart/constants.ts @@ -0,0 +1,5 @@ +export const BLOCK_HEIGHT = 44; + +export const HEADER_HEIGHT = 60; + +export const SIDEBAR_WIDTH = 360; diff --git a/web/components/gantt-chart/contexts/index.tsx b/web/components/gantt-chart/contexts/index.tsx index 137cc2607..84e7a19b5 100644 --- a/web/components/gantt-chart/contexts/index.tsx +++ b/web/components/gantt-chart/contexts/index.tsx @@ -24,6 +24,7 @@ const chartReducer = (state: ChartContextData, action: ChartContextActionPayload const initialView = "month"; export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + // states; const [state, dispatch] = useState({ currentView: initialView, currentViewData: currentViewDataWithView(initialView), @@ -31,23 +32,25 @@ export const ChartContextProvider: React.FC<{ children: React.ReactNode }> = ({ allViews: allViewsWithData, activeBlock: null, }); - const [scrollLeft, setScrollLeft] = useState(0); const handleDispatch = (action: ChartContextActionPayload): ChartContextData => { const newState = chartReducer(state, action); - dispatch(() => newState); - return newState; }; - const updateScrollLeft = (scrollLeft: number) => { - setScrollLeft(scrollLeft); - }; + const updateScrollLeft = (scrollLeft: number) => setScrollLeft(scrollLeft); return ( - + {children} ); diff --git a/web/components/gantt-chart/helpers/block-structure.tsx b/web/components/gantt-chart/helpers/block-structure.ts similarity index 100% rename from web/components/gantt-chart/helpers/block-structure.tsx rename to web/components/gantt-chart/helpers/block-structure.ts diff --git a/web/components/gantt-chart/helpers/draggable.tsx b/web/components/gantt-chart/helpers/draggable.tsx index d2c4448bb..ac1602346 100644 --- a/web/components/gantt-chart/helpers/draggable.tsx +++ b/web/components/gantt-chart/helpers/draggable.tsx @@ -1,9 +1,11 @@ import React, { useEffect, useRef, useState } from "react"; -import { ArrowLeft, ArrowRight } from "lucide-react"; +import { ArrowRight } from "lucide-react"; // hooks -import { useChart } from "../hooks"; -// types -import { IGanttBlock } from "../types"; +import { IGanttBlock, useChart } from "components/gantt-chart"; +// helpers +import { cn } from "helpers/common.helper"; +// constants +import { SIDEBAR_WIDTH } from "../constants"; type Props = { block: IGanttBlock; @@ -20,7 +22,7 @@ export const ChartDraggable: React.FC = (props) => { const [isLeftResizing, setIsLeftResizing] = useState(false); const [isRightResizing, setIsRightResizing] = useState(false); const [isMoving, setIsMoving] = useState(false); - const [posFromLeft, setPosFromLeft] = useState(null); + const [isHidden, setIsHidden] = useState(true); // refs const resizableRef = useRef(null); // chart hook @@ -31,12 +33,10 @@ export const ChartDraggable: React.FC = (props) => { let delWidth = 0; - const ganttContainer = document.querySelector("#gantt-container") as HTMLElement; - const ganttSidebar = document.querySelector("#gantt-sidebar") as HTMLElement; + const ganttContainer = document.querySelector("#gantt-container") as HTMLDivElement; + const ganttSidebar = document.querySelector("#gantt-sidebar") as HTMLDivElement; - const scrollContainer = document.querySelector("#scroll-container") as HTMLElement; - - if (!ganttContainer || !ganttSidebar || !scrollContainer) return 0; + if (!ganttContainer || !ganttSidebar) return 0; const posFromLeft = e.clientX; // manually scroll to left if reached the left end while dragging @@ -45,7 +45,7 @@ export const ChartDraggable: React.FC = (props) => { delWidth = -5; - scrollContainer.scrollBy(delWidth, 0); + ganttContainer.scrollBy(delWidth, 0); } else delWidth = e.movementX; // manually scroll to right if reached the right end while dragging @@ -55,7 +55,7 @@ export const ChartDraggable: React.FC = (props) => { delWidth = 5; - scrollContainer.scrollBy(delWidth, 0); + ganttContainer.scrollBy(delWidth, 0); } else delWidth = e.movementX; return delWidth; @@ -201,50 +201,61 @@ export const ChartDraggable: React.FC = (props) => { }; // scroll to a hidden block const handleScrollToBlock = () => { - const scrollContainer = document.querySelector("#scroll-container") as HTMLElement; - + const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement; if (!scrollContainer || !block.position) return; - // update container's scroll position to the block's position scrollContainer.scrollLeft = block.position.marginLeft - 4; }; - // update block position from viewport's left end on scroll - useEffect(() => { - const block = resizableRef.current; - - if (!block) return; - - setPosFromLeft(block.getBoundingClientRect().left); - }, [scrollLeft]); // check if block is hidden on either side const isBlockHiddenOnLeft = block.position?.marginLeft && block.position?.width && scrollLeft > block.position.marginLeft + block.position.width; - const isBlockHiddenOnRight = posFromLeft && window && posFromLeft > window.innerWidth; + + useEffect(() => { + const intersectionRoot = document.querySelector("#gantt-container") as HTMLDivElement; + const resizableBlock = resizableRef.current; + if (!resizableBlock || !intersectionRoot) return; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + setIsHidden(!entry.isIntersecting); + }); + }, + { + root: intersectionRoot, + rootMargin: `0px 0px 0px -${SIDEBAR_WIDTH}px`, + } + ); + + observer.observe(resizableBlock); + + return () => { + observer.unobserve(resizableBlock); + }; + }, [block.data.name]); return ( <> - {/* move to left side hidden block button */} - {isBlockHiddenOnLeft && ( - - )} - {/* move to right side hidden block button */} - {isBlockHiddenOnRight && ( -
- -
+ + )}
= (props) => { onMouseDown={handleBlockLeftResize} onMouseEnter={() => setIsLeftResizing(true)} onMouseLeave={() => setIsLeftResizing(false)} - className="absolute -left-2.5 top-1/2 z-[3] h-full w-6 -translate-y-1/2 cursor-col-resize rounded-md" + className="absolute -left-2.5 top-1/2 -translate-y-1/2 z-[3] h-full w-6 cursor-col-resize rounded-md" />
)}
{blockToRender(block.data)} @@ -281,12 +297,15 @@ export const ChartDraggable: React.FC = (props) => { onMouseDown={handleBlockRightResize} onMouseEnter={() => setIsRightResizing(true)} onMouseLeave={() => setIsRightResizing(false)} - className="absolute -right-2.5 top-1/2 z-[2] h-full w-6 -translate-y-1/2 cursor-col-resize rounded-md" + className="absolute -right-2.5 top-1/2 -translate-y-1/2 z-[2] h-full w-6 cursor-col-resize rounded-md" />
)} diff --git a/web/components/gantt-chart/index.ts b/web/components/gantt-chart/index.ts index ead696086..54a2cc597 100644 --- a/web/components/gantt-chart/index.ts +++ b/web/components/gantt-chart/index.ts @@ -1,4 +1,5 @@ export * from "./blocks"; +export * from "./chart"; export * from "./helpers"; export * from "./hooks"; export * from "./root"; diff --git a/web/components/gantt-chart/root.tsx b/web/components/gantt-chart/root.tsx index 7673da88e..c8576436e 100644 --- a/web/components/gantt-chart/root.tsx +++ b/web/components/gantt-chart/root.tsx @@ -1,10 +1,8 @@ import { FC } from "react"; // components -import { ChartViewRoot } from "./chart"; +import { ChartViewRoot, IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; // context import { ChartContextProvider } from "./contexts"; -// types -import { IBlockUpdateData, IGanttBlock } from "./types"; type GanttChartRootProps = { border?: boolean; diff --git a/web/components/gantt-chart/sidebar/cycle-sidebar.tsx b/web/components/gantt-chart/sidebar/cycles.tsx similarity index 85% rename from web/components/gantt-chart/sidebar/cycle-sidebar.tsx rename to web/components/gantt-chart/sidebar/cycles.tsx index dddccda5a..384869a40 100644 --- a/web/components/gantt-chart/sidebar/cycle-sidebar.tsx +++ b/web/components/gantt-chart/sidebar/cycles.tsx @@ -1,4 +1,3 @@ -import { useRouter } from "next/router"; import { DragDropContext, Draggable, DropResult, Droppable } from "@hello-pangea/dnd"; import { MoreVertical } from "lucide-react"; // hooks @@ -9,8 +8,11 @@ import { Loader } from "@plane/ui"; import { CycleGanttSidebarBlock } from "components/cycles"; // helpers import { findTotalDaysInRange } from "helpers/date-time.helper"; +import { cn } from "helpers/common.helper"; // types import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart/types"; +// constants +import { BLOCK_HEIGHT } from "../constants"; type Props = { title: string; @@ -20,12 +22,8 @@ type Props = { }; export const CycleGanttSidebar: React.FC = (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { title, blockUpdateHandler, blocks, enableReorder } = props; - - const router = useRouter(); - const { cycleId } = router.query; - + const { blockUpdateHandler, blocks, enableReorder } = props; + // chart hook const { activeBlock, dispatch } = useChart(); // update the active block on hover @@ -84,12 +82,7 @@ export const CycleGanttSidebar: React.FC = (props) => { {(droppableProvided) => ( -
+
<> {blocks ? ( blocks.map((block, index) => { @@ -104,7 +97,9 @@ export const CycleGanttSidebar: React.FC = (props) => { > {(provided, snapshot) => (
updateActiveBlock(block)} onMouseLeave={() => updateActiveBlock(null)} ref={provided.innerRef} @@ -112,9 +107,12 @@ export const CycleGanttSidebar: React.FC = (props) => { >
{enableReorder && ( + )} +
+
+ +
+ {duration && ( +
+ + {duration} day{duration > 1 ? "s" : ""} + +
+ )} +
+
+
+ )} + + ); + }) + ) : ( + + + + + + + )} + {droppableProvided.placeholder} + +
+ )} + + + {enableQuickIssueCreate && !disableIssueCreation && ( + + )} + + ); +}); diff --git a/web/components/gantt-chart/sidebar/module-sidebar.tsx b/web/components/gantt-chart/sidebar/modules.tsx similarity index 86% rename from web/components/gantt-chart/sidebar/module-sidebar.tsx rename to web/components/gantt-chart/sidebar/modules.tsx index 8f8788787..bdf8ca571 100644 --- a/web/components/gantt-chart/sidebar/module-sidebar.tsx +++ b/web/components/gantt-chart/sidebar/modules.tsx @@ -1,4 +1,3 @@ -import { useRouter } from "next/router"; import { DragDropContext, Draggable, Droppable, DropResult } from "@hello-pangea/dnd"; import { MoreVertical } from "lucide-react"; // hooks @@ -9,8 +8,11 @@ import { Loader } from "@plane/ui"; import { ModuleGanttSidebarBlock } from "components/modules"; // helpers import { findTotalDaysInRange } from "helpers/date-time.helper"; +import { cn } from "helpers/common.helper"; // types import { IBlockUpdateData, IGanttBlock } from "components/gantt-chart"; +// constants +import { BLOCK_HEIGHT } from "../constants"; type Props = { title: string; @@ -20,12 +22,8 @@ type Props = { }; export const ModuleGanttSidebar: React.FC = (props) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { title, blockUpdateHandler, blocks, enableReorder } = props; - - const router = useRouter(); - const { cycleId } = router.query; - + const { blockUpdateHandler, blocks, enableReorder } = props; + // chart hook const { activeBlock, dispatch } = useChart(); // update the active block on hover @@ -84,12 +82,7 @@ export const ModuleGanttSidebar: React.FC = (props) => { {(droppableProvided) => ( -
+
<> {blocks ? ( blocks.map((block, index) => { @@ -104,7 +97,9 @@ export const ModuleGanttSidebar: React.FC = (props) => { > {(provided, snapshot) => (
updateActiveBlock(block)} onMouseLeave={() => updateActiveBlock(null)} ref={provided.innerRef} @@ -112,9 +107,12 @@ export const ModuleGanttSidebar: React.FC = (props) => { >
{enableReorder && ( - )} -
-
- -
- {duration !== undefined && ( -
- - {duration} day{duration > 1 ? "s" : ""} - -
- )} -
-
-
- )} - - ); - }) - ) : ( - - - - - - - )} - {droppableProvided.placeholder} - - {enableQuickIssueCreate && !disableIssueCreation && ( - - )} -
- )} - - - ); -}; diff --git a/web/components/headers/cycle-issues.tsx b/web/components/headers/cycle-issues.tsx index 44f4f944d..05030c500 100644 --- a/web/components/headers/cycle-issues.tsx +++ b/web/components/headers/cycle-issues.tsx @@ -153,7 +153,7 @@ export const CycleIssuesHeader: React.FC = observer(() => {
- + { label={ <> - {cycleDetails?.name && truncateText(cycleDetails.name, 40)} +
+ {cycleDetails?.name && cycleDetails.name} +
} className="ml-1.5 flex-shrink-0" diff --git a/web/components/headers/cycles.tsx b/web/components/headers/cycles.tsx index a4bf963ab..496fabecd 100644 --- a/web/components/headers/cycles.tsx +++ b/web/components/headers/cycles.tsx @@ -50,7 +50,7 @@ export const CyclesHeader: FC = observer(() => {
- + { toggleCreateCycleModal(true); }} > - Add Cycle +
Add
Cycle
)} diff --git a/web/components/headers/module-issues.tsx b/web/components/headers/module-issues.tsx index 6287223b0..d51c0f432 100644 --- a/web/components/headers/module-issues.tsx +++ b/web/components/headers/module-issues.tsx @@ -21,7 +21,7 @@ import { ProjectAnalyticsModal } from "components/analytics"; import { SidebarHamburgerToggle } from "components/core/sidebar/sidebar-menu-hamburger-toggle"; import { BreadcrumbLink } from "components/common"; // ui -import { Breadcrumbs, Button, CustomMenu, DiceIcon } from "@plane/ui"; +import { Breadcrumbs, Button, CustomMenu, DiceIcon, LayersIcon } from "@plane/ui"; // icons import { ArrowRight, PanelRight, Plus } from "lucide-react"; // helpers @@ -156,7 +156,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => {
- + { label={ <> - {moduleDetails?.name && truncateText(moduleDetails.name, 40)} +
+ {moduleDetails?.name && moduleDetails.name} +
} className="ml-1.5 flex-shrink-0" @@ -251,6 +253,7 @@ export const ModuleIssuesHeader: React.FC = observer(() => { Analytics )} diff --git a/web/components/headers/modules-list.tsx b/web/components/headers/modules-list.tsx index 91e05db46..9ad34678a 100644 --- a/web/components/headers/modules-list.tsx +++ b/web/components/headers/modules-list.tsx @@ -1,11 +1,11 @@ import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { Plus } from "lucide-react"; +import { GanttChartSquare, LayoutGrid, List, Plus } from "lucide-react"; // hooks import { useApplication, useEventTracker, useProject, useUser } from "hooks/store"; import useLocalStorage from "hooks/use-local-storage"; // ui -import { Breadcrumbs, Button, Tooltip, DiceIcon } from "@plane/ui"; +import { Breadcrumbs, Button, Tooltip, DiceIcon, CustomMenu } from "@plane/ui"; // helper import { renderEmoji } from "helpers/emoji.helper"; // constants @@ -31,75 +31,101 @@ export const ModulesListHeader: React.FC = observer(() => { const canUserCreateModule = currentProjectRole && [EUserProjectRoles.ADMIN, EUserProjectRoles.MEMBER].includes(currentProjectRole); - return ( -
-
- -
- - - {currentProjectDetails?.name.charAt(0)} - - ) - } - /> - } - /> - } />} - /> - +
+
+
+ +
+ + + {currentProjectDetails?.name.charAt(0)} + + ) + } + /> + } + /> + } />} + /> + +
+
+
+
+ {MODULE_VIEW_LAYOUTS.map((layout) => ( + + + + ))} +
+ {canUserCreateModule && ( + + )}
-
-
+
+ + {modulesView === 'gantt_chart' ? : modulesView === 'grid' ? : } + Layout + + } + customButtonClassName="flex flex-grow justify-center items-center text-custom-text-200 text-sm" + closeOnSelect + > {MODULE_VIEW_LAYOUTS.map((layout) => ( - - - + setModulesView(layout.key)} + className="flex items-center gap-2" + > + +
{layout.title}
+
))} -
- {canUserCreateModule && ( - - )} +
); }); + + diff --git a/web/components/headers/page-details.tsx b/web/components/headers/page-details.tsx index 83595af60..e2a427db7 100644 --- a/web/components/headers/page-details.tsx +++ b/web/components/headers/page-details.tsx @@ -36,23 +36,34 @@ export const PageDetailsHeader: FC = observer((props) => { - {currentProjectDetails?.name.charAt(0)} - - ) - } - /> + + + + {currentProjectDetails?.name.charAt(0)} + + ) + } + /> + + + + + } /> + { const { workspaceSlug, projectId, issueId } = router.query; // store hooks const { currentProjectDetails, getProjectById } = useProject(); + const { theme: themeStore } = useApplication(); const { data: issueDetails } = useSWR( workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null, @@ -33,12 +36,14 @@ export const ProjectIssueDetailsHeader: FC = observer(() => { : null ); + const isSidebarCollapsed = themeStore.issueDetailSidebarCollapsed; + return (
- + {
+
); }); diff --git a/web/components/headers/project-issues.tsx b/web/components/headers/project-issues.tsx index 81e2d2d76..5c44a84d6 100644 --- a/web/components/headers/project-issues.tsx +++ b/web/components/headers/project-issues.tsx @@ -2,7 +2,7 @@ import { useCallback, useState } from "react"; import Link from "next/link"; import { useRouter } from "next/router"; import { observer } from "mobx-react-lite"; -import { Briefcase, Circle, ExternalLink, Plus } from "lucide-react"; +import { Briefcase, Circle, ExternalLink, Plus, Inbox } from "lucide-react"; // hooks import { useApplication, @@ -120,35 +120,35 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
- + router.back()}> - {renderEmoji(currentProjectDetails.emoji)} - - ) : currentProjectDetails?.icon_prop ? ( -
- {renderEmoji(currentProjectDetails.icon_prop)} -
- ) : ( - - {currentProjectDetails?.name.charAt(0)} - - ) - ) : ( + - + {renderEmoji(currentProjectDetails.emoji)} + + ) : currentProjectDetails?.icon_prop ? ( +
+ {renderEmoji(currentProjectDetails.icon_prop)} +
+ ) : ( + + {currentProjectDetails?.name.charAt(0)} ) - } - /> + ) : ( + + + + ) + } + /> } /> @@ -202,18 +202,19 @@ export const ProjectIssuesHeader: React.FC = observer(() => {
{currentProjectDetails?.inbox_view && inboxDetails && ( - - - - - + + + + + + )} {canUserCreateIssue && ( <> @@ -228,7 +229,7 @@ export const ProjectIssuesHeader: React.FC = observer(() => { size="sm" prependIcon={} > - Add Issue +
Add
Issue )} diff --git a/web/components/headers/project-settings.tsx b/web/components/headers/project-settings.tsx index fdb033a21..b70a4614f 100644 --- a/web/components/headers/project-settings.tsx +++ b/web/components/headers/project-settings.tsx @@ -36,7 +36,7 @@ export const ProjectSettingHeader: FC = observer((props)
- + { }} className="items-center" > - Add Project +
Add
Project )}
diff --git a/web/components/issues/description-form.tsx b/web/components/issues/description-form.tsx index ca6d7e0e7..b7601ef52 100644 --- a/web/components/issues/description-form.tsx +++ b/web/components/issues/description-form.tsx @@ -4,7 +4,7 @@ import { Controller, useForm } from "react-hook-form"; import useReloadConfirmations from "hooks/use-reload-confirmation"; import debounce from "lodash/debounce"; // components -import { TextArea } from "@plane/ui"; +import { Loader, TextArea } from "@plane/ui"; import { RichReadOnlyEditor, RichTextEditor } from "@plane/rich-text-editor"; // types import { TIssue } from "@plane/types"; @@ -12,6 +12,8 @@ import { TIssueOperations } from "./issue-detail"; // services import { FileService } from "services/file.service"; import { useMention, useWorkspace } from "hooks/store"; +import { observer } from "mobx-react"; +import { isNil } from "lodash"; export interface IssueDescriptionFormValues { name: string; @@ -36,7 +38,7 @@ export interface IssueDetailsProps { const fileService = new FileService(); -export const IssueDescriptionForm: FC = (props) => { +export const IssueDescriptionForm: FC = observer((props) => { const { workspaceSlug, projectId, issueId, issue, issueOperations, disabled, isSubmitting, setIsSubmitting } = props; const workspaceStore = useWorkspace(); const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug)?.id as string; @@ -71,12 +73,20 @@ export const IssueDescriptionForm: FC = (props) => { // editor rerendering on every save useEffect(() => { if (issue.id) { - setLocalIssueDescription({ id: issue.id, description_html: issue.description_html }); setLocalTitleValue(issue.name); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [issue.id]); // TODO: verify the exhaustive-deps warning + useEffect(() => { + if (issue.description_html) { + setLocalIssueDescription((state) => { + if (!isNil(state.description_html)) return state; + return { id: issue.id, description_html: issue.description_html }; + }); + } + }, [issue.description_html]); + const handleDescriptionFormSubmit = useCallback( async (formData: Partial) => { if (!formData?.name || formData?.name.length === 0 || formData?.name.length > 255) return; @@ -167,42 +177,48 @@ export const IssueDescriptionForm: FC = (props) => {
{errors.name ? errors.name.message : null}
- - !disabled ? ( - { - setShowAlert(true); - setIsSubmitting("submitting"); - onChange(description_html); - debouncedFormSave(); - }} - mentionSuggestions={mentionSuggestions} - mentionHighlights={mentionHighlights} - /> - ) : ( - - ) - } - /> + {issue.description_html ? ( + + !disabled ? ( + { + setShowAlert(true); + setIsSubmitting("submitting"); + onChange(description_html); + debouncedFormSave(); + }} + mentionSuggestions={mentionSuggestions} + mentionHighlights={mentionHighlights} + /> + ) : ( + + ) + } + /> + ) : ( + + + + )}
); -}; +}); diff --git a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx index eabe5d518..c7b75340b 100644 --- a/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx +++ b/web/components/issues/issue-detail/issue-activity/activity/actions/helpers/activity-block.tsx @@ -33,7 +33,7 @@ export const IssueActivityBlockComponent: FC = (pr }`} >
-
+
{icon ? icon : }
diff --git a/web/components/issues/issue-detail/root.tsx b/web/components/issues/issue-detail/root.tsx index 92badf4b2..1fab25d96 100644 --- a/web/components/issues/issue-detail/root.tsx +++ b/web/components/issues/issue-detail/root.tsx @@ -9,7 +9,7 @@ import { EmptyState } from "components/common"; // images import emptyIssue from "public/empty-state/issue.svg"; // hooks -import { useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; +import { useApplication, useEventTracker, useIssueDetail, useIssues, useUser } from "hooks/store"; import useToast from "hooks/use-toast"; // types import { TIssue } from "@plane/types"; @@ -17,6 +17,7 @@ import { TIssue } from "@plane/types"; import { EUserProjectRoles } from "constants/project"; import { EIssuesStoreType } from "constants/issue"; import { ISSUE_UPDATED, ISSUE_DELETED } from "constants/event-tracker"; +import { observer } from "mobx-react"; export type TIssueOperations = { fetch: (workspaceSlug: string, projectId: string, issueId: string) => Promise; @@ -52,7 +53,7 @@ export type TIssueDetailRoot = { is_archived?: boolean; }; -export const IssueDetailRoot: FC = (props) => { +export const IssueDetailRoot: FC = observer((props) => { const { workspaceSlug, projectId, issueId, is_archived = false } = props; // router const router = useRouter(); @@ -76,6 +77,7 @@ export const IssueDetailRoot: FC = (props) => { const { membership: { currentProjectRole }, } = useUser(); + const { theme: themeStore } = useApplication(); const issueOperations: TIssueOperations = useMemo( () => ({ @@ -347,8 +349,8 @@ export const IssueDetailRoot: FC = (props) => { }} /> ) : ( -
-
+
+
= (props) => { is_editable={!is_archived && is_editable} />
-
+
= (props) => { ); -}; +}); diff --git a/web/components/issues/issue-detail/sidebar.tsx b/web/components/issues/issue-detail/sidebar.tsx index 668d3538f..c78bbe942 100644 --- a/web/components/issues/issue-detail/sidebar.tsx +++ b/web/components/issues/issue-detail/sidebar.tsx @@ -187,9 +187,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { buttonVariant={issue?.assignee_ids?.length > 1 ? "transparent-without-text" : "transparent-with-text"} className="w-3/5 flex-grow group" buttonContainerClassName="w-full text-left" - buttonClassName={`text-sm justify-between ${ - issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400" - }`} + buttonClassName={`text-sm justify-between ${issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400" + }`} hideIcon={issue.assignee_ids?.length === 0} dropdownArrow dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline" @@ -233,8 +232,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`} hideIcon clearIconClassName="h-3 w-3 hidden group-hover:inline" - // TODO: add this logic - // showPlaceholderIcon + // TODO: add this logic + // showPlaceholderIcon />
@@ -259,8 +258,8 @@ export const IssueDetailsSidebar: React.FC = observer((props) => { buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`} hideIcon clearIconClassName="h-3 w-3 hidden group-hover:inline" - // TODO: add this logic - // showPlaceholderIcon + // TODO: add this logic + // showPlaceholderIcon />
diff --git a/web/components/issues/issue-detail/subscription.tsx b/web/components/issues/issue-detail/subscription.tsx index b57e75bed..8d05140b3 100644 --- a/web/components/issues/issue-detail/subscription.tsx +++ b/web/components/issues/issue-detail/subscription.tsx @@ -1,5 +1,5 @@ import { FC, useState } from "react"; -import { Bell } from "lucide-react"; +import { Bell, BellOff } from "lucide-react"; import { observer } from "mobx-react-lite"; // UI import { Button } from "@plane/ui"; @@ -52,12 +52,20 @@ export const IssueSubscription: FC = observer((props) => {
); diff --git a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx index 601205b5c..b0eb2be9c 100644 --- a/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx +++ b/web/components/issues/issue-layouts/gantt/base-gantt-root.tsx @@ -69,7 +69,7 @@ export const BaseGanttRoot: React.FC = observer((props: IBaseGan loaderTitle="Issues" blocks={issues ? renderIssueBlocksStructure(issues as TIssue[]) : null} blockUpdateHandler={updateIssueBlockStructure} - blockToRender={(data: TIssue) => } + blockToRender={(data: TIssue) => } sidebarToRender={(props) => ( { - // hooks +type Props = { + issueId: string; +}; + +export const IssueGanttBlock: React.FC = observer((props) => { + const { issueId } = props; + // store hooks const { router: { workspaceSlug }, } = useApplication(); const { getProjectStates } = useProjectState(); - const { setPeekIssue } = useIssueDetail(); + const { + issue: { getIssueById }, + setPeekIssue, + } = useIssueDetail(); + // derived values + const issueDetails = getIssueById(issueId); + const stateDetails = + issueDetails && getProjectStates(issueDetails?.project_id)?.find((state) => state?.id == issueDetails?.state_id); const handleIssuePeekOverview = () => workspaceSlug && - data && - data.project_id && - data.id && - setPeekIssue({ workspaceSlug, projectId: data.project_id, issueId: data.id }); - - const stateColor = getProjectStates(data?.project_id)?.find((state) => state?.id == data?.state_id)?.color || ""; + issueDetails && + setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id }); return (
@@ -35,58 +43,62 @@ export const IssueGanttBlock = ({ data }: { data: TIssue }) => { -
{data?.name}
+
{issueDetails?.name}
- {renderFormattedDate(data?.start_date ?? "")} to {renderFormattedDate(data?.target_date ?? "")} + {renderFormattedDate(issueDetails?.start_date ?? "")} to{" "} + {renderFormattedDate(issueDetails?.target_date ?? "")}
} position="top-left" > -
{data?.name}
+
+ {issueDetails?.name} +
); -}; +}); // rendering issues on gantt sidebar -export const IssueGanttSidebarBlock = ({ data }: { data: TIssue }) => { - // hooks - const { getProjectStates } = useProjectState(); +export const IssueGanttSidebarBlock: React.FC = observer((props) => { + const { issueId } = props; + // store hooks + const { getStateById } = useProjectState(); const { getProjectById } = useProject(); const { router: { workspaceSlug }, } = useApplication(); - const { setPeekIssue } = useIssueDetail(); + const { + issue: { getIssueById }, + setPeekIssue, + } = useIssueDetail(); + // derived values + const issueDetails = getIssueById(issueId); + const projectDetails = issueDetails && getProjectById(issueDetails?.project_id); + const stateDetails = issueDetails && getStateById(issueDetails?.state_id); const handleIssuePeekOverview = () => workspaceSlug && - data && - data.project_id && - data.id && - setPeekIssue({ workspaceSlug, projectId: data.project_id, issueId: data.id }); - - const currentStateDetails = - getProjectStates(data?.project_id)?.find((state) => state?.id == data?.state_id) || undefined; + issueDetails && + setPeekIssue({ workspaceSlug, projectId: issueDetails.project_id, issueId: issueDetails.id }); return (
- {currentStateDetails != undefined && ( - - )} + {stateDetails && }
- {getProjectById(data?.project_id)?.identifier} {data?.sequence_id} + {projectDetails?.identifier} {issueDetails?.sequence_id}
- - {data?.name} + + {issueDetails?.name}
); -}; +}); diff --git a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx index bfecb993b..1ddd21ce2 100644 --- a/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx +++ b/web/components/issues/issue-layouts/gantt/quick-add-issue-form.tsx @@ -11,6 +11,7 @@ import useOutsideClickDetector from "hooks/use-outside-click-detector"; // helpers import { renderFormattedPayloadDate } from "helpers/date-time.helper"; import { createIssuePayload } from "helpers/issue.helper"; +import { cn } from "helpers/common.helper"; // types import { IProject, TIssue } from "@plane/types"; // constants @@ -138,10 +139,12 @@ export const GanttQuickAddIssueForm: React.FC = observe }; return ( <> -
- {isOpen ? ( + {isOpen ? ( +
= observe
{`Press 'Enter' to add another issue`}
- ) : ( -
setIsOpen(true)} - > - - New Issue -
- )} -
+
+ ) : ( + + )} ); }); diff --git a/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx b/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx index 73478c6ac..b7f432385 100644 --- a/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx +++ b/web/components/issues/issue-layouts/spreadsheet/columns/header-column.tsx @@ -66,14 +66,14 @@ export const HeaderColumn = (props: Props) => { } onMenuClose={onClose} placement="bottom-end" + closeOnSelect > handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
@@ -87,11 +87,10 @@ export const HeaderColumn = (props: Props) => { handleOrderBy(propertyDetails.descendingOrderKey, property)}>
diff --git a/web/components/issues/peek-overview/issue-detail.tsx b/web/components/issues/peek-overview/issue-detail.tsx index fefba1713..8c5101938 100644 --- a/web/components/issues/peek-overview/issue-detail.tsx +++ b/web/components/issues/peek-overview/issue-detail.tsx @@ -4,6 +4,7 @@ import { useIssueDetail, useProject, useUser } from "hooks/store"; // components import { IssueDescriptionForm, TIssueOperations } from "components/issues"; import { IssueReaction } from "../issue-detail/reactions"; +import { observer } from "mobx-react"; interface IPeekOverviewIssueDetails { workspaceSlug: string; @@ -15,7 +16,7 @@ interface IPeekOverviewIssueDetails { setIsSubmitting: (value: "submitting" | "submitted" | "saved") => void; } -export const PeekOverviewIssueDetails: FC = (props) => { +export const PeekOverviewIssueDetails: FC = observer((props) => { const { workspaceSlug, projectId, issueId, issueOperations, disabled, isSubmitting, setIsSubmitting } = props; // store hooks const { getProjectById } = useProject(); @@ -23,6 +24,7 @@ export const PeekOverviewIssueDetails: FC = (props) = const { issue: { getIssueById }, } = useIssueDetail(); + // derived values const issue = getIssueById(issueId); if (!issue) return <>; @@ -53,4 +55,4 @@ export const PeekOverviewIssueDetails: FC = (props) = )} ); -}; +}); diff --git a/web/components/modules/gantt-chart/blocks.tsx b/web/components/modules/gantt-chart/blocks.tsx index 72717f12b..188d7b130 100644 --- a/web/components/modules/gantt-chart/blocks.tsx +++ b/web/components/modules/gantt-chart/blocks.tsx @@ -1,52 +1,74 @@ import { useRouter } from "next/router"; +import { observer } from "mobx-react"; +// hooks +import { useApplication, useModule } from "hooks/store"; // ui import { Tooltip, ModuleStatusIcon } from "@plane/ui"; // helpers import { renderFormattedDate } from "helpers/date-time.helper"; -// types -import { IModule } from "@plane/types"; // constants import { MODULE_STATUS } from "constants/module"; -export const ModuleGanttBlock = ({ data }: { data: IModule }) => { +type Props = { + moduleId: string; +}; + +export const ModuleGanttBlock: React.FC = observer((props) => { + const { moduleId } = props; + // router const router = useRouter(); - const { workspaceSlug } = router.query; + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { getModuleById } = useModule(); + // derived values + const moduleDetails = getModuleById(moduleId); return (
s.value === data?.status)?.color }} - onClick={() => router.push(`/${workspaceSlug}/projects/${data?.project}/modules/${data?.id}`)} + style={{ backgroundColor: MODULE_STATUS.find((s) => s.value === moduleDetails?.status)?.color }} + onClick={() => router.push(`/${workspaceSlug}/projects/${moduleDetails?.project}/modules/${moduleDetails?.id}`)} >
-
{data?.name}
+
{moduleDetails?.name}
- {renderFormattedDate(data?.start_date ?? "")} to {renderFormattedDate(data?.target_date ?? "")} + {renderFormattedDate(moduleDetails?.start_date ?? "")} to{" "} + {renderFormattedDate(moduleDetails?.target_date ?? "")}
} position="top-left" > -
{data?.name}
+
{moduleDetails?.name}
); -}; +}); -export const ModuleGanttSidebarBlock = ({ data }: { data: IModule }) => { +export const ModuleGanttSidebarBlock: React.FC = observer((props) => { + const { moduleId } = props; + // router const router = useRouter(); - const { workspaceSlug } = router.query; + // store hooks + const { + router: { workspaceSlug }, + } = useApplication(); + const { getModuleById } = useModule(); + // derived values + const moduleDetails = getModuleById(moduleId); return (
router.push(`/${workspaceSlug}/projects/${data?.project}/modules/${data.id}`)} + onClick={() => router.push(`/${workspaceSlug}/projects/${moduleDetails?.project}/modules/${moduleDetails?.id}`)} > - -
{data.name}
+ +
{moduleDetails?.name}
); -}; +}); diff --git a/web/components/modules/gantt-chart/modules-list-layout.tsx b/web/components/modules/gantt-chart/modules-list-layout.tsx index 53948f71d..a3f37df5c 100644 --- a/web/components/modules/gantt-chart/modules-list-layout.tsx +++ b/web/components/modules/gantt-chart/modules-list-layout.tsx @@ -47,7 +47,7 @@ export const ModulesListGanttChartView: React.FC = observer(() => { blocks={projectModuleIds ? blockFormat(projectModuleIds) : null} sidebarToRender={(props) => } blockUpdateHandler={(block, payload) => handleModuleUpdate(block, payload)} - blockToRender={(data: IModule) => } + blockToRender={(data: IModule) => } enableBlockLeftResize={isAllowed} enableBlockRightResize={isAllowed} enableBlockMove={isAllowed} diff --git a/web/components/modules/module-card-item.tsx b/web/components/modules/module-card-item.tsx index 219942550..ce93ff961 100644 --- a/web/components/modules/module-card-item.tsx +++ b/web/components/modules/module-card-item.tsx @@ -147,8 +147,8 @@ export const ModuleCardItem: React.FC = observer((props) => { ? !moduleTotalIssues || moduleTotalIssues === 0 ? "0 Issue" : moduleTotalIssues === moduleDetails.completed_issues - ? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}` - : `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues` + ? `${moduleTotalIssues} Issue${moduleTotalIssues > 1 ? "s" : ""}` + : `${moduleDetails.completed_issues}/${moduleTotalIssues} Issues` : "0 Issue"; return ( @@ -164,7 +164,7 @@ export const ModuleCardItem: React.FC = observer((props) => { )} setDeleteModal(false)} /> -
+
@@ -240,7 +240,7 @@ export const ModuleCardItem: React.FC = observer((props) => { No due date )} -
+
{isEditingAllowed && (moduleDetails.is_favorite ? (
- -
- -
-
+
{moduleStatus && ( = observer((props) => { )}
+
- {renderDate && ( - - {renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"} - - )} - - -
- {moduleDetails.members_detail.length > 0 ? ( - - {moduleDetails.members_detail.map((member) => ( - - ))} - - ) : ( - - - - )} -
-
- - {isEditingAllowed && - (moduleDetails.is_favorite ? ( - - ) : ( - - ))} - - - {isEditingAllowed && ( - <> - - - - Edit module - - - - - - Delete module - - - - )} - - - - Copy module link +
+
+ {renderDate && ( + + {renderFormattedDate(startDate) ?? "_ _"} - {renderFormattedDate(endDate) ?? "_ _"} - - + )} +
+ +
+ +
+ {moduleDetails.members_detail.length > 0 ? ( + + {moduleDetails.members_detail.map((member) => ( + + ))} + + ) : ( + + + + )} +
+
+ + {isEditingAllowed && + (moduleDetails.is_favorite ? ( + + ) : ( + + ))} + + + {isEditingAllowed && ( + <> + + + + Edit module + + + + + + Delete module + + + + )} + + + + Copy module link + + + +
diff --git a/web/components/modules/module-peek-overview.tsx b/web/components/modules/module-peek-overview.tsx index f82d436bc..81614b61b 100644 --- a/web/components/modules/module-peek-overview.tsx +++ b/web/components/modules/module-peek-overview.tsx @@ -39,7 +39,7 @@ export const ModulePeekOverview: React.FC = observer(({ projectId, worksp {peekModule && (
= observer((props) => { = observer((props) => { <> {renderFormattedDate(startDate) ?? "No date selected"} @@ -430,15 +427,13 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => { <> {renderFormattedDate(endDate) ?? "No date selected"} @@ -596,7 +591,7 @@ export const ModuleDetailsSidebar: React.FC = observer((props) => {
-
+
{ return ( <> + {currentUser && !currentUser.is_tour_completed && ( +
+ +
+ )} {homeDashboardId && joinedProjectIds ? ( <> {joinedProjectIds.length > 0 ? ( @@ -66,11 +71,7 @@ export const WorkspaceDashboardView = observer(() => {
{currentUser && } - {currentUser && !currentUser.is_tour_completed && ( -
- -
- )} +
diff --git a/web/components/pages/create-update-page-modal.tsx b/web/components/pages/create-update-page-modal.tsx index 31cee45b3..e81a0040b 100644 --- a/web/components/pages/create-update-page-modal.tsx +++ b/web/components/pages/create-update-page-modal.tsx @@ -104,7 +104,7 @@ export const CreateUpdatePageModal: FC = (props) => { leaveFrom="opacity-100 translate-y-0 sm:scale-100" leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95" > - + diff --git a/web/components/pages/page-form.tsx b/web/components/pages/page-form.tsx index 79d378c59..4f5874e5f 100644 --- a/web/components/pages/page-form.tsx +++ b/web/components/pages/page-form.tsx @@ -67,7 +67,7 @@ export const PageForm: React.FC = (props) => {
-
+
= (props) => {
)} /> -
+
diff --git a/web/components/pages/pages-list/list-item.tsx b/web/components/pages/pages-list/list-item.tsx index 03133d5df..6b1a4793d 100644 --- a/web/components/pages/pages-list/list-item.tsx +++ b/web/components/pages/pages-list/list-item.tsx @@ -155,7 +155,7 @@ export const PagesListItem: FC = observer(({ pageId, projectId } setDeletePageModal(false)} pageId={pageId} />
  • -
    +
    diff --git a/web/components/pages/pages-list/recent-pages-list.tsx b/web/components/pages/pages-list/recent-pages-list.tsx index 960d5253b..e998629c6 100644 --- a/web/components/pages/pages-list/recent-pages-list.tsx +++ b/web/components/pages/pages-list/recent-pages-list.tsx @@ -52,7 +52,7 @@ export const RecentPagesList: FC = observer(() => { return (
    -

    +

    {replaceUnderscoreIfSnakeCase(key)}

    diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx b/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx index 6be784368..64f43939e 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/issues/[issueId].tsx @@ -1,4 +1,4 @@ -import React, { ReactElement } from "react"; +import React, { ReactElement, useEffect } from "react"; import { useRouter } from "next/router"; import useSWR from "swr"; import { observer } from "mobx-react-lite"; @@ -12,7 +12,7 @@ import { Loader } from "@plane/ui"; // types import { NextPageWithLayout } from "lib/types"; // fetch-keys -import { useIssueDetail } from "hooks/store"; +import { useApplication, useIssueDetail } from "hooks/store"; const IssueDetailsPage: NextPageWithLayout = observer(() => { // router @@ -23,6 +23,7 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => { fetchIssue, issue: { getIssueById }, } = useIssueDetail(); + const { theme: themeStore } = useApplication(); const { isLoading } = useSWR( workspaceSlug && projectId && issueId ? `ISSUE_DETAIL_${workspaceSlug}_${projectId}_${issueId}` : null, @@ -34,6 +35,21 @@ const IssueDetailsPage: NextPageWithLayout = observer(() => { const issue = getIssueById(issueId?.toString() || "") || undefined; const issueLoader = !issue || isLoading ? true : false; + useEffect(() => { + const handleToggleIssueDetailSidebar = () => { + if (window && window.innerWidth < 768) { + themeStore.toggleIssueDetailSidebar(true); + } + if (window && themeStore.issueDetailSidebarCollapsed && window.innerWidth >= 768) { + themeStore.toggleIssueDetailSidebar(false); + } + }; + + window.addEventListener("resize", handleToggleIssueDetailSidebar); + handleToggleIssueDetailSidebar(); + return () => window.removeEventListener("resize", handleToggleIssueDetailSidebar); + }, [themeStore]); + return ( <> {issueLoader ? ( diff --git a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx index b0ab29285..1f3204045 100644 --- a/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx +++ b/web/pages/[workspaceSlug]/projects/[projectId]/pages/index.tsx @@ -103,6 +103,25 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { const isEditingAllowed = !!currentProjectRole && currentProjectRole >= EUserWorkspaceRoles.MEMBER; + const mobileTabList = ( + +
    + {PAGE_TABS_LIST.map((tab) => ( + + `text-sm outline-none pb-3 ${ + selected ? "border-custom-primary-100 text-custom-primary-100 border-b" : "" + }` + } + > + {tab.title} + + ))} +
    +
    + ); + if (loader || archivedPageLoader) return (
    @@ -121,8 +140,8 @@ const ProjectPagesPage: NextPageWithLayout = observer(() => { projectId={projectId.toString()} /> )} -
    -
    +
    +

    Pages

    { } }} > - +
    {mobileTabList}
    +
    {PAGE_TABS_LIST.map((tab) => ( void; setTheme: (theme: any) => void; toggleProfileSidebar: (collapsed?: boolean) => void; toggleWorkspaceAnalyticsSidebar: (collapsed?: boolean) => void; + toggleIssueDetailSidebar: (collapsed?: boolean) => void; } export class ThemeStore implements IThemeStore { @@ -22,6 +24,7 @@ export class ThemeStore implements IThemeStore { theme: string | null = null; profileSidebarCollapsed: boolean | undefined = undefined; workspaceAnalyticsSidebarCollapsed: boolean | undefined = undefined; + issueDetailSidebarCollapsed: boolean | undefined = undefined; // root store rootStore; @@ -32,11 +35,13 @@ export class ThemeStore implements IThemeStore { theme: observable.ref, profileSidebarCollapsed: observable.ref, workspaceAnalyticsSidebarCollapsed: observable.ref, + issueDetailSidebarCollapsed: observable.ref, // action toggleSidebar: action, setTheme: action, toggleProfileSidebar: action, - toggleWorkspaceAnalyticsSidebar: action + toggleWorkspaceAnalyticsSidebar: action, + toggleIssueDetailSidebar: action, // computed }); // root store @@ -82,6 +87,15 @@ export class ThemeStore implements IThemeStore { localStorage.setItem("workspace_analytics_sidebar_collapsed", this.workspaceAnalyticsSidebarCollapsed.toString()); }; + toggleIssueDetailSidebar = (collapsed?: boolean) => { + if(collapsed === undefined) { + this.issueDetailSidebarCollapsed = !this.issueDetailSidebarCollapsed; + } else { + this.issueDetailSidebarCollapsed = collapsed; + } + localStorage.setItem("issue_detail_sidebar_collapsed", this.issueDetailSidebarCollapsed.toString()); + } + /** * Sets the user theme and applies it to the platform * @param _theme diff --git a/web/store/issue/issue-details/issue.store.ts b/web/store/issue/issue-details/issue.store.ts index 46605c771..43a7ca093 100644 --- a/web/store/issue/issue-details/issue.store.ts +++ b/web/store/issue/issue-details/issue.store.ts @@ -4,6 +4,7 @@ import { IssueArchiveService, IssueService } from "services/issue"; // types import { IIssueDetail } from "./root.store"; import { TIssue } from "@plane/types"; +import { computedFn } from "mobx-utils"; export interface IIssueStoreActions { // actions @@ -44,10 +45,10 @@ export class IssueStore implements IIssueStore { } // helper methods - getIssueById = (issueId: string) => { + getIssueById = computedFn((issueId: string) => { if (!issueId) return undefined; return this.rootIssueDetailStore.rootIssueStore.issues.getIssueById(issueId) ?? undefined; - }; + }); // actions fetchIssue = async (workspaceSlug: string, projectId: string, issueId: string, isArchived = false) => { @@ -63,12 +64,12 @@ export class IssueStore implements IIssueStore { if (!issue) throw new Error("Issue not found"); - this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue]); + this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue], true); // store handlers from issue detail // parent if (issue && issue?.parent && issue?.parent?.id) - this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue?.parent]); + this.rootIssueDetailStore.rootIssueStore.issues.addIssue([issue.parent]); // assignees // labels // state diff --git a/web/store/issue/issue.store.ts b/web/store/issue/issue.store.ts index 8ee689daf..36b2d8741 100644 --- a/web/store/issue/issue.store.ts +++ b/web/store/issue/issue.store.ts @@ -10,7 +10,7 @@ export type IIssueStore = { // observables issuesMap: Record; // Record defines issue_id as key and TIssue as value // actions - addIssue(issues: TIssue[]): void; + addIssue(issues: TIssue[], shouldReplace?: boolean): void; updateIssue(issueId: string, issue: Partial): void; removeIssue(issueId: string): void; // helper methods @@ -39,11 +39,11 @@ export class IssueStore implements IIssueStore { * @param {TIssue[]} issues * @returns {void} */ - addIssue = (issues: TIssue[]) => { + addIssue = (issues: TIssue[], shouldReplace = false) => { if (issues && issues.length <= 0) return; runInAction(() => { issues.forEach((issue) => { - if (!this.issuesMap[issue.id]) set(this.issuesMap, issue.id, issue); + if (!this.issuesMap[issue.id] || shouldReplace) set(this.issuesMap, issue.id, issue); }); }); };