[WEB-2442] fix : Timeline layout bugs (#5946)

* fix relation creation and removal for Issue relations

* fix Scrolling to block when the block is beyond current chart's limits

* fix dark mode for timeline layout

* use a hook to get the current relations available in the environment, instead of directly importing it

* Update relation activity for all the relations
This commit is contained in:
rahulramesha 2024-11-04 16:55:38 +05:30 committed by GitHub
parent a1bfde6af9
commit 71589f93ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 201 additions and 105 deletions

View file

@ -318,7 +318,7 @@ class IssueRelationViewSet(BaseViewSet):
origin=request.META.get("HTTP_ORIGIN"), origin=request.META.get("HTTP_ORIGIN"),
) )
if relation_type == "blocking": if relation_type in ["blocking", "start_after", "finish_after"]:
return Response( return Response(
RelatedIssueSerializer(issue_relation, many=True).data, RelatedIssueSerializer(issue_relation, many=True).data,
status=status.HTTP_201_CREATED, status=status.HTTP_201_CREATED,
@ -333,7 +333,7 @@ class IssueRelationViewSet(BaseViewSet):
relation_type = request.data.get("relation_type", None) relation_type = request.data.get("relation_type", None)
related_issue = request.data.get("related_issue", None) related_issue = request.data.get("related_issue", None)
if relation_type == "blocking": if relation_type in ["blocking", "start_after", "finish_after"]:
issue_relation = IssueRelation.objects.get( issue_relation = IssueRelation.objects.get(
workspace__slug=slug, workspace__slug=slug,
project_id=project_id, project_id=project_id,

View file

@ -0,0 +1,20 @@
import { TIssueActivity } from "@plane/types";
export const getRelationActivityContent = (activity: TIssueActivity | undefined): string | undefined => {
if (!activity) return;
switch (activity.field) {
case "blocking":
return activity.old_value === "" ? `marked this issue is blocking issue ` : `removed the blocking issue `;
case "blocked_by":
return activity.old_value === ""
? `marked this issue is being blocked by `
: `removed this issue being blocked by issue `;
case "duplicate":
return activity.old_value === "" ? `marked this issue as duplicate of ` : `removed this issue as a duplicate of `;
case "relates_to":
activity.old_value === "" ? `marked that this issue relates to ` : `removed the relation from `;
}
return;
};

View file

@ -3,6 +3,8 @@ import { RelatedIcon } from "@plane/ui";
import { TRelationObject } from "@/components/issues"; import { TRelationObject } from "@/components/issues";
import { TIssueRelationTypes } from "../../types"; import { TIssueRelationTypes } from "../../types";
export * from "./activity";
export const ISSUE_RELATION_OPTIONS: Record<TIssueRelationTypes, TRelationObject> = { export const ISSUE_RELATION_OPTIONS: Record<TIssueRelationTypes, TRelationObject> = {
relates_to: { relates_to: {
key: "relates_to", key: "relates_to",
@ -33,3 +35,5 @@ export const ISSUE_RELATION_OPTIONS: Record<TIssueRelationTypes, TRelationObject
placeholder: "None", placeholder: "None",
}, },
}; };
export const useTimeLineRelationOptions = () => ISSUE_RELATION_OPTIONS;

View file

@ -5,12 +5,13 @@ import RenderIfVisible from "@/components/core/render-if-visible-HOC";
import { TSelectionHelper } from "@/hooks/use-multiple-select"; import { TSelectionHelper } from "@/hooks/use-multiple-select";
// types // types
import { BLOCK_HEIGHT } from "../constants"; import { BLOCK_HEIGHT } from "../constants";
import { IBlockUpdateData } from "../types"; import { IBlockUpdateData, IGanttBlock } from "../types";
import { BlockRow } from "./block-row"; import { BlockRow } from "./block-row";
export type GanttChartBlocksProps = { export type GanttChartBlocksProps = {
blockIds: string[]; blockIds: string[];
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
handleScrollToBlock: (block: IGanttBlock) => void;
enableAddBlock: boolean | ((blockId: string) => boolean); enableAddBlock: boolean | ((blockId: string) => boolean);
showAllBlocks: boolean; showAllBlocks: boolean;
selectionHelpers: TSelectionHelper; selectionHelpers: TSelectionHelper;
@ -18,7 +19,15 @@ export type GanttChartBlocksProps = {
}; };
export const GanttChartRowList: FC<GanttChartBlocksProps> = (props) => { export const GanttChartRowList: FC<GanttChartBlocksProps> = (props) => {
const { blockIds, blockUpdateHandler, enableAddBlock, showAllBlocks, selectionHelpers, ganttContainerRef } = props; const {
blockIds,
blockUpdateHandler,
handleScrollToBlock,
enableAddBlock,
showAllBlocks,
selectionHelpers,
ganttContainerRef,
} = props;
return ( return (
<div className="absolute top-0 left-0 min-w-full w-max"> <div className="absolute top-0 left-0 min-w-full w-max">
@ -37,6 +46,7 @@ export const GanttChartRowList: FC<GanttChartBlocksProps> = (props) => {
blockId={blockId} blockId={blockId}
showAllBlocks={showAllBlocks} showAllBlocks={showAllBlocks}
blockUpdateHandler={blockUpdateHandler} blockUpdateHandler={blockUpdateHandler}
handleScrollToBlock={handleScrollToBlock}
enableAddBlock={typeof enableAddBlock === "function" ? enableAddBlock(blockId) : enableAddBlock} enableAddBlock={typeof enableAddBlock === "function" ? enableAddBlock(blockId) : enableAddBlock}
selectionHelpers={selectionHelpers} selectionHelpers={selectionHelpers}
ganttContainerRef={ganttContainerRef} ganttContainerRef={ganttContainerRef}

View file

@ -10,19 +10,20 @@ import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
// //
import { BLOCK_HEIGHT, SIDEBAR_WIDTH } from "../constants"; import { BLOCK_HEIGHT, SIDEBAR_WIDTH } from "../constants";
import { ChartAddBlock } from "../helpers"; import { ChartAddBlock } from "../helpers";
import { IBlockUpdateData } from "../types"; import { IBlockUpdateData, IGanttBlock } from "../types";
type Props = { type Props = {
blockId: string; blockId: string;
showAllBlocks: boolean; showAllBlocks: boolean;
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void; blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
handleScrollToBlock: (block: IGanttBlock) => void;
enableAddBlock: boolean; enableAddBlock: boolean;
selectionHelpers: TSelectionHelper; selectionHelpers: TSelectionHelper;
ganttContainerRef: React.RefObject<HTMLDivElement>; ganttContainerRef: React.RefObject<HTMLDivElement>;
}; };
export const BlockRow: React.FC<Props> = observer((props) => { export const BlockRow: React.FC<Props> = observer((props) => {
const { blockId, showAllBlocks, blockUpdateHandler, enableAddBlock, selectionHelpers } = props; const { blockId, showAllBlocks, blockUpdateHandler, handleScrollToBlock, enableAddBlock, selectionHelpers } = props;
// states // states
const [isHidden, setIsHidden] = useState(false); const [isHidden, setIsHidden] = useState(false);
const [isBlockHiddenOnLeft, setIsBlockHiddenOnLeft] = useState(false); const [isBlockHiddenOnLeft, setIsBlockHiddenOnLeft] = useState(false);
@ -67,14 +68,6 @@ export const BlockRow: React.FC<Props> = observer((props) => {
// hide the block if it doesn't have start and target dates and showAllBlocks is false // hide the block if it doesn't have start and target dates and showAllBlocks is false
if (!block || !block.data || (!showAllBlocks && !(block.start_date && block.target_date))) return null; if (!block || !block.data || (!showAllBlocks && !(block.start_date && block.target_date))) return null;
// scroll to a hidden block
const handleScrollToBlock = () => {
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;
};
const isBlockVisibleOnChart = block.start_date && block.target_date; const isBlockVisibleOnChart = block.start_date && block.target_date;
const isBlockSelected = selectionHelpers.getIsEntitySelected(block.id); const isBlockSelected = selectionHelpers.getIsEntitySelected(block.id);
const isBlockFocused = selectionHelpers.getIsEntityActive(block.id); const isBlockFocused = selectionHelpers.getIsEntityActive(block.id);
@ -106,7 +99,7 @@ export const BlockRow: React.FC<Props> = observer((props) => {
style={{ style={{
left: `${SIDEBAR_WIDTH + 4}px`, left: `${SIDEBAR_WIDTH + 4}px`,
}} }}
onClick={handleScrollToBlock} onClick={() => handleScrollToBlock(block)}
> >
<ArrowRight <ArrowRight
className={cn("h-3.5 w-3.5", { className={cn("h-3.5 w-3.5", {

View file

@ -5,10 +5,12 @@ import { observer } from "mobx-react";
// components // components
import { MultipleSelectGroup } from "@/components/core"; import { MultipleSelectGroup } from "@/components/core";
import { import {
ChartDataType,
GanttChartBlocksList, GanttChartBlocksList,
GanttChartSidebar, GanttChartSidebar,
IBlockUpdateData, IBlockUpdateData,
IBlockUpdateDependencyData, IBlockUpdateDependencyData,
IGanttBlock,
MonthChartView, MonthChartView,
QuarterChartView, QuarterChartView,
TGanttViews, TGanttViews,
@ -16,6 +18,7 @@ import {
} from "@/components/gantt-chart"; } from "@/components/gantt-chart";
// helpers // helpers
import { cn } from "@/helpers/common.helper"; import { cn } from "@/helpers/common.helper";
import { getDate } from "@/helpers/date-time.helper";
// hooks // hooks
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart"; import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
// plane web components // plane web components
@ -26,6 +29,7 @@ import { useBulkOperationStatus } from "@/plane-web/hooks/use-bulk-operation-sta
// //
import { GanttChartRowList } from "../blocks/block-row-list"; import { GanttChartRowList } from "../blocks/block-row-list";
import { GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants"; import { GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants";
import { getItemPositionWidth } from "../views";
import { TimelineDragHelper } from "./timeline-drag-helper"; import { TimelineDragHelper } from "./timeline-drag-helper";
type Props = { type Props = {
@ -46,7 +50,11 @@ type Props = {
showAllBlocks: boolean; showAllBlocks: boolean;
sidebarToRender: (props: any) => React.ReactNode; sidebarToRender: (props: any) => React.ReactNode;
title: string; title: string;
updateCurrentViewRenderPayload: (direction: "left" | "right", currentView: TGanttViews) => void; updateCurrentViewRenderPayload: (
direction: "left" | "right",
currentView: TGanttViews,
targetDate?: Date
) => ChartDataType | undefined;
quickAdd?: React.JSX.Element | undefined; quickAdd?: React.JSX.Element | undefined;
}; };
@ -105,6 +113,26 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
if (approxRangeLeft < clientWidth) updateCurrentViewRenderPayload("left", currentView); if (approxRangeLeft < clientWidth) updateCurrentViewRenderPayload("left", currentView);
}; };
const handleScrollToBlock = (block: IGanttBlock) => {
const scrollContainer = ganttContainerRef.current as HTMLDivElement;
const scrollToDate = getDate(block.start_date);
let chartData;
if (!scrollContainer || !currentViewData || !scrollToDate) return;
if (scrollToDate.getTime() < currentViewData.data.startDate.getTime()) {
chartData = updateCurrentViewRenderPayload("left", currentView, scrollToDate);
} else if (scrollToDate.getTime() > currentViewData.data.endDate.getTime()) {
chartData = updateCurrentViewRenderPayload("right", currentView, scrollToDate);
}
// update container's scroll position to the block's position
const updatedPosition = getItemPositionWidth(chartData ?? currentViewData, block);
setTimeout(() => {
if (updatedPosition) scrollContainer.scrollLeft = updatedPosition.marginLeft - 4;
});
};
const CHART_VIEW_COMPONENTS: { const CHART_VIEW_COMPONENTS: {
[key in TGanttViews]: React.FC; [key in TGanttViews]: React.FC;
} = { } = {
@ -166,6 +194,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
<GanttChartRowList <GanttChartRowList
blockIds={blockIds} blockIds={blockIds}
blockUpdateHandler={blockUpdateHandler} blockUpdateHandler={blockUpdateHandler}
handleScrollToBlock={handleScrollToBlock}
enableAddBlock={enableAddBlock} enableAddBlock={enableAddBlock}
showAllBlocks={showAllBlocks} showAllBlocks={showAllBlocks}
selectionHelpers={helpers} selectionHelpers={helpers}

View file

@ -86,7 +86,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
updateAllBlocksOnChartChangeWhileDragging, updateAllBlocksOnChartChangeWhileDragging,
} = useTimeLineChartStore(); } = useTimeLineChartStore();
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => { const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews, targetDate?: Date) => {
const selectedCurrentView: TGanttViews = view; const selectedCurrentView: TGanttViews = view;
const selectedCurrentViewData: ChartDataType | undefined = const selectedCurrentViewData: ChartDataType | undefined =
selectedCurrentView && selectedCurrentView === currentViewData?.key selectedCurrentView && selectedCurrentView === currentViewData?.key
@ -96,7 +96,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
if (selectedCurrentViewData === undefined) return; if (selectedCurrentViewData === undefined) return;
const currentViewHelpers = timelineViewHelpers[selectedCurrentView]; const currentViewHelpers = timelineViewHelpers[selectedCurrentView];
const currentRender = currentViewHelpers.generateChart(selectedCurrentViewData, side); const currentRender = currentViewHelpers.generateChart(selectedCurrentViewData, side, targetDate);
const mergeRenderPayloads = currentViewHelpers.mergeRenderPayloads as ( const mergeRenderPayloads = currentViewHelpers.mergeRenderPayloads as (
a: IWeekBlock[] | IMonthView | IMonthBlock[], a: IWeekBlock[] | IMonthView | IMonthBlock[],
b: IWeekBlock[] | IMonthView | IMonthBlock[] b: IWeekBlock[] | IMonthView | IMonthBlock[]
@ -109,7 +109,8 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
if (side === "left") { if (side === "left") {
updateCurrentView(selectedCurrentView); updateCurrentView(selectedCurrentView);
updateRenderView(mergeRenderPayloads(currentRender.payload, renderView)); updateRenderView(mergeRenderPayloads(currentRender.payload, renderView));
updatingCurrentLeftScrollPosition(currentRender.scrollWidth); updateItemsContainerWidth(currentRender.scrollWidth);
if (!targetDate) updateCurrentLeftScrollPosition(currentRender.scrollWidth);
updateAllBlocksOnChartChangeWhileDragging(currentRender.scrollWidth); updateAllBlocksOnChartChangeWhileDragging(currentRender.scrollWidth);
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth); setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
} else if (side === "right") { } else if (side === "right") {
@ -125,6 +126,8 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
}, 50); }, 50);
} }
} }
return currentRender.state;
}; };
const handleToday = () => updateCurrentViewRenderPayload(null, currentView); const handleToday = () => updateCurrentViewRenderPayload(null, currentView);
@ -135,12 +138,17 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
const updatingCurrentLeftScrollPosition = (width: number) => { const updateItemsContainerWidth = (width: number) => {
const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement;
if (!scrollContainer) return;
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
};
const updateCurrentLeftScrollPosition = (width: number) => {
const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement; const scrollContainer = document.querySelector("#gantt-container") as HTMLDivElement;
if (!scrollContainer) return; if (!scrollContainer) return;
scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft; scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft;
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
}; };
const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => { const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => {

View file

@ -67,7 +67,7 @@ export const MonthChartView: FC<any> = observer(() => {
className={cn( className={cn(
"flex flex-shrink-0 py-1 px-2 text-center capitalize justify-between outline-[0.25px] outline outline-custom-border-200", "flex flex-shrink-0 py-1 px-2 text-center capitalize justify-between outline-[0.25px] outline outline-custom-border-200",
{ {
"bg-custom-primary-10": weekBlock.today, "bg-custom-primary-100/20": weekBlock.today,
} }
)} )}
style={{ width: `${currentViewData?.data.dayWidth * 7}px` }} style={{ width: `${currentViewData?.data.dayWidth * 7}px` }}
@ -92,7 +92,7 @@ export const MonthChartView: FC<any> = observer(() => {
<div <div
key={`column-${weekBlock.startDate}-${weekBlock.endDate}`} key={`column-${weekBlock.startDate}-${weekBlock.endDate}`}
className={cn("h-full overflow-hidden outline-[0.25px] outline outline-custom-border-100", { className={cn("h-full overflow-hidden outline-[0.25px] outline outline-custom-border-100", {
"bg-custom-primary-10": weekBlock.today, "bg-custom-primary-100/20": weekBlock.today,
})} })}
style={{ width: `${currentViewData?.data.dayWidth * 7}px` }} style={{ width: `${currentViewData?.data.dayWidth * 7}px` }}
/> />

View file

@ -56,7 +56,7 @@ export const QuarterChartView: FC<any> = observer(() => {
className={cn( className={cn(
"flex flex-shrink-0 text-center capitalize justify-center outline-[0.25px] outline outline-custom-border-200", "flex flex-shrink-0 text-center capitalize justify-center outline-[0.25px] outline outline-custom-border-200",
{ {
"bg-custom-primary-10": monthBlock.today, "bg-custom-primary-100/20": monthBlock.today,
} }
)} )}
style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }} style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }}
@ -80,7 +80,7 @@ export const QuarterChartView: FC<any> = observer(() => {
<div <div
key={`column-${rootIndex}-${index}`} key={`column-${rootIndex}-${index}`}
className={cn("h-full overflow-hidden outline-[0.25px] outline outline-custom-border-100", { className={cn("h-full overflow-hidden outline-[0.25px] outline outline-custom-border-100", {
"bg-custom-primary-10": monthBlock.today, "bg-custom-primary-100/20": monthBlock.today,
})} })}
style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }} style={{ width: `${currentViewData?.data.dayWidth * monthBlock.days}px` }}
/> />

View file

@ -49,7 +49,7 @@ export const WeekChartView: FC<any> = observer(() => {
className={cn( className={cn(
"flex flex-shrink-0 p-1 text-center capitalize justify-between outline-[0.25px] outline outline-custom-border-200", "flex flex-shrink-0 p-1 text-center capitalize justify-between outline-[0.25px] outline outline-custom-border-200",
{ {
"bg-custom-primary-10": weekDay.today, "bg-custom-primary-100/20": weekDay.today,
} }
)} )}
style={{ width: `${currentViewData?.data.dayWidth}px` }} style={{ width: `${currentViewData?.data.dayWidth}px` }}
@ -71,12 +71,12 @@ export const WeekChartView: FC<any> = observer(() => {
</div> </div>
</div> </div>
{/** Day Columns */} {/** Day Columns */}
<div className="h-full w-full flex-grow flex"> <div className="h-full w-full flex-grow flex bg-custom-background-100">
{block?.children?.map((weekDay, index) => ( {block?.children?.map((weekDay, index) => (
<div <div
key={`column-${rootIndex}-${index}`} key={`column-${rootIndex}-${index}`}
className={cn("h-full overflow-hidden outline-[0.25px] outline outline-custom-border-100", { className={cn("h-full overflow-hidden outline-[0.25px] outline outline-custom-border-100", {
"bg-custom-primary-10": weekDay.today, "bg-custom-primary-100/20": weekDay.today,
})} })}
style={{ width: `${currentViewData?.data.dayWidth}px` }} style={{ width: `${currentViewData?.data.dayWidth}px` }}
> >

View file

@ -30,7 +30,7 @@ export interface IMonthView {
* @param side * @param side
* @returns * @returns
*/ */
const generateMonthChart = (monthPayload: ChartDataType, side: null | "left" | "right") => { const generateMonthChart = (monthPayload: ChartDataType, side: null | "left" | "right", targetDate?: Date) => {
let renderState = cloneDeep(monthPayload); let renderState = cloneDeep(monthPayload);
const range: number = renderState.data.approxFilterRange || 6; const range: number = renderState.data.approxFilterRange || 6;
@ -63,15 +63,16 @@ const generateMonthChart = (monthPayload: ChartDataType, side: null | "left" | "
} }
// When side is left, generate more months on the left side of the start date // When side is left, generate more months on the left side of the start date
else if (side === "left") { else if (side === "left") {
const currentDate = renderState.data.startDate; const chartStartDate = renderState.data.startDate;
const currentDate = targetDate ? targetDate : chartStartDate;
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1);
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1); plusDate = new Date(chartStartDate.getFullYear(), chartStartDate.getMonth(), chartStartDate.getDate() - 1);
if (minusDate && plusDate) filteredDates = getMonthsViewBetweenTwoDates(minusDate, plusDate); if (minusDate && plusDate) filteredDates = getMonthsViewBetweenTwoDates(minusDate, plusDate);
startDate = filteredDates.weeks[0]?.startDate; startDate = filteredDates.weeks[0]?.startDate;
endDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1); endDate = new Date(chartStartDate.getFullYear(), chartStartDate.getMonth(), chartStartDate.getDate() - 1);
renderState = { renderState = {
...renderState, ...renderState,
data: { ...renderState.data, startDate }, data: { ...renderState.data, startDate },
@ -79,14 +80,15 @@ const generateMonthChart = (monthPayload: ChartDataType, side: null | "left" | "
} }
// When side is right, generate more months on the right side of the end date // When side is right, generate more months on the right side of the end date
else if (side === "right") { else if (side === "right") {
const currentDate = renderState.data.endDate; const chartEndDate = renderState.data.endDate;
const currentDate = targetDate ? targetDate : chartEndDate;
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 1); minusDate = new Date(chartEndDate.getFullYear(), chartEndDate.getMonth(), chartEndDate.getDate() + 1);
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 1);
if (minusDate && plusDate) filteredDates = getMonthsViewBetweenTwoDates(minusDate, plusDate); if (minusDate && plusDate) filteredDates = getMonthsViewBetweenTwoDates(minusDate, plusDate);
startDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 1); startDate = new Date(chartEndDate.getFullYear(), chartEndDate.getMonth(), chartEndDate.getDate() + 1);
endDate = filteredDates.weeks[filteredDates.weeks.length - 1]?.endDate; endDate = filteredDates.weeks[filteredDates.weeks.length - 1]?.endDate;
renderState = { renderState = {
...renderState, ...renderState,

View file

@ -19,7 +19,7 @@ export interface IQuarterMonthBlock {
* @param side * @param side
* @returns * @returns
*/ */
const generateQuarterChart = (quarterPayload: ChartDataType, side: null | "left" | "right") => { const generateQuarterChart = (quarterPayload: ChartDataType, side: null | "left" | "right", targetDate?: Date) => {
let renderState = quarterPayload; let renderState = quarterPayload;
const range: number = renderState.data.approxFilterRange || 12; const range: number = renderState.data.approxFilterRange || 12;
@ -55,16 +55,17 @@ const generateQuarterChart = (quarterPayload: ChartDataType, side: null | "left"
} }
// When side is left, generate more months on the left side of the start date // When side is left, generate more months on the left side of the start date
else if (side === "left") { else if (side === "left") {
const currentDate = renderState.data.startDate; const chartStartDate = renderState.data.startDate;
const currentDate = targetDate ? targetDate : chartStartDate;
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range / 2, 1); minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range / 2, 1);
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - 1, 1); plusDate = new Date(chartStartDate.getFullYear(), chartStartDate.getMonth() - 1, 1);
if (minusDate && plusDate) filteredDates = getMonthsBetweenTwoDates(minusDate, plusDate); if (minusDate && plusDate) filteredDates = getMonthsBetweenTwoDates(minusDate, plusDate);
const startMonthBlock = filteredDates[0]; const startMonthBlock = filteredDates[0];
startDate = new Date(startMonthBlock.year, startMonthBlock.month, 1); startDate = new Date(startMonthBlock.year, startMonthBlock.month, 1);
endDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1); endDate = new Date(chartStartDate.getFullYear(), chartStartDate.getMonth(), chartStartDate.getDate() - 1);
renderState = { renderState = {
...renderState, ...renderState,
data: { ...renderState.data, startDate }, data: { ...renderState.data, startDate },
@ -72,15 +73,16 @@ const generateQuarterChart = (quarterPayload: ChartDataType, side: null | "left"
} }
// When side is right, generate more months on the right side of the end date // When side is right, generate more months on the right side of the end date
else if (side === "right") { else if (side === "right") {
const currentDate = renderState.data.endDate; const chartEndDate = renderState.data.endDate;
const currentDate = targetDate ? targetDate : chartEndDate;
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + 1, 1); minusDate = new Date(chartEndDate.getFullYear(), chartEndDate.getMonth() + 1, 1);
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range / 2, 1); plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range / 2, 1);
if (minusDate && plusDate) filteredDates = getMonthsBetweenTwoDates(minusDate, plusDate); if (minusDate && plusDate) filteredDates = getMonthsBetweenTwoDates(minusDate, plusDate);
const endMonthBlock = filteredDates[filteredDates.length - 1]; const endMonthBlock = filteredDates[filteredDates.length - 1];
startDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 1); startDate = new Date(chartEndDate.getFullYear(), chartEndDate.getMonth(), chartEndDate.getDate() + 1);
endDate = new Date(endMonthBlock.year, endMonthBlock.month + 1, 0); endDate = new Date(endMonthBlock.year, endMonthBlock.month + 1, 0);
renderState = { renderState = {
...renderState, ...renderState,

View file

@ -38,7 +38,7 @@ export interface IWeekBlock {
* @param side * @param side
* @returns * @returns
*/ */
const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "right") => { const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "right", targetDate?: Date) => {
let renderState = weekPayload; let renderState = weekPayload;
const range: number = renderState.data.approxFilterRange || 6; const range: number = renderState.data.approxFilterRange || 6;
@ -71,15 +71,16 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri
} }
// When side is left, generate more weeks on the left side of the start date // When side is left, generate more weeks on the left side of the start date
else if (side === "left") { else if (side === "left") {
const currentDate = renderState.data.startDate; const chartStartDate = renderState.data.startDate;
const currentDate = targetDate ? targetDate : chartStartDate;
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, currentDate.getDate()); minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1);
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1); plusDate = new Date(chartStartDate.getFullYear(), chartStartDate.getMonth(), chartStartDate.getDate() - 1);
if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate); if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate);
startDate = filteredDates[0].startDate; startDate = filteredDates[0].startDate;
endDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1); endDate = new Date(chartStartDate.getFullYear(), chartStartDate.getMonth(), chartStartDate.getDate() - 1);
renderState = { renderState = {
...renderState, ...renderState,
data: { ...renderState.data, startDate }, data: { ...renderState.data, startDate },
@ -87,14 +88,15 @@ const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "ri
} }
// When side is right, generate more weeks on the right side of the end date // When side is right, generate more weeks on the right side of the end date
else if (side === "right") { else if (side === "right") {
const currentDate = renderState.data.endDate; const chartEndDate = renderState.data.endDate;
const currentDate = targetDate ? targetDate : chartEndDate;
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 1); minusDate = new Date(chartEndDate.getFullYear(), chartEndDate.getMonth(), chartEndDate.getDate() + 1);
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate()); plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 1);
if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate); if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate);
startDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() + 1); startDate = new Date(chartEndDate.getFullYear(), chartEndDate.getMonth(), chartEndDate.getDate() + 1);
endDate = filteredDates[filteredDates.length - 1].endDate; endDate = filteredDates[filteredDates.length - 1].endDate;
renderState = { renderState = {
...renderState, ...renderState,

View file

@ -10,6 +10,8 @@ import {
} from "@/components/issues/issue-detail-widgets"; } from "@/components/issues/issue-detail-widgets";
// hooks // hooks
import { useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
// Plane-web
import { useTimeLineRelationOptions } from "@/plane-web/components/relations";
type Props = { type Props = {
workspaceSlug: string; workspaceSlug: string;
@ -31,7 +33,8 @@ export const IssueDetailWidgetCollapsibles: FC<Props> = observer((props) => {
// derived values // derived values
const issue = getIssueById(issueId); const issue = getIssueById(issueId);
const subIssues = subIssuesByIssueId(issueId); const subIssues = subIssuesByIssueId(issueId);
const issueRelationsCount = getRelationCountByIssueId(issueId); const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
const issueRelationsCount = getRelationCountByIssueId(issueId, ISSUE_RELATION_OPTIONS);
// render conditions // render conditions
const shouldRenderSubIssues = !!subIssues && subIssues.length > 0; const shouldRenderSubIssues = !!subIssues && subIssues.length > 0;

View file

@ -10,7 +10,7 @@ import { CreateUpdateIssueModal } from "@/components/issues/issue-modal";
// hooks // hooks
import { useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
// Plane-web // Plane-web
import { ISSUE_RELATION_OPTIONS } from "@/plane-web/components/relations"; import { useTimeLineRelationOptions } from "@/plane-web/components/relations";
import { TIssueRelationTypes } from "@/plane-web/types"; import { TIssueRelationTypes } from "@/plane-web/types";
// helper // helper
import { useRelationOperations } from "./helper"; import { useRelationOperations } from "./helper";
@ -63,6 +63,7 @@ export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
// derived values // derived values
const relations = getRelationsByIssueId(issueId); const relations = getRelationsByIssueId(issueId);
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
const handleIssueCrudState = (key: "update" | "delete", _issueId: string | null, issue: TIssue | null = null) => { const handleIssueCrudState = (key: "update" | "delete", _issueId: string | null, issue: TIssue | null = null) => {
setIssueCrudState({ setIssueCrudState({

View file

@ -6,7 +6,7 @@ import { CustomMenu } from "@plane/ui";
// hooks // hooks
import { useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
// Plane-web // Plane-web
import { ISSUE_RELATION_OPTIONS } from "@/plane-web/components/relations"; import { useTimeLineRelationOptions } from "@/plane-web/components/relations";
import { TIssueRelationTypes } from "@/plane-web/types"; import { TIssueRelationTypes } from "@/plane-web/types";
type Props = { type Props = {
@ -20,6 +20,8 @@ export const RelationActionButton: FC<Props> = observer((props) => {
// store hooks // store hooks
const { toggleRelationModal, setRelationKey } = useIssueDetail(); const { toggleRelationModal, setRelationKey } = useIssueDetail();
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
// handlers // handlers
const handleOnClick = (relationKey: TIssueRelationTypes) => { const handleOnClick = (relationKey: TIssueRelationTypes) => {
setRelationKey(relationKey); setRelationKey(relationKey);
@ -37,7 +39,10 @@ export const RelationActionButton: FC<Props> = observer((props) => {
maxHeight="lg" maxHeight="lg"
closeOnSelect closeOnSelect
> >
{Object.values(ISSUE_RELATION_OPTIONS).map((item, index) => ( {Object.values(ISSUE_RELATION_OPTIONS).map((item, index) => {
if (!item) return <></>;
return (
<CustomMenu.MenuItem <CustomMenu.MenuItem
key={index} key={index}
onClick={(e) => { onClick={(e) => {
@ -51,7 +56,8 @@ export const RelationActionButton: FC<Props> = observer((props) => {
<span>{item.label}</span> <span>{item.label}</span>
</div> </div>
</CustomMenu.MenuItem> </CustomMenu.MenuItem>
))} );
})}
</CustomMenu> </CustomMenu>
); );
}); });

View file

@ -6,6 +6,8 @@ import { CollapsibleButton } from "@plane/ui";
import { RelationActionButton } from "@/components/issues/issue-detail-widgets"; import { RelationActionButton } from "@/components/issues/issue-detail-widgets";
// hooks // hooks
import { useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
// Plane-web
import { useTimeLineRelationOptions } from "@/plane-web/components/relations";
type Props = { type Props = {
isOpen: boolean; isOpen: boolean;
@ -20,8 +22,9 @@ export const RelationsCollapsibleTitle: FC<Props> = observer((props) => {
relation: { getRelationCountByIssueId }, relation: { getRelationCountByIssueId },
} = useIssueDetail(); } = useIssueDetail();
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
// derived values // derived values
const relationsCount = getRelationCountByIssueId(issueId); const relationsCount = getRelationCountByIssueId(issueId, ISSUE_RELATION_OPTIONS);
// indicator element // indicator element
const indicatorElement = useMemo( const indicatorElement = useMemo(

View file

@ -3,7 +3,7 @@ import { observer } from "mobx-react";
// hooks // hooks
import { useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
// Plane-web // Plane-web
import { ISSUE_RELATION_OPTIONS } from "@/plane-web/components/relations"; import { getRelationActivityContent, useTimeLineRelationOptions } from "@/plane-web/components/relations";
import { TIssueRelationTypes } from "@/plane-web/types"; import { TIssueRelationTypes } from "@/plane-web/types";
// //
import { IssueActivityBlockComponent } from "./"; import { IssueActivityBlockComponent } from "./";
@ -18,32 +18,17 @@ export const IssueRelationActivity: FC<TIssueRelationActivity> = observer((props
} = useIssueDetail(); } = useIssueDetail();
const activity = getActivityById(activityId); const activity = getActivityById(activityId);
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
const activityContent = getRelationActivityContent(activity);
if (!activity) return <></>; if (!activity) return <></>;
return ( return (
<IssueActivityBlockComponent <IssueActivityBlockComponent
icon={activity.field ? ISSUE_RELATION_OPTIONS[activity.field as TIssueRelationTypes].icon(14) : <></>} icon={activity.field ? ISSUE_RELATION_OPTIONS[activity.field as TIssueRelationTypes]?.icon(14) : <></>}
activityId={activityId} activityId={activityId}
ends={ends} ends={ends}
> >
<> {activityContent}
{activity.field === "blocking" &&
(activity.old_value === "" ? `marked this issue is blocking issue ` : `removed the blocking issue `)}
{activity.field === "blocked_by" &&
(activity.old_value === ""
? `marked this issue is being blocked by `
: `removed this issue being blocked by issue `)}
{activity.field === "duplicate" &&
(activity.old_value === "" ? `marked this issue as duplicate of ` : `removed this issue as a duplicate of `)}
{activity.field === "relates_to" &&
(activity.old_value === "" ? `marked that this issue relates to ` : `removed the relation from `)}
{activity.old_value === "" ? (
<span className="font-medium text-custom-text-100">{activity.new_value}.</span>
) : (
<span className="font-medium text-custom-text-100">{activity.old_value}.</span>
)}
</>
</IssueActivityBlockComponent> </IssueActivityBlockComponent>
); );
}); });

View file

@ -1,9 +1,12 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// helpers
import { getValidKeysFromObject } from "@/helpers/array.helper";
// hooks // hooks
import { useIssueDetail } from "@/hooks/store"; import { useIssueDetail } from "@/hooks/store";
// plane web components // plane web components
import { IssueTypeActivity } from "@/plane-web/components/issues/issue-details"; import { IssueTypeActivity } from "@/plane-web/components/issues/issue-details";
import { useTimeLineRelationOptions } from "@/plane-web/components/relations";
// local components // local components
import { import {
IssueDefaultActivity, IssueDefaultActivity,
@ -38,6 +41,8 @@ export const IssueActivityItem: FC<TIssueActivityItem> = observer((props) => {
activity: { getActivityById }, activity: { getActivityById },
comment: {}, comment: {},
} = useIssueDetail(); } = useIssueDetail();
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
const activityRelations = getValidKeysFromObject(ISSUE_RELATION_OPTIONS);
const componentDefaultProps = { activityId, ends }; const componentDefaultProps = { activityId, ends };
@ -59,7 +64,7 @@ export const IssueActivityItem: FC<TIssueActivityItem> = observer((props) => {
return <IssueEstimateActivity {...componentDefaultProps} showIssue={false} />; return <IssueEstimateActivity {...componentDefaultProps} showIssue={false} />;
case "parent": case "parent":
return <IssueParentActivity {...componentDefaultProps} showIssue={false} />; return <IssueParentActivity {...componentDefaultProps} showIssue={false} />;
case ["blocking", "blocked_by", "duplicate", "relates_to"].find((field) => field === activityField): case activityRelations.find((field) => field === activityField):
return <IssueRelationActivity {...componentDefaultProps} />; return <IssueRelationActivity {...componentDefaultProps} />;
case "start_date": case "start_date":
return <IssueStartDateActivity {...componentDefaultProps} showIssue={false} />; return <IssueStartDateActivity {...componentDefaultProps} showIssue={false} />;

View file

@ -15,8 +15,10 @@ import { cn } from "@/helpers/common.helper";
import { useIssueDetail, useIssues, useProject } from "@/hooks/store"; import { useIssueDetail, useIssues, useProject } from "@/hooks/store";
import { usePlatformOS } from "@/hooks/use-platform-os"; import { usePlatformOS } from "@/hooks/use-platform-os";
// Plane-web // Plane-web
import { ISSUE_RELATION_OPTIONS } from "@/plane-web/components/relations"; import { useTimeLineRelationOptions } from "@/plane-web/components/relations";
import { TIssueRelationTypes } from "@/plane-web/types"; import { TIssueRelationTypes } from "@/plane-web/types";
//
import { TRelationObject } from "../issue-detail-widgets";
type TIssueRelationSelect = { type TIssueRelationSelect = {
className?: string; className?: string;
@ -41,6 +43,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
const { issueMap } = useIssues(); const { issueMap } = useIssues();
const { isMobile } = usePlatformOS(); const { isMobile } = usePlatformOS();
const relationIssueIds = getRelationByIssueIdRelationType(issueId, relationKey); const relationIssueIds = getRelationByIssueIdRelationType(issueId, relationKey);
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
const onSubmit = async (data: ISearchIssueResponse[]) => { const onSubmit = async (data: ISearchIssueResponse[]) => {
if (data.length === 0) { if (data.length === 0) {
@ -68,6 +71,8 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
const isRelationKeyModalActive = const isRelationKeyModalActive =
isRelationModalOpen?.relationType === relationKey && isRelationModalOpen?.issueId === issueId; isRelationModalOpen?.relationType === relationKey && isRelationModalOpen?.issueId === issueId;
const currRelationOption: TRelationObject | undefined = ISSUE_RELATION_OPTIONS[relationKey];
return ( return (
<> <>
<ExistingIssuesListModal <ExistingIssuesListModal
@ -106,7 +111,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
return ( return (
<div <div
key={relationIssueId} key={relationIssueId}
className={`group flex items-center gap-1 rounded px-1.5 pb-1 pt-1 leading-3 hover:bg-custom-background-90 ${ISSUE_RELATION_OPTIONS[relationKey].className}`} className={`group flex items-center gap-1 rounded px-1.5 pb-1 pt-1 leading-3 hover:bg-custom-background-90 ${currRelationOption?.className}`}
> >
<Tooltip tooltipHeading="Title" tooltipContent={currentIssue.name} isMobile={isMobile}> <Tooltip tooltipHeading="Title" tooltipContent={currentIssue.name} isMobile={isMobile}>
<Link <Link
@ -137,7 +142,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
})} })}
</div> </div>
) : ( ) : (
<span className="text-sm text-custom-text-400">{ISSUE_RELATION_OPTIONS[relationKey].placeholder}</span> <span className="text-sm text-custom-text-400">{currRelationOption?.placeholder}</span>
)} )}
{!disabled && ( {!disabled && (
<span <span

View file

@ -5,8 +5,9 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"
import { computedFn } from "mobx-utils"; import { computedFn } from "mobx-utils";
// Plane // Plane
import { TIssueRelationIdMap, TIssueRelationMap, TIssueRelation, TIssue } from "@plane/types"; import { TIssueRelationIdMap, TIssueRelationMap, TIssueRelation, TIssue } from "@plane/types";
// components
import { TRelationObject } from "@/components/issues";
// Plane-web // Plane-web
import { ISSUE_RELATION_OPTIONS } from "@/plane-web/components/relations";
import { REVERSE_RELATIONS } from "@/plane-web/constants"; import { REVERSE_RELATIONS } from "@/plane-web/constants";
import { TIssueRelationTypes } from "@/plane-web/types"; import { TIssueRelationTypes } from "@/plane-web/types";
// services // services
@ -39,7 +40,10 @@ export interface IIssueRelationStore extends IIssueRelationStoreActions {
issueRelations: TIssueRelationIdMap | undefined; issueRelations: TIssueRelationIdMap | undefined;
// helper methods // helper methods
getRelationsByIssueId: (issueId: string) => TIssueRelationIdMap | undefined; getRelationsByIssueId: (issueId: string) => TIssueRelationIdMap | undefined;
getRelationCountByIssueId: (issueId: string) => number; getRelationCountByIssueId: (
issueId: string,
ISSUE_RELATION_OPTIONS: { [key in TIssueRelationTypes]?: TRelationObject }
) => number;
getRelationByIssueIdRelationType: (issueId: string, relationType: TIssueRelationTypes) => string[] | undefined; getRelationByIssueIdRelationType: (issueId: string, relationType: TIssueRelationTypes) => string[] | undefined;
extractRelationsFromIssues: (issues: TIssue[]) => void; extractRelationsFromIssues: (issues: TIssue[]) => void;
createCurrentRelation: (issueId: string, relationType: TIssueRelationTypes, relatedIssueId: string) => Promise<void>; createCurrentRelation: (issueId: string, relationType: TIssueRelationTypes, relatedIssueId: string) => Promise<void>;
@ -85,7 +89,8 @@ export class IssueRelationStore implements IIssueRelationStore {
return this.relationMap?.[issueId] ?? undefined; return this.relationMap?.[issueId] ?? undefined;
}; };
getRelationCountByIssueId = computedFn((issueId: string) => { getRelationCountByIssueId = computedFn(
(issueId: string, ISSUE_RELATION_OPTIONS: { [key in TIssueRelationTypes]?: TRelationObject }) => {
const issueRelations = this.getRelationsByIssueId(issueId); const issueRelations = this.getRelationsByIssueId(issueId);
const issueRelationKeys = (Object.keys(issueRelations ?? {}) as TIssueRelationTypes[]).filter( const issueRelationKeys = (Object.keys(issueRelations ?? {}) as TIssueRelationTypes[]).filter(
@ -93,7 +98,8 @@ export class IssueRelationStore implements IIssueRelationStore {
); );
return issueRelationKeys.reduce((acc, curr) => acc + (issueRelations?.[curr]?.length ?? 0), 0); return issueRelationKeys.reduce((acc, curr) => acc + (issueRelations?.[curr]?.length ?? 0), 0);
}); }
);
getRelationByIssueIdRelationType = (issueId: string, relationType: TIssueRelationTypes) => { getRelationByIssueIdRelationType = (issueId: string, relationType: TIssueRelationTypes) => {
if (!issueId || !relationType) return undefined; if (!issueId || !relationType) return undefined;

View file

@ -1,3 +1,4 @@
import isEmpty from "lodash/isEmpty";
import { IIssueLabel, IIssueLabelTree } from "@plane/types"; import { IIssueLabel, IIssueLabelTree } from "@plane/types";
export const groupBy = (array: any[], key: string) => { export const groupBy = (array: any[], key: string) => {
@ -90,3 +91,14 @@ export const buildTree = (array: IIssueLabel[], parent = null) => {
return tree; return tree;
}; };
/**
* Returns Valid keys from object whose value is not falsy
* @param obj
* @returns
*/
export const getValidKeysFromObject = (obj: any) => {
if (!obj || isEmpty(obj) || typeof obj !== "object" || Array.isArray(obj)) return [];
return Object.keys(obj).filter((key) => !!obj[key]);
};