Compare commits

...
Author SHA1 Message Date
vamsikrishnamathala 97bdad00f8 Merge branch 'preview' of github.com:makeplane/plane into fix-cycle_timezone 2025-06-09 12:57:47 +05:30
vamsikrishnamathala d7fff8e200 chore: moved helper function to utils package 2025-06-09 12:55:12 +05:30
sriram veeraghanta b3b285b1e5 chore: upgrade django version to 4.2.22 2025-06-09 12:49:26 +05:30
Prateek ShouryaandGitHub 11debee402 fix: build errors related to project member list (#7185) 2025-06-09 00:31:27 +05:30
Vamsi KrishnaandGitHub 1608e4f122 [WEB-3374]feat: added merge date display (#7141)
* feat: added merge date display

* chore: moved formatter ti utils

* chore: removed unwanted props
2025-06-08 23:47:08 +05:30
edeeee1227 [WEB-4063]chore: updated work item email template (#7044)
* chore: updated work item email template

* chore: passed dynamic value for email template

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2025-06-08 23:46:12 +05:30
vamsikrishnamathala 66cfb3d8f2 fix: timezone conversion for cycles date 2025-06-06 17:19:39 +05:30
14 changed files with 312 additions and 113 deletions
@@ -284,6 +284,7 @@ def send_email_notification(
"project": str(issue.project.name),
"user_preference": f"{base_api}/profile/preferences/email",
"comments": comments,
"entity_type": "issue",
}
html_content = render_to_string(
"emails/notifications/issue-updates.html", context
+1 -1
View File
@@ -1,7 +1,7 @@
# base requirements
# django
Django==4.2.21
Django==4.2.22
# rest framework
djangorestframework==3.15.2
# postgres
@@ -3,7 +3,7 @@
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Updates on issue</title>
<title>Updates on {{entity_type}}</title>
<style type="text/css" emogrify="no"> html { font-family: system-ui; } p, h1, h2, h3, h4, ol, ul { margin: 0; } h-full { height: 100%; } a:hover { color: #3358d4 !important; } </style>
<style> *[class="gmail-fix"] { display: none !important; } </style>
<style type="text/css" emogrify="no"> @media (max-width: 600px) { .gmx-killpill { content: " \03D1"; } } </style>
@@ -37,7 +37,7 @@
{% else %}
<p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> {{summary}} <span style="font-size: 1rem; font-weight: 700; line-height: 28px"> {% if data|length > 0 %} {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name}} {% else %} {{ comments.0.actor_detail.first_name}} {{comments.0.actor_detail.last_name}} {% endif %} </span>and others. </p>
{% endif %} <!-- {% if actors_involved == 1 %} {% if data|length > 0 and comments|length == 0 %} <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> <span style="font-size: 1rem; font-weight: 700; line-height: 28px"> {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name }} </span> made {{total_updates}} {% if total_updates > 1 %}updates{% else %}update{% endif %} to the issue. </p> {% elif data|length == 0 and comments|length > 0 %} <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> <span style="font-size: 1rem; font-weight: 700; line-height: 28px"> {{ comments.0.actor_detail.first_name}} {{comments.0.actor_detail.last_name }} </span> added {{total_comments}} new {% if total_comments > 1 %}comments{% else %}comment{% endif %}. </p> {% elif data|length > 0 and comments|length > 0 %} <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> <span style="font-size: 1rem; font-weight: 700; line-height: 28px"> {{ data.0.actor_detail.first_name}} {{data.0.actor_detail.last_name }} </span> made {{total_updates}} {% if total_updates > 1 %}updates{% else %}update{% endif %} and added {{total_comments}} new {% if total_comments > 1 %}comments{% else %}comment{% endif %} on the issue. </p> {% endif %} {% else %} <p style="font-size: 1rem;color: #1f2d5c; line-height: 28px"> There are {{ total_updates }} new updates and {{total_comments}} new comments on the issue. </p> {% endif %} --> {% for update in data %} {% if update.changes.name %} <!-- Issue title updated -->
<p style="font-size: 1rem; line-height: 28px; color: #1f2d5c"> The issue title has been updated to {{ issue.name}} </p>
<p style="font-size: 1rem; line-height: 28px; color: #1f2d5c"> The {{entity_type}} title has been updated to {{ issue.name}} </p>
{% endif %} <!-- Outer update Box start --> {% if data %}
<div style=" background-color: #f7f9ff; border-radius: 8px; border-style: solid; border-width: 1px; border-color: #c1d0ff; padding: 20px; margin-top: 15px; max-width: 100%; " >
<!-- Block Heading -->
@@ -224,7 +224,7 @@
{% endif %}
</div>
<a href="{{ issue_url }}" style="text-decoration: none;">
<div style=" max-width: min-content; white-space: nowrap; background-color: #3e63dd; padding: 10px 15px; border: 1px solid #2f4ba8; border-radius: 4px; margin-top: 15px; cursor: pointer; font-size: 0.8rem; color: white; " > View issue </div>
<div style=" max-width: min-content; white-space: nowrap; background-color: #3e63dd; padding: 10px 15px; border: 1px solid #2f4ba8; border-radius: 4px; margin-top: 15px; cursor: pointer; font-size: 0.8rem; color: white; " > View {{entity_type}} </div>
</a>
</div>
<!-- Footer -->
@@ -232,7 +232,7 @@
<tr>
<td>
<div style="font-size: 0.8rem; color: #1c2024">
This email was sent to <a href="mailto:{{receiver.email}}" style="color: #3a5bc7; font-weight: 500; text-decoration: none" >{{ receiver.email }}.</a > If you'd rather not receive this kind of email, <a href="{{ issue_url }}" style="color: #3a5bc7; text-decoration: none" >you can unsubscribe to the issue</a > or <a href="{{ user_preference }}" style="color: #3a5bc7; text-decoration: none" >manage your email preferences</a >. <!-- Github | LinkedIn | Twitter -->
This email was sent to <a href="mailto:{{receiver.email}}" style="color: #3a5bc7; font-weight: 500; text-decoration: none" >{{ receiver.email }}.</a > If you'd rather not receive this kind of email, <a href="{{ issue_url }}" style="color: #3a5bc7; text-decoration: none" >you can unsubscribe to the {{entity_type}}</a > or <a href="{{ user_preference }}" style="color: #3a5bc7; text-decoration: none" >manage your email preferences</a >. <!-- Github | LinkedIn | Twitter -->
<div style="margin-top: 60px; float: right"> <a href="https://github.com/makeplane" target="_blank" style="margin-left: 10px; text-decoration: none" > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/github_32px.png" width="25" height="25" border="0" style="display: inline-block" /> </a> <a href="https://www.linkedin.com/company/planepowers/" target="_blank" style="margin-left: 10px; text-decoration: none" > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/linkedin_32px.png" width="25" height="25" border="0" style="display: inline-block" /> </a> <a href="https://twitter.com/planepowers" target="_blank" style="margin-left: 10px; text-decoration: none" > <img src="https://creative-assets.mailinblue.com/editor/social-icons/rounded_colored/twitter_32px.png" width="25" height="25" border="0" style="display: inline-block" /> </a> </div>
</div>
</td>
+2 -1
View File
@@ -85,15 +85,16 @@ export interface IProjectMemberLite {
export type TProjectMembership = {
member: string;
role: TUserPermissions | EUserProjectRoles;
created_at: string;
} & (
| {
id: string;
original_role: EUserProjectRoles;
created_at: string;
}
| {
id: null;
original_role: null;
created_at: null;
}
);
+109
View File
@@ -333,3 +333,112 @@ export const generateDateArray = (startDate: string | Date, endDate: string | Da
}
return dateArray;
};
/**
* @returns {string | null} formatted date in the format of yyyy-mm-dd to be used in payload
* @description Returns date in the formatted format to be used in payload
* @param {Date | string} date
* @example renderFormattedPayloadDate("Jan 01, 20224") // "2024-01-01"
*/
export const renderFormattedPayloadDate = (date: Date | string | undefined | null): string | undefined => {
// Parse the date to check if it is valid
const parsedDate = getDate(date);
// return if undefined
if (!parsedDate) return;
// Check if the parsed date is valid before formatting
if (!isValid(parsedDate)) return; // Return null for invalid dates
// Format the date in payload format (yyyy-mm-dd)
const formattedDate = format(parsedDate, "yyyy-MM-dd");
return formattedDate;
};
/**
* @returns {string} safely formatted date or fallback text
* @description Safely formats a date using renderFormattedPayloadDate and date-fns format, with fallback for invalid dates
* @param {Date | string | undefined | null} date
* @param {string} formatToken (optional) // default "MMM dd, yyyy"
* @param {string} fallback (optional) // default "Invalid date"
* @example renderSafeFormattedDate("2024-01-01") // "Jan 01, 2024"
* @example renderSafeFormattedDate(null) // "Invalid date"
* @example renderSafeFormattedDate("2024-01-01", "MM/dd/yyyy", "N/A") // "01/01/2024"
*/
export const renderSafeFormattedDate = (
date: Date | string | undefined | null,
formatToken: string = "MMM dd, yyyy",
fallback: string = "Invalid date"
): string => {
if (!date) return fallback;
// Use renderFormattedPayloadDate to get a properly formatted payload date
const payloadDate = renderFormattedPayloadDate(date);
// If renderFormattedPayloadDate returns undefined/null, return fallback
if (!payloadDate) return fallback;
try {
// Parse and format the payload date
const parsedDate = getDate(payloadDate);
if (!parsedDate || !isValid(parsedDate)) return fallback;
return format(parsedDate, formatToken);
} catch (error) {
// Return fallback if any error occurs during formatting
return fallback;
}
};
/*
* Formats merged date range display with smart formatting
* - Single date: "Jan 24, 2025"
* - Same year, same month: "Jan 24 - 28, 2025"
* - Same year, different month: "Jan 24 - Feb 6, 2025"
* - Different year: "Dec 28, 2024 - Jan 4, 2025"
*/
export const formatDateRange = (
parsedStartDate: Date | null | undefined,
parsedEndDate: Date | null | undefined
): string => {
// If no dates are provided
if (!parsedStartDate && !parsedEndDate) {
return "";
}
// If only start date is provided
if (parsedStartDate && !parsedEndDate) {
return format(parsedStartDate, "MMM dd, yyyy");
}
// If only end date is provided
if (!parsedStartDate && parsedEndDate) {
return format(parsedEndDate, "MMM dd, yyyy");
}
// If both dates are provided
if (parsedStartDate && parsedEndDate) {
const startYear = parsedStartDate.getFullYear();
const startMonth = parsedStartDate.getMonth();
const endYear = parsedEndDate.getFullYear();
const endMonth = parsedEndDate.getMonth();
// Same year, same month
if (startYear === endYear && startMonth === endMonth) {
const startDay = format(parsedStartDate, "dd");
const endDay = format(parsedEndDate, "dd");
return `${format(parsedStartDate, "MMM")} ${startDay} - ${endDay}, ${startYear}`;
}
// Same year, different month
if (startYear === endYear) {
const startFormatted = format(parsedStartDate, "MMM dd");
const endFormatted = format(parsedEndDate, "MMM dd");
return `${startFormatted} - ${endFormatted}, ${startYear}`;
}
// Different year
const startFormatted = format(parsedStartDate, "MMM dd, yyyy");
const endFormatted = format(parsedEndDate, "MMM dd, yyyy");
return `${startFormatted} - ${endFormatted}`;
}
return "";
};
@@ -3,21 +3,12 @@
import React, { FC, useEffect, useState } from "react";
import { observer } from "mobx-react";
import { Controller, useForm } from "react-hook-form";
import {
ArchiveIcon,
ArchiveRestoreIcon,
ArrowRight,
ChevronRight,
EllipsisIcon,
LinkIcon,
Trash2,
} from "lucide-react";
import { ArrowRight, ChevronRight } from "lucide-react";
// Plane Imports
import { CYCLE_STATUS, CYCLE_UPDATED, EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { ICycle } from "@plane/types";
import { CustomMenu, setToast, TOAST_TYPE } from "@plane/ui";
import { copyUrlToClipboard } from "@plane/utils";
import { setToast, TOAST_TYPE } from "@plane/ui";
// components
import { DateRangeDropdown } from "@/components/dropdowns";
// helpers
@@ -239,6 +230,7 @@ export const CycleSidebarHeader: FC<Props> = observer((props) => {
{renderFormattedDateInUserTimezone(cycleDetails.end_date ?? "")}
</span>
}
mergeDates
showTooltip={!!cycleDetails.start_date && !!cycleDetails.end_date} // show tooltip only if both start and end date are present
required={cycleDetails.status !== "draft"}
disabled={!isEditingAllowed || isArchived || isCompleted}
@@ -1,7 +1,6 @@
"use client";
import React, { FC, MouseEvent, useEffect, useMemo, useState } from "react";
import { format, parseISO } from "date-fns";
import { observer } from "mobx-react";
import { useParams, usePathname, useSearchParams } from "next/navigation";
import { useForm } from "react-hook-form";
@@ -17,12 +16,12 @@ import {
import { useLocalStorage } from "@plane/hooks";
import { useTranslation } from "@plane/i18n";
import { ICycle, TCycleGroups } from "@plane/types";
// ui
import { Avatar, AvatarGroup, FavoriteStar, LayersIcon, Tooltip, TransferIcon, setPromiseToast } from "@plane/ui";
// components
import { CycleQuickActions, TransferIssuesModal } from "@/components/cycles";
import { DateRangeDropdown } from "@/components/dropdowns";
import { ButtonAvatars } from "@/components/dropdowns/member/avatar";
import { MergedDateDisplay } from "@/components/dropdowns/merged-date";
import { getDate } from "@/helpers/date-time.helper";
import { getFileURL } from "@/helpers/file.helper";
// hooks
@@ -64,7 +63,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
const searchParams = useSearchParams();
const pathname = usePathname();
// store hooks
const { addCycleToFavorites, removeCycleFromFavorites, updateCycleDetails } = useCycle();
const { addCycleToFavorites, removeCycleFromFavorites } = useCycle();
const { captureEvent } = useEventTracker();
const { allowPermissions } = useUserPermissions();
@@ -77,7 +76,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
const { getUserDetails } = useMember();
// form
const { control, reset, getValues } = useForm({
const { reset } = useForm({
defaultValues,
});
@@ -230,9 +229,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
>
<div className="flex gap-1 text-xs text-custom-text-300 font-medium items-center">
<CalendarDays className="h-3 w-3 flex-shrink-0 my-auto" />
{cycleDetails.start_date && <span>{format(parseISO(cycleDetails.start_date), "MMM dd, yyyy")}</span>}
<ArrowRight className="h-3 w-3 flex-shrink-0 my-auto" />
{cycleDetails.end_date && <span>{format(parseISO(cycleDetails.end_date), "MMM dd, yyyy")}</span>}
<MergedDateDisplay startDate={cycleDetails.start_date} endDate={cycleDetails.end_date} />
</div>
</Tooltip>
{projectUTCOffset && (
@@ -269,6 +266,7 @@ export const CycleListItemAction: FC<Props> = observer((props) => {
{renderFormattedDateInUserTimezone(cycleDetails.end_date ?? "")}
</span>
}
mergeDates
required={cycleDetails.status !== "draft"}
disabled
hideIcon={{
+83 -27
View File
@@ -4,7 +4,7 @@ import React, { useEffect, useRef, useState } from "react";
import { Placement } from "@popperjs/core";
import { DateRange, Matcher } from "react-day-picker";
import { usePopper } from "react-popper";
import { ArrowRight, CalendarCheck2, CalendarDays } from "lucide-react";
import { ArrowRight, CalendarCheck2, CalendarDays, X } from "lucide-react";
import { Combobox } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
@@ -17,6 +17,7 @@ import { renderFormattedDate } from "@/helpers/date-time.helper";
import { useDropdown } from "@/hooks/use-dropdown";
// components
import { DropdownButton } from "./buttons";
import { MergedDateDisplay } from "./merged-date";
// types
import { TButtonVariants } from "./types";
@@ -30,11 +31,14 @@ type Props = {
buttonVariant: TButtonVariants;
cancelButtonText?: string;
className?: string;
clearIconClassName?: string;
disabled?: boolean;
hideIcon?: {
from?: boolean;
to?: boolean;
};
isClearable?: boolean;
mergeDates?: boolean;
minDate?: Date;
maxDate?: Date;
onSelect?: (range: DateRange | undefined) => void;
@@ -65,11 +69,14 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
buttonToDateClassName,
buttonVariant,
className,
clearIconClassName = "",
disabled = false,
hideIcon = {
from: true,
to: true,
},
isClearable = false,
mergeDates,
minDate,
maxDate,
onSelect,
@@ -118,20 +125,18 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
setIsOpen,
});
const handleClose = () => {
if (!isOpen) return;
setIsOpen(false);
setDateRange({
from: value.from,
to: value.to,
});
if (referenceElement) referenceElement.blur();
};
const disabledDays: Matcher[] = [];
if (minDate) disabledDays.push({ before: minDate });
if (maxDate) disabledDays.push({ after: maxDate });
const clearDates = () => {
const clearedRange = { from: undefined, to: undefined };
setDateRange(clearedRange);
onSelect?.(clearedRange);
};
const hasDisplayedDates = dateRange.from || dateRange.to;
useEffect(() => {
setDateRange(value);
}, [value]);
@@ -158,9 +163,9 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
tooltipContent={
customTooltipContent ?? (
<>
{dateRange.from ? renderFormattedDate(dateRange.from) : "N/A"}
{" - "}
{dateRange.to ? renderFormattedDate(dateRange.to) : "N/A"}
{dateRange.from ? renderFormattedDate(dateRange.from) : ""}
{dateRange.from && dateRange.to ? " - " : ""}
{dateRange.to ? renderFormattedDate(dateRange.to) : ""}
</>
)
}
@@ -168,19 +173,70 @@ export const DateRangeDropdown: React.FC<Props> = (props) => {
variant={buttonVariant}
renderToolTipByDefault={renderByDefault}
>
<span
className={cn("h-full flex items-center justify-center gap-1 rounded-sm flex-grow", buttonFromDateClassName)}
>
{!hideIcon.from && <CalendarDays className="h-3 w-3 flex-shrink-0" />}
{dateRange.from ? renderFormattedDate(dateRange.from) : renderPlaceholder ? placeholder.from : ""}
</span>
<ArrowRight className="h-3 w-3 flex-shrink-0" />
<span
className={cn("h-full flex items-center justify-center gap-1 rounded-sm flex-grow", buttonToDateClassName)}
>
{!hideIcon.to && <CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
{dateRange.to ? renderFormattedDate(dateRange.to) : renderPlaceholder ? placeholder.to : ""}
</span>
{mergeDates ? (
// Merged date display
<div className="flex items-center gap-1.5 w-full">
{!hideIcon.from && <CalendarDays className="h-3 w-3 flex-shrink-0" />}
{dateRange.from || dateRange.to ? (
<MergedDateDisplay
startDate={dateRange.from}
endDate={dateRange.to}
className="flex-grow truncate text-xs"
/>
) : (
renderPlaceholder && (
<>
<span className="text-custom-text-400">{placeholder.from}</span>
<ArrowRight className="h-3 w-3 flex-shrink-0 text-custom-text-400" />
<span className="text-custom-text-400">{placeholder.to}</span>
</>
)
)}
{isClearable && !disabled && hasDisplayedDates && (
<X
className={cn("h-2.5 w-2.5 flex-shrink-0 cursor-pointer", clearIconClassName)}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
clearDates();
}}
/>
)}
</div>
) : (
// Original separate date display
<>
<span
className={cn(
"h-full flex items-center justify-center gap-1 rounded-sm flex-grow",
buttonFromDateClassName
)}
>
{!hideIcon.from && <CalendarDays className="h-3 w-3 flex-shrink-0" />}
{dateRange.from ? renderFormattedDate(dateRange.from) : renderPlaceholder ? placeholder.from : ""}
</span>
<ArrowRight className="h-3 w-3 flex-shrink-0" />
<span
className={cn(
"h-full flex items-center justify-center gap-1 rounded-sm flex-grow",
buttonToDateClassName
)}
>
{!hideIcon.to && <CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
{dateRange.to ? renderFormattedDate(dateRange.to) : renderPlaceholder ? placeholder.to : ""}
</span>
{isClearable && !disabled && hasDisplayedDates && (
<X
className={cn("h-2.5 w-2.5 flex-shrink-0 cursor-pointer ml-1", clearIconClassName)}
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
clearDates();
}}
/>
)}
</>
)}
</DropdownButton>
</button>
);
+1
View File
@@ -3,6 +3,7 @@ export * from "./cycle";
export * from "./date-range";
export * from "./date";
export * from "./estimate";
export * from "./merged-date";
export * from "./module";
export * from "./priority";
export * from "./project";
@@ -0,0 +1,34 @@
import React from "react";
import { observer } from "mobx-react";
// helpers
import { formatDateRange } from "@plane/utils";
import { getDate } from "@/helpers/date-time.helper";
type Props = {
startDate: Date | string | null | undefined;
endDate: Date | string | null | undefined;
className?: string;
};
/**
* Formats merged date range display with smart formatting
* - Single date: "Jan 24, 2025"
* - Same year, same month: "Jan 24 - 28, 2025"
* - Same year, different month: "Jan 24 - Feb 6, 2025"
* - Different year: "Dec 28, 2024 - Jan 4, 2025"
*/
export const MergedDateDisplay: React.FC<Props> = observer((props) => {
const { startDate, endDate, className = "" } = props;
// Parse dates
const parsedStartDate = getDate(startDate);
const parsedEndDate = getDate(endDate);
const displayText = formatDateRange(parsedStartDate, parsedEndDate);
if (!displayText) {
return null;
}
return <span className={className}>{displayText}</span>;
});
@@ -1,11 +1,10 @@
// plane imports
import { SyntheticEvent } from "react";
import { observer } from "mobx-react";
import { CalendarClock } from "lucide-react";
import { useTranslation } from "@plane/i18n";
import { IIssueDisplayProperties, TIssue } from "@plane/types";
// components
import { PriorityDropdown, MemberDropdown, StateDropdown, DateDropdown } from "@/components/dropdowns";
import { PriorityDropdown, MemberDropdown, StateDropdown, DateRangeDropdown } from "@/components/dropdowns";
// hooks
import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/properties/with-display-properties-HOC";
import { getDate, renderFormattedPayloadDate } from "@/helpers/date-time.helper";
@@ -37,6 +36,22 @@ export const SubIssuesListItemProperties: React.FC<Props> = observer((props) =>
e.preventDefault();
};
const handleStartDate = (date: Date | null) => {
if (issue.project_id) {
updateSubIssue(workspaceSlug, issue.project_id, parentIssueId, issueId, {
start_date: date ? renderFormattedPayloadDate(date) : null,
});
}
};
const handleTargetDate = (date: Date | null) => {
if (issue.project_id) {
updateSubIssue(workspaceSlug, issue.project_id, parentIssueId, issueId, {
target_date: date ? renderFormattedPayloadDate(date) : null,
});
}
};
if (!displayProperties) return <></>;
const maxDate = getDate(issue.target_date);
@@ -88,29 +103,32 @@ export const SubIssuesListItemProperties: React.FC<Props> = observer((props) =>
</div>
</WithDisplayPropertiesHOC>
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="due_date">
<div className="h-5 flex-shrink-0" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<DateDropdown
value={issue.target_date ?? null}
onChange={(val) =>
issue.project_id &&
updateSubIssue(
workspaceSlug,
issue.project_id,
parentIssueId,
issueId,
{
target_date: val ? renderFormattedPayloadDate(val) : null,
},
{ ...issue }
)
}
maxDate={maxDate}
placeholder={t("common.order_by.due_date")}
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
optionsClassName="z-30"
{/* merged dates */}
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey={["start_date", "due_date"]}
shouldRenderProperty={(properties) => !!(properties.start_date || properties.due_date)}
>
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<DateRangeDropdown
value={{
from: getDate(issue.start_date) || undefined,
to: getDate(issue.target_date) || undefined,
}}
onSelect={(range) => {
handleStartDate(range?.from ?? null);
handleTargetDate(range?.to ?? null);
}}
hideIcon={{
from: false,
}}
isClearable
mergeDates
buttonVariant={issue.start_date || issue.target_date ? "border-with-text" : "border-without-text"}
disabled={!disabled}
showTooltip
customTooltipHeading="Date Range"
renderPlaceholder={false}
/>
</div>
</WithDisplayPropertiesHOC>
@@ -5,7 +5,7 @@ import xor from "lodash/xor";
import { observer } from "mobx-react";
import { useParams, usePathname } from "next/navigation";
// icons
import { CalendarCheck2, CalendarClock, Layers, Link, Paperclip } from "lucide-react";
import { Layers, Link, Paperclip } from "lucide-react";
// types
import { ISSUE_UPDATED } from "@plane/constants";
// i18n
@@ -15,13 +15,13 @@ import { TIssue, IIssueDisplayProperties, TIssuePriorities } from "@plane/types"
import { Tooltip } from "@plane/ui";
// components
import {
DateDropdown,
EstimateDropdown,
PriorityDropdown,
MemberDropdown,
ModuleDropdown,
CycleDropdown,
StateDropdown,
DateRangeDropdown,
} from "@/components/dropdowns";
// constants
// helpers
@@ -265,12 +265,6 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
const defaultLabelOptions = issue?.label_ids?.map((id) => labelMap[id]) || [];
const minDate = getDate(issue.start_date);
minDate?.setDate(minDate.getDate());
const maxDate = getDate(issue.target_date);
maxDate?.setDate(maxDate.getDate());
const handleEventPropagation = (e: SyntheticEvent<HTMLDivElement>) => {
e.stopPropagation();
e.preventDefault();
@@ -310,40 +304,34 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
</div>
</WithDisplayPropertiesHOC>
{/* start date */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="start_date">
{/* merged dates */}
<WithDisplayPropertiesHOC
displayProperties={displayProperties}
displayPropertyKey={["start_date", "due_date"]}
shouldRenderProperty={(properties) => !!(properties.start_date || properties.due_date)}
>
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<DateDropdown
value={issue.start_date ?? null}
onChange={handleStartDate}
maxDate={maxDate}
placeholder={t("common.order_by.start_date")}
icon={<CalendarClock className="h-3 w-3 flex-shrink-0" />}
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
optionsClassName="z-10"
disabled={isReadOnly}
renderByDefault={isMobile}
showTooltip
/>
</div>
</WithDisplayPropertiesHOC>
{/* target/due date */}
<WithDisplayPropertiesHOC displayProperties={displayProperties} displayPropertyKey="due_date">
<div className="h-5" onFocus={handleEventPropagation} onClick={handleEventPropagation}>
<DateDropdown
value={issue?.target_date ?? null}
onChange={handleTargetDate}
minDate={minDate}
placeholder={t("common.order_by.due_date")}
icon={<CalendarCheck2 className="h-3 w-3 flex-shrink-0" />}
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
<DateRangeDropdown
value={{
from: getDate(issue.start_date) || undefined,
to: getDate(issue.target_date) || undefined,
}}
onSelect={(range) => {
handleStartDate(range?.from ?? null);
handleTargetDate(range?.to ?? null);
}}
hideIcon={{
from: false,
}}
isClearable
mergeDates
buttonVariant={issue.start_date || issue.target_date ? "border-with-text" : "border-without-text"}
buttonClassName={shouldHighlightIssueDueDate(issue.target_date, stateDetails?.group) ? "text-red-500" : ""}
clearIconClassName="!text-custom-text-100"
optionsClassName="z-10"
disabled={isReadOnly}
renderByDefault={isMobile}
showTooltip
renderPlaceholder={false}
customTooltipHeading="Date Range"
/>
</div>
</WithDisplayPropertiesHOC>
@@ -158,6 +158,7 @@ export const ModuleListItemAction: FC<Props> = observer((props) => {
target_date: val?.to ? renderFormattedPayloadDate(val.to) : null,
});
}}
mergeDates
placeholder={{
from: t("start_date"),
to: t("end_date"),
@@ -177,7 +177,7 @@ export abstract class BaseProjectMemberStore implements IBaseProjectMemberStore
original_role: projectMember.original_role,
member: {
...userDetails,
joining_date: projectMember.created_at,
joining_date: projectMember.created_at ?? undefined,
},
created_at: projectMember.created_at,
};