[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:
parent
a1bfde6af9
commit
71589f93ca
22 changed files with 201 additions and 105 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
20
web/ce/components/relations/activity.ts
Normal file
20
web/ce/components/relations/activity.ts
Normal 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;
|
||||||
|
};
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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", {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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) => {
|
||||||
|
|
|
||||||
|
|
@ -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` }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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` }}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -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` }}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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} />;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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]);
|
||||||
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue