Merge pull request #439 from makeplane/sync/ce-ee

sync: community changes
This commit is contained in:
pushya22
2024-06-18 20:52:41 +05:30
committed by GitHub
7 changed files with 43 additions and 146 deletions

View File

@@ -81,7 +81,7 @@ export type TInboxDuplicateIssueDetails = {
export type TInboxIssue = {
id: string;
status: TInboxIssueStatus;
snoozed_till: Date | undefined;
snoozed_till: Date | null;
duplicate_to: string | undefined;
source: string;
issue: TIssue;

View File

@@ -1,6 +1,5 @@
export * from "./preview";
export * from "./create";
export * from "./update";
export * from "./select-dropdown";
export * from "./create-root";

View File

@@ -1,129 +0,0 @@
"use client";
import { FC, useRef, Fragment, useState } from "react";
import { Info, Check, ChevronDown } from "lucide-react";
import { Listbox, Transition } from "@headlessui/react";
import { TEstimatePointsObject } from "@plane/types";
import { Tooltip } from "@plane/ui";
// helpers
import { cn } from "@/helpers/common.helper";
// hooks
import useDynamicDropdownPosition from "@/hooks/use-dynamic-dropdown";
import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
type TEstimatePointDropdown = {
options: TEstimatePointsObject[];
error: string | undefined;
callback: (estimateId: string) => void;
};
export const EstimatePointDropdown: FC<TEstimatePointDropdown> = (props) => {
const { options, error, callback } = props;
// states
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState<string | undefined>(undefined);
// ref
const dropdownContainerRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const dropdownRef = useRef<HTMLDivElement>(null);
useDynamicDropdownPosition(isDropdownOpen, () => setIsDropdownOpen(false), buttonRef, dropdownRef);
useOutsideClickDetector(dropdownContainerRef, () => setIsDropdownOpen(false));
// derived values
const selectedValue = selectedOption
? selectedOption === "none"
? {
id: undefined,
key: undefined,
value: "None",
}
: options.find((option) => option?.id === selectedOption)
: undefined;
return (
<div ref={dropdownContainerRef} className="w-full relative">
<Listbox
as="div"
value={selectedOption}
onChange={(selectedOption) => {
setSelectedOption(selectedOption);
callback(selectedOption);
setIsDropdownOpen(false);
}}
className="w-full flex-shrink-0 text-left"
>
<Listbox.Button
type="button"
ref={buttonRef}
onClick={() => setIsDropdownOpen((prev) => !prev)}
className={cn(
"relative w-full rounded border flex items-center gap-3 p-2.5",
error ? `border-red-500` : `border-custom-border-200`
)}
>
<div
className={cn(`w-full text-sm text-left`, !selectedValue ? "text-custom-text-300" : "text-custom-text-100")}
>
{selectedValue?.value || "Select an estimate point"}
</div>
<ChevronDown className={`size-3 ${true ? "stroke-onboarding-text-400" : "stroke-onboarding-text-100"}`} />
{error && (
<>
<Tooltip tooltipContent={error} position="bottom">
<div className="flex-shrink-0 w-3.5 h-3.5 overflow-hidden relative flex justify-center items-center text-red-500">
<Info size={14} />
</div>
</Tooltip>
</>
)}
</Listbox.Button>
<Transition
show={isDropdownOpen}
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Listbox.Options
ref={dropdownRef}
className="fixed z-10 mt-1 h-fit w-48 sm:w-60 overflow-y-auto rounded-md border border-custom-border-200 bg-custom-background-100 shadow-sm focus:outline-none"
>
<div className="p-1.5">
<Listbox.Option
value={"none"}
className={cn(
`cursor-pointer select-none truncate rounded px-1 py-1.5 hover:bg-custom-background-90`,
selectedOption === "none" ? "text-custom-text-100" : "text-custom-text-300"
)}
>
<div className="relative flex items-center text-wrap gap-2 px-1 py-0.5">
<div className="text-sm font-medium w-full line-clamp-1">None</div>
{selectedOption === "none" && <Check size={12} />}
</div>
</Listbox.Option>
{options.map((option) => (
<Listbox.Option
key={option?.key}
value={option?.id}
className={cn(
`cursor-pointer select-none truncate rounded px-1 py-1.5 hover:bg-custom-background-90`,
selectedOption === option?.id ? "text-custom-text-100" : "text-custom-text-300"
)}
>
<div className="relative flex items-center text-wrap gap-2 px-1 py-0.5">
<div className="text-sm font-medium w-full line-clamp-1">{option.value}</div>
{selectedOption === option?.id && <Check size={12} />}
</div>
</Listbox.Option>
))}
</div>
</Listbox.Options>
</Transition>
</Listbox>
</div>
);
};

View File

@@ -28,6 +28,7 @@ import { IssueUpdateStatus } from "@/components/issues";
// constants
import { EUserProjectRoles } from "@/constants/project";
// helpers
import { findHowManyDaysLeft } from "@/helpers/date-time.helper";
import { EInboxIssueStatus } from "@/helpers/inbox.helper";
import { copyUrlToClipboard } from "@/helpers/string.helper";
// hooks
@@ -71,6 +72,8 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
const canMarkAsDeclined = isAllowed && (inboxIssue?.status === 0 || inboxIssue?.status === -2);
const canDelete = isAllowed || inboxIssue?.created_by === currentUser?.id;
const isAcceptedOrDeclined = inboxIssue?.status ? [-1, 1, 2].includes(inboxIssue.status) : undefined;
// days left for snooze
const numberOfDaysLeft = findHowManyDaysLeft(inboxIssue?.snoozed_till);
const currentInboxIssueId = inboxIssue?.issue?.id;
@@ -109,7 +112,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
handleRedirection(nextOrPreviousIssueId);
};
const handleInboxSIssueSnooze = async (date: Date) => {
const handleInboxIssueSnooze = async (date: Date) => {
const nextOrPreviousIssueId = redirectIssue();
await inboxIssue?.updateInboxIssueSnoozeTill(date);
setIsSnoozeDateModalOpen(false);
@@ -127,6 +130,16 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
});
};
const handleIssueSnoozeAction = async () => {
if (inboxIssue?.snoozed_till && numberOfDaysLeft && numberOfDaysLeft > 0) {
const nextOrPreviousIssueId = redirectIssue();
await inboxIssue?.updateInboxIssueSnoozeTill(undefined);
handleRedirection(nextOrPreviousIssueId);
} else {
setIsSnoozeDateModalOpen(true);
}
};
const handleCopyIssueLink = () =>
copyUrlToClipboard(issueLink).then(() =>
setToast({
@@ -209,7 +222,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
isOpen={isSnoozeDateModalOpen}
handleClose={() => setIsSnoozeDateModalOpen(false)}
value={inboxIssue?.snoozed_till}
onConfirm={handleInboxSIssueSnooze}
onConfirm={handleInboxIssueSnooze}
/>
</>
@@ -299,10 +312,12 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
{isAllowed && (
<CustomMenu verticalEllipsis placement="bottom-start">
{canMarkAsAccepted && (
<CustomMenu.MenuItem onClick={() => setIsSnoozeDateModalOpen(true)}>
<CustomMenu.MenuItem onClick={handleIssueSnoozeAction}>
<div className="flex items-center gap-2">
<Clock size={14} strokeWidth={2} />
Snooze
{inboxIssue?.snoozed_till && numberOfDaysLeft && numberOfDaysLeft > 0
? "Un-snooze"
: "Snooze"}
</div>
</CustomMenu.MenuItem>
)}
@@ -337,7 +352,7 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
handleCopyIssueLink={handleCopyIssueLink}
setAcceptIssueModal={setAcceptIssueModal}
setDeclineIssueModal={setDeclineIssueModal}
setIsSnoozeDateModalOpen={setIsSnoozeDateModalOpen}
handleIssueSnoozeAction={handleIssueSnoozeAction}
setSelectDuplicateIssue={setSelectDuplicateIssue}
setDeleteIssueModal={setDeleteIssueModal}
canMarkAsAccepted={canMarkAsAccepted}

View File

@@ -20,6 +20,7 @@ import { InboxIssueStatus } from "@/components/inbox";
import { IssueUpdateStatus } from "@/components/issues";
// helpers
import { cn } from "@/helpers/common.helper";
import { findHowManyDaysLeft } from "@/helpers/date-time.helper";
// hooks
import { useAppRouter } from "@/hooks/use-app-router";
// store types
@@ -38,7 +39,7 @@ type Props = {
setAcceptIssueModal: (value: boolean) => void;
setDeclineIssueModal: (value: boolean) => void;
setDeleteIssueModal: (value: boolean) => void;
setIsSnoozeDateModalOpen: (value: boolean) => void;
handleIssueSnoozeAction: () => Promise<void>;
setSelectDuplicateIssue: (value: boolean) => void;
handleCopyIssueLink: () => void;
isMobileSidebar: boolean;
@@ -59,7 +60,7 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
setAcceptIssueModal,
setDeclineIssueModal,
setDeleteIssueModal,
setIsSnoozeDateModalOpen,
handleIssueSnoozeAction,
setSelectDuplicateIssue,
handleCopyIssueLink,
isMobileSidebar,
@@ -68,6 +69,8 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
const router = useAppRouter();
const issue = inboxIssue?.issue;
const currentInboxIssueId = issue?.id;
// days left for snooze
const numberOfDaysLeft = findHowManyDaysLeft(inboxIssue?.snoozed_till);
if (!issue || !inboxIssue) return null;
@@ -126,10 +129,10 @@ export const InboxIssueActionsMobileHeader: React.FC<Props> = observer((props) =
</CustomMenu.MenuItem>
)}
{canMarkAsAccepted && !isAcceptedOrDeclined && (
<CustomMenu.MenuItem onClick={() => setIsSnoozeDateModalOpen(true)}>
<CustomMenu.MenuItem onClick={handleIssueSnoozeAction}>
<div className="flex items-center gap-2">
<Clock size={14} strokeWidth={2} />
Snooze
{inboxIssue?.snoozed_till && numberOfDaysLeft && numberOfDaysLeft > 0 ? "Un-snooze" : "Snooze"}
</div>
</CustomMenu.MenuItem>
)}

View File

@@ -273,10 +273,19 @@ export class Estimate implements IEstimate {
newEstimatePointId ? { new_estimate_id: newEstimatePointId } : undefined
);
const currentIssues = Object.values(this.store.issue.issues.issuesMap || {});
if (currentIssues) {
currentIssues.map((issue) => {
if (issue.estimate_point === estimatePointId) {
this.store.issue.issues.updateIssue(issue.id, { estimate_point: newEstimatePointId });
}
});
}
runInAction(() => {
unset(this.estimatePoints, [estimatePointId]);
});
if (deleteEstimatePoint && deleteEstimatePoint.length > 0) {
if (deleteEstimatePoint) {
runInAction(() => {
deleteEstimatePoint.map((estimatePoint) => {
if (estimatePoint.id)

View File

@@ -22,7 +22,7 @@ export interface IInboxIssueStore {
// actions
updateInboxIssueStatus: (status: TInboxIssueStatus) => Promise<void>; // accept, decline
updateInboxIssueDuplicateTo: (issueId: string) => Promise<void>; // connecting the inbox issue to the project existing issue
updateInboxIssueSnoozeTill: (date: Date) => Promise<void>; // snooze the issue
updateInboxIssueSnoozeTill: (date: Date | undefined) => Promise<void>; // snooze the issue
updateIssue: (issue: Partial<TIssue>) => Promise<void>; // updating the issue
updateProjectIssue: (issue: Partial<TIssue>) => Promise<void>; // updating the issue
fetchIssueActivity: () => Promise<void>; // fetching the issue activity
@@ -53,7 +53,7 @@ export class InboxIssueStore implements IInboxIssueStore {
this.id = data.id;
this.status = data.status;
this.issue = data?.issue;
this.snoozed_till = data?.snoozed_till ? new Date(data.snoozed_till) : undefined;
this.snoozed_till = data?.snoozed_till || undefined;
this.duplicate_to = data?.duplicate_to || undefined;
this.created_by = data?.created_by || undefined;
this.duplicate_issue_detail = data?.duplicate_issue_detail || undefined;
@@ -124,8 +124,8 @@ export class InboxIssueStore implements IInboxIssueStore {
}
};
updateInboxIssueSnoozeTill = async (date: Date) => {
const inboxStatus = EInboxIssueStatus.SNOOZED;
updateInboxIssueSnoozeTill = async (date: Date | undefined) => {
const inboxStatus = date ? EInboxIssueStatus.SNOOZED : EInboxIssueStatus.PENDING;
const previousData: Partial<TInboxIssue> = {
status: this.status,
snoozed_till: this.snoozed_till,
@@ -134,7 +134,7 @@ export class InboxIssueStore implements IInboxIssueStore {
if (!this.issue.id) return;
const inboxIssue = await this.inboxIssueService.update(this.workspaceSlug, this.projectId, this.issue.id, {
status: inboxStatus,
snoozed_till: new Date(date),
snoozed_till: date ? new Date(date) : null,
});
runInAction(() => {
set(this, "status", inboxIssue?.status);