[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"),
|
||||
)
|
||||
|
||||
if relation_type == "blocking":
|
||||
if relation_type in ["blocking", "start_after", "finish_after"]:
|
||||
return Response(
|
||||
RelatedIssueSerializer(issue_relation, many=True).data,
|
||||
status=status.HTTP_201_CREATED,
|
||||
|
|
@ -333,7 +333,7 @@ class IssueRelationViewSet(BaseViewSet):
|
|||
relation_type = request.data.get("relation_type", 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(
|
||||
workspace__slug=slug,
|
||||
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 { TIssueRelationTypes } from "../../types";
|
||||
|
||||
export * from "./activity";
|
||||
|
||||
export const ISSUE_RELATION_OPTIONS: Record<TIssueRelationTypes, TRelationObject> = {
|
||||
relates_to: {
|
||||
key: "relates_to",
|
||||
|
|
@ -33,3 +35,5 @@ export const ISSUE_RELATION_OPTIONS: Record<TIssueRelationTypes, TRelationObject
|
|||
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";
|
||||
// types
|
||||
import { BLOCK_HEIGHT } from "../constants";
|
||||
import { IBlockUpdateData } from "../types";
|
||||
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||
import { BlockRow } from "./block-row";
|
||||
|
||||
export type GanttChartBlocksProps = {
|
||||
blockIds: string[];
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
handleScrollToBlock: (block: IGanttBlock) => void;
|
||||
enableAddBlock: boolean | ((blockId: string) => boolean);
|
||||
showAllBlocks: boolean;
|
||||
selectionHelpers: TSelectionHelper;
|
||||
|
|
@ -18,7 +19,15 @@ export type GanttChartBlocksProps = {
|
|||
};
|
||||
|
||||
export const GanttChartRowList: FC<GanttChartBlocksProps> = (props) => {
|
||||
const { blockIds, blockUpdateHandler, enableAddBlock, showAllBlocks, selectionHelpers, ganttContainerRef } = props;
|
||||
const {
|
||||
blockIds,
|
||||
blockUpdateHandler,
|
||||
handleScrollToBlock,
|
||||
enableAddBlock,
|
||||
showAllBlocks,
|
||||
selectionHelpers,
|
||||
ganttContainerRef,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="absolute top-0 left-0 min-w-full w-max">
|
||||
|
|
@ -37,6 +46,7 @@ export const GanttChartRowList: FC<GanttChartBlocksProps> = (props) => {
|
|||
blockId={blockId}
|
||||
showAllBlocks={showAllBlocks}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
handleScrollToBlock={handleScrollToBlock}
|
||||
enableAddBlock={typeof enableAddBlock === "function" ? enableAddBlock(blockId) : enableAddBlock}
|
||||
selectionHelpers={selectionHelpers}
|
||||
ganttContainerRef={ganttContainerRef}
|
||||
|
|
|
|||
|
|
@ -10,19 +10,20 @@ import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
|||
//
|
||||
import { BLOCK_HEIGHT, SIDEBAR_WIDTH } from "../constants";
|
||||
import { ChartAddBlock } from "../helpers";
|
||||
import { IBlockUpdateData } from "../types";
|
||||
import { IBlockUpdateData, IGanttBlock } from "../types";
|
||||
|
||||
type Props = {
|
||||
blockId: string;
|
||||
showAllBlocks: boolean;
|
||||
blockUpdateHandler: (block: any, payload: IBlockUpdateData) => void;
|
||||
handleScrollToBlock: (block: IGanttBlock) => void;
|
||||
enableAddBlock: boolean;
|
||||
selectionHelpers: TSelectionHelper;
|
||||
ganttContainerRef: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export const BlockRow: React.FC<Props> = observer((props) => {
|
||||
const { blockId, showAllBlocks, blockUpdateHandler, enableAddBlock, selectionHelpers } = props;
|
||||
const { blockId, showAllBlocks, blockUpdateHandler, handleScrollToBlock, enableAddBlock, selectionHelpers } = props;
|
||||
// states
|
||||
const [isHidden, setIsHidden] = 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
|
||||
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 isBlockSelected = selectionHelpers.getIsEntitySelected(block.id);
|
||||
const isBlockFocused = selectionHelpers.getIsEntityActive(block.id);
|
||||
|
|
@ -106,7 +99,7 @@ export const BlockRow: React.FC<Props> = observer((props) => {
|
|||
style={{
|
||||
left: `${SIDEBAR_WIDTH + 4}px`,
|
||||
}}
|
||||
onClick={handleScrollToBlock}
|
||||
onClick={() => handleScrollToBlock(block)}
|
||||
>
|
||||
<ArrowRight
|
||||
className={cn("h-3.5 w-3.5", {
|
||||
|
|
|
|||
|
|
@ -5,10 +5,12 @@ import { observer } from "mobx-react";
|
|||
// components
|
||||
import { MultipleSelectGroup } from "@/components/core";
|
||||
import {
|
||||
ChartDataType,
|
||||
GanttChartBlocksList,
|
||||
GanttChartSidebar,
|
||||
IBlockUpdateData,
|
||||
IBlockUpdateDependencyData,
|
||||
IGanttBlock,
|
||||
MonthChartView,
|
||||
QuarterChartView,
|
||||
TGanttViews,
|
||||
|
|
@ -16,6 +18,7 @@ import {
|
|||
} from "@/components/gantt-chart";
|
||||
// helpers
|
||||
import { cn } from "@/helpers/common.helper";
|
||||
import { getDate } from "@/helpers/date-time.helper";
|
||||
// hooks
|
||||
import { useTimeLineChartStore } from "@/hooks/use-timeline-chart";
|
||||
// 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 { GANTT_SELECT_GROUP, HEADER_HEIGHT } from "../constants";
|
||||
import { getItemPositionWidth } from "../views";
|
||||
import { TimelineDragHelper } from "./timeline-drag-helper";
|
||||
|
||||
type Props = {
|
||||
|
|
@ -46,7 +50,11 @@ type Props = {
|
|||
showAllBlocks: boolean;
|
||||
sidebarToRender: (props: any) => React.ReactNode;
|
||||
title: string;
|
||||
updateCurrentViewRenderPayload: (direction: "left" | "right", currentView: TGanttViews) => void;
|
||||
updateCurrentViewRenderPayload: (
|
||||
direction: "left" | "right",
|
||||
currentView: TGanttViews,
|
||||
targetDate?: Date
|
||||
) => ChartDataType | undefined;
|
||||
quickAdd?: React.JSX.Element | undefined;
|
||||
};
|
||||
|
||||
|
|
@ -105,6 +113,26 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
|||
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: {
|
||||
[key in TGanttViews]: React.FC;
|
||||
} = {
|
||||
|
|
@ -166,6 +194,7 @@ export const GanttChartMainContent: React.FC<Props> = observer((props) => {
|
|||
<GanttChartRowList
|
||||
blockIds={blockIds}
|
||||
blockUpdateHandler={blockUpdateHandler}
|
||||
handleScrollToBlock={handleScrollToBlock}
|
||||
enableAddBlock={enableAddBlock}
|
||||
showAllBlocks={showAllBlocks}
|
||||
selectionHelpers={helpers}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
|||
updateAllBlocksOnChartChangeWhileDragging,
|
||||
} = useTimeLineChartStore();
|
||||
|
||||
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews) => {
|
||||
const updateCurrentViewRenderPayload = (side: null | "left" | "right", view: TGanttViews, targetDate?: Date) => {
|
||||
const selectedCurrentView: TGanttViews = view;
|
||||
const selectedCurrentViewData: ChartDataType | undefined =
|
||||
selectedCurrentView && selectedCurrentView === currentViewData?.key
|
||||
|
|
@ -96,7 +96,7 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
|||
if (selectedCurrentViewData === undefined) return;
|
||||
|
||||
const currentViewHelpers = timelineViewHelpers[selectedCurrentView];
|
||||
const currentRender = currentViewHelpers.generateChart(selectedCurrentViewData, side);
|
||||
const currentRender = currentViewHelpers.generateChart(selectedCurrentViewData, side, targetDate);
|
||||
const mergeRenderPayloads = currentViewHelpers.mergeRenderPayloads as (
|
||||
a: IWeekBlock[] | IMonthView | IMonthBlock[],
|
||||
b: IWeekBlock[] | IMonthView | IMonthBlock[]
|
||||
|
|
@ -109,7 +109,8 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
|||
if (side === "left") {
|
||||
updateCurrentView(selectedCurrentView);
|
||||
updateRenderView(mergeRenderPayloads(currentRender.payload, renderView));
|
||||
updatingCurrentLeftScrollPosition(currentRender.scrollWidth);
|
||||
updateItemsContainerWidth(currentRender.scrollWidth);
|
||||
if (!targetDate) updateCurrentLeftScrollPosition(currentRender.scrollWidth);
|
||||
updateAllBlocksOnChartChangeWhileDragging(currentRender.scrollWidth);
|
||||
setItemsContainerWidth(itemsContainerWidth + currentRender.scrollWidth);
|
||||
} else if (side === "right") {
|
||||
|
|
@ -125,6 +126,8 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
|||
}, 50);
|
||||
}
|
||||
}
|
||||
|
||||
return currentRender.state;
|
||||
};
|
||||
|
||||
const handleToday = () => updateCurrentViewRenderPayload(null, currentView);
|
||||
|
|
@ -135,12 +138,17 @@ export const ChartViewRoot: FC<ChartViewRootProps> = observer((props) => {
|
|||
// 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;
|
||||
if (!scrollContainer) return;
|
||||
|
||||
scrollContainer.scrollLeft = width + scrollContainer?.scrollLeft;
|
||||
setItemsContainerWidth(width + scrollContainer?.scrollLeft);
|
||||
};
|
||||
|
||||
const handleScrollToCurrentSelectedDate = (currentState: ChartDataType, date: Date) => {
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export const MonthChartView: FC<any> = observer(() => {
|
|||
className={cn(
|
||||
"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` }}
|
||||
|
|
@ -92,7 +92,7 @@ export const MonthChartView: FC<any> = observer(() => {
|
|||
<div
|
||||
key={`column-${weekBlock.startDate}-${weekBlock.endDate}`}
|
||||
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` }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export const QuarterChartView: FC<any> = observer(() => {
|
|||
className={cn(
|
||||
"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` }}
|
||||
|
|
@ -80,7 +80,7 @@ export const QuarterChartView: FC<any> = observer(() => {
|
|||
<div
|
||||
key={`column-${rootIndex}-${index}`}
|
||||
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` }}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export const WeekChartView: FC<any> = observer(() => {
|
|||
className={cn(
|
||||
"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` }}
|
||||
|
|
@ -71,12 +71,12 @@ export const WeekChartView: FC<any> = observer(() => {
|
|||
</div>
|
||||
</div>
|
||||
{/** 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) => (
|
||||
<div
|
||||
key={`column-${rootIndex}-${index}`}
|
||||
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` }}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export interface IMonthView {
|
|||
* @param side
|
||||
* @returns
|
||||
*/
|
||||
const generateMonthChart = (monthPayload: ChartDataType, side: null | "left" | "right") => {
|
||||
const generateMonthChart = (monthPayload: ChartDataType, side: null | "left" | "right", targetDate?: Date) => {
|
||||
let renderState = cloneDeep(monthPayload);
|
||||
|
||||
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
|
||||
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());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1);
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1);
|
||||
plusDate = new Date(chartStartDate.getFullYear(), chartStartDate.getMonth(), chartStartDate.getDate() - 1);
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getMonthsViewBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
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,
|
||||
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
|
||||
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);
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate());
|
||||
minusDate = new Date(chartEndDate.getFullYear(), chartEndDate.getMonth(), chartEndDate.getDate() + 1);
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 1);
|
||||
|
||||
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;
|
||||
renderState = {
|
||||
...renderState,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export interface IQuarterMonthBlock {
|
|||
* @param side
|
||||
* @returns
|
||||
*/
|
||||
const generateQuarterChart = (quarterPayload: ChartDataType, side: null | "left" | "right") => {
|
||||
const generateQuarterChart = (quarterPayload: ChartDataType, side: null | "left" | "right", targetDate?: Date) => {
|
||||
let renderState = quarterPayload;
|
||||
|
||||
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
|
||||
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);
|
||||
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);
|
||||
|
||||
const startMonthBlock = filteredDates[0];
|
||||
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,
|
||||
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
|
||||
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);
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getMonthsBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
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);
|
||||
renderState = {
|
||||
...renderState,
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export interface IWeekBlock {
|
|||
* @param side
|
||||
* @returns
|
||||
*/
|
||||
const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "right") => {
|
||||
const generateWeekChart = (weekPayload: ChartDataType, side: null | "left" | "right", targetDate?: Date) => {
|
||||
let renderState = weekPayload;
|
||||
|
||||
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
|
||||
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());
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth(), currentDate.getDate() - 1);
|
||||
minusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() - range, 1);
|
||||
plusDate = new Date(chartStartDate.getFullYear(), chartStartDate.getMonth(), chartStartDate.getDate() - 1);
|
||||
|
||||
if (minusDate && plusDate) filteredDates = getWeeksBetweenTwoDates(minusDate, plusDate);
|
||||
|
||||
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,
|
||||
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
|
||||
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);
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, currentDate.getDate());
|
||||
minusDate = new Date(chartEndDate.getFullYear(), chartEndDate.getMonth(), chartEndDate.getDate() + 1);
|
||||
plusDate = new Date(currentDate.getFullYear(), currentDate.getMonth() + range, 1);
|
||||
|
||||
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;
|
||||
renderState = {
|
||||
...renderState,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import {
|
|||
} from "@/components/issues/issue-detail-widgets";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// Plane-web
|
||||
import { useTimeLineRelationOptions } from "@/plane-web/components/relations";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
|
|
@ -31,7 +33,8 @@ export const IssueDetailWidgetCollapsibles: FC<Props> = observer((props) => {
|
|||
// derived values
|
||||
const issue = getIssueById(issueId);
|
||||
const subIssues = subIssuesByIssueId(issueId);
|
||||
const issueRelationsCount = getRelationCountByIssueId(issueId);
|
||||
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
|
||||
const issueRelationsCount = getRelationCountByIssueId(issueId, ISSUE_RELATION_OPTIONS);
|
||||
|
||||
// render conditions
|
||||
const shouldRenderSubIssues = !!subIssues && subIssues.length > 0;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { CreateUpdateIssueModal } from "@/components/issues/issue-modal";
|
|||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// Plane-web
|
||||
import { ISSUE_RELATION_OPTIONS } from "@/plane-web/components/relations";
|
||||
import { useTimeLineRelationOptions } from "@/plane-web/components/relations";
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
// helper
|
||||
import { useRelationOperations } from "./helper";
|
||||
|
|
@ -63,6 +63,7 @@ export const RelationsCollapsibleContent: FC<Props> = observer((props) => {
|
|||
|
||||
// derived values
|
||||
const relations = getRelationsByIssueId(issueId);
|
||||
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
|
||||
|
||||
const handleIssueCrudState = (key: "update" | "delete", _issueId: string | null, issue: TIssue | null = null) => {
|
||||
setIssueCrudState({
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { CustomMenu } from "@plane/ui";
|
|||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// Plane-web
|
||||
import { ISSUE_RELATION_OPTIONS } from "@/plane-web/components/relations";
|
||||
import { useTimeLineRelationOptions } from "@/plane-web/components/relations";
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
|
||||
type Props = {
|
||||
|
|
@ -20,6 +20,8 @@ export const RelationActionButton: FC<Props> = observer((props) => {
|
|||
// store hooks
|
||||
const { toggleRelationModal, setRelationKey } = useIssueDetail();
|
||||
|
||||
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
|
||||
|
||||
// handlers
|
||||
const handleOnClick = (relationKey: TIssueRelationTypes) => {
|
||||
setRelationKey(relationKey);
|
||||
|
|
@ -37,7 +39,10 @@ export const RelationActionButton: FC<Props> = observer((props) => {
|
|||
maxHeight="lg"
|
||||
closeOnSelect
|
||||
>
|
||||
{Object.values(ISSUE_RELATION_OPTIONS).map((item, index) => (
|
||||
{Object.values(ISSUE_RELATION_OPTIONS).map((item, index) => {
|
||||
if (!item) return <></>;
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={index}
|
||||
onClick={(e) => {
|
||||
|
|
@ -51,7 +56,8 @@ export const RelationActionButton: FC<Props> = observer((props) => {
|
|||
<span>{item.label}</span>
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import { CollapsibleButton } from "@plane/ui";
|
|||
import { RelationActionButton } from "@/components/issues/issue-detail-widgets";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// Plane-web
|
||||
import { useTimeLineRelationOptions } from "@/plane-web/components/relations";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
|
|
@ -20,8 +22,9 @@ export const RelationsCollapsibleTitle: FC<Props> = observer((props) => {
|
|||
relation: { getRelationCountByIssueId },
|
||||
} = useIssueDetail();
|
||||
|
||||
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
|
||||
// derived values
|
||||
const relationsCount = getRelationCountByIssueId(issueId);
|
||||
const relationsCount = getRelationCountByIssueId(issueId, ISSUE_RELATION_OPTIONS);
|
||||
|
||||
// indicator element
|
||||
const indicatorElement = useMemo(
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { observer } from "mobx-react";
|
|||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// 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 { IssueActivityBlockComponent } from "./";
|
||||
|
|
@ -18,32 +18,17 @@ export const IssueRelationActivity: FC<TIssueRelationActivity> = observer((props
|
|||
} = useIssueDetail();
|
||||
|
||||
const activity = getActivityById(activityId);
|
||||
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
|
||||
const activityContent = getRelationActivityContent(activity);
|
||||
|
||||
if (!activity) return <></>;
|
||||
return (
|
||||
<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}
|
||||
ends={ends}
|
||||
>
|
||||
<>
|
||||
{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>
|
||||
)}
|
||||
</>
|
||||
{activityContent}
|
||||
</IssueActivityBlockComponent>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// helpers
|
||||
import { getValidKeysFromObject } from "@/helpers/array.helper";
|
||||
// hooks
|
||||
import { useIssueDetail } from "@/hooks/store";
|
||||
// plane web components
|
||||
import { IssueTypeActivity } from "@/plane-web/components/issues/issue-details";
|
||||
import { useTimeLineRelationOptions } from "@/plane-web/components/relations";
|
||||
// local components
|
||||
import {
|
||||
IssueDefaultActivity,
|
||||
|
|
@ -38,6 +41,8 @@ export const IssueActivityItem: FC<TIssueActivityItem> = observer((props) => {
|
|||
activity: { getActivityById },
|
||||
comment: {},
|
||||
} = useIssueDetail();
|
||||
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
|
||||
const activityRelations = getValidKeysFromObject(ISSUE_RELATION_OPTIONS);
|
||||
|
||||
const componentDefaultProps = { activityId, ends };
|
||||
|
||||
|
|
@ -59,7 +64,7 @@ export const IssueActivityItem: FC<TIssueActivityItem> = observer((props) => {
|
|||
return <IssueEstimateActivity {...componentDefaultProps} showIssue={false} />;
|
||||
case "parent":
|
||||
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} />;
|
||||
case "start_date":
|
||||
return <IssueStartDateActivity {...componentDefaultProps} showIssue={false} />;
|
||||
|
|
|
|||
|
|
@ -15,8 +15,10 @@ import { cn } from "@/helpers/common.helper";
|
|||
import { useIssueDetail, useIssues, useProject } from "@/hooks/store";
|
||||
import { usePlatformOS } from "@/hooks/use-platform-os";
|
||||
// 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 { TRelationObject } from "../issue-detail-widgets";
|
||||
|
||||
type TIssueRelationSelect = {
|
||||
className?: string;
|
||||
|
|
@ -41,6 +43,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
|||
const { issueMap } = useIssues();
|
||||
const { isMobile } = usePlatformOS();
|
||||
const relationIssueIds = getRelationByIssueIdRelationType(issueId, relationKey);
|
||||
const ISSUE_RELATION_OPTIONS = useTimeLineRelationOptions();
|
||||
|
||||
const onSubmit = async (data: ISearchIssueResponse[]) => {
|
||||
if (data.length === 0) {
|
||||
|
|
@ -68,6 +71,8 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
|||
const isRelationKeyModalActive =
|
||||
isRelationModalOpen?.relationType === relationKey && isRelationModalOpen?.issueId === issueId;
|
||||
|
||||
const currRelationOption: TRelationObject | undefined = ISSUE_RELATION_OPTIONS[relationKey];
|
||||
|
||||
return (
|
||||
<>
|
||||
<ExistingIssuesListModal
|
||||
|
|
@ -106,7 +111,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
|||
return (
|
||||
<div
|
||||
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}>
|
||||
<Link
|
||||
|
|
@ -137,7 +142,7 @@ export const IssueRelationSelect: React.FC<TIssueRelationSelect> = observer((pro
|
|||
})}
|
||||
</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 && (
|
||||
<span
|
||||
|
|
|
|||
|
|
@ -5,8 +5,9 @@ import { action, computed, makeObservable, observable, runInAction } from "mobx"
|
|||
import { computedFn } from "mobx-utils";
|
||||
// Plane
|
||||
import { TIssueRelationIdMap, TIssueRelationMap, TIssueRelation, TIssue } from "@plane/types";
|
||||
// components
|
||||
import { TRelationObject } from "@/components/issues";
|
||||
// Plane-web
|
||||
import { ISSUE_RELATION_OPTIONS } from "@/plane-web/components/relations";
|
||||
import { REVERSE_RELATIONS } from "@/plane-web/constants";
|
||||
import { TIssueRelationTypes } from "@/plane-web/types";
|
||||
// services
|
||||
|
|
@ -39,7 +40,10 @@ export interface IIssueRelationStore extends IIssueRelationStoreActions {
|
|||
issueRelations: TIssueRelationIdMap | undefined;
|
||||
// helper methods
|
||||
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;
|
||||
extractRelationsFromIssues: (issues: TIssue[]) => void;
|
||||
createCurrentRelation: (issueId: string, relationType: TIssueRelationTypes, relatedIssueId: string) => Promise<void>;
|
||||
|
|
@ -85,7 +89,8 @@ export class IssueRelationStore implements IIssueRelationStore {
|
|||
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 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);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
getRelationByIssueIdRelationType = (issueId: string, relationType: TIssueRelationTypes) => {
|
||||
if (!issueId || !relationType) return undefined;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import isEmpty from "lodash/isEmpty";
|
||||
import { IIssueLabel, IIssueLabelTree } from "@plane/types";
|
||||
|
||||
export const groupBy = (array: any[], key: string) => {
|
||||
|
|
@ -90,3 +91,14 @@ export const buildTree = (array: IIssueLabel[], parent = null) => {
|
|||
|
||||
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