mirror of
https://github.com/makeplane/plane.git
synced 2026-02-24 04:00:14 +01:00
merge conflicts resolved
This commit is contained in:
@@ -27,8 +27,8 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
||||
// states
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [issues, setIssues] = useState<ISearchIssueResponse[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [selectedIssues, setSelectedIssues] = useState<ISearchIssueResponse[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isWorkspaceLevel, setIsWorkspaceLevel] = useState(false);
|
||||
|
||||
@@ -72,8 +72,7 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen || !workspaceSlug || !projectId) return;
|
||||
|
||||
setIsSearching(true);
|
||||
if (issues.length <= 0) setIsSearching(true);
|
||||
|
||||
projectService
|
||||
.projectIssuesSearch(workspaceSlug as string, projectId as string, {
|
||||
@@ -83,12 +82,21 @@ export const ExistingIssuesListModal: React.FC<Props> = (props) => {
|
||||
})
|
||||
.then((res) => setIssues(res))
|
||||
.finally(() => setIsSearching(false));
|
||||
}, [debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, searchParams, workspaceSlug]);
|
||||
}, [issues, debouncedSearchTerm, isOpen, isWorkspaceLevel, projectId, searchParams, workspaceSlug]);
|
||||
|
||||
useEffect(() => {
|
||||
setSearchTerm("");
|
||||
setIssues([]);
|
||||
setSelectedIssues([]);
|
||||
setIsSearching(false);
|
||||
setIsSubmitting(false);
|
||||
setIsWorkspaceLevel(false);
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Transition.Root show={isOpen} as={React.Fragment} afterLeave={() => setSearchTerm("")} appear>
|
||||
<Dialog as="div" className="relative z-20" onClose={() => {}}>
|
||||
<Dialog as="div" className="relative z-20" onClose={handleClose}>
|
||||
<Transition.Child
|
||||
as={React.Fragment}
|
||||
enter="ease-out duration-300"
|
||||
|
||||
@@ -128,7 +128,7 @@ export const IssuesByPriorityWidget: React.FC<WidgetProps> = observer((props) =>
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full py-6 hover:shadow-custom-shadow-4xl duration-300 overflow-hidden min-h-96">
|
||||
<div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full py-6 hover:shadow-custom-shadow-4xl duration-300 overflow-hidden min-h-96 flex flex-col">
|
||||
<div className="flex items-start justify-between gap-2 pl-7 pr-6">
|
||||
<div>
|
||||
<Link
|
||||
|
||||
@@ -127,7 +127,7 @@ export const IssuesByStateGroupWidget: React.FC<WidgetProps> = observer((props)
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full py-6 hover:shadow-custom-shadow-4xl duration-300 overflow-hidden min-h-96">
|
||||
<div className="bg-custom-background-100 rounded-xl border-[0.5px] border-custom-border-200 w-full py-6 hover:shadow-custom-shadow-4xl duration-300 overflow-hidden min-h-96 flex flex-col">
|
||||
<div className="flex items-start justify-between gap-2 pl-7 pr-6">
|
||||
<div>
|
||||
<Link
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useApplication, useCycle } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// icons
|
||||
import { ContrastIcon } from "@plane/ui";
|
||||
import { ContrastIcon, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
@@ -32,6 +32,7 @@ type ButtonProps = {
|
||||
dropdownArrow: boolean;
|
||||
dropdownArrowClassName: string;
|
||||
placeholder: string;
|
||||
tooltip: boolean;
|
||||
};
|
||||
|
||||
type DropdownOptions =
|
||||
@@ -51,21 +52,24 @@ const BorderButton = (props: ButtonProps) => {
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}{" "}
|
||||
{!hideText && <span className="flex-grow truncate">{cycle?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<Tooltip tooltipHeading="Cycle" tooltipContent={cycle?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}{" "}
|
||||
{!hideText && <span className="flex-grow truncate">{cycle?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -78,18 +82,24 @@ const BackgroundButton = (props: ButtonProps) => {
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80", className)}
|
||||
>
|
||||
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{cycle?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<Tooltip tooltipHeading="Cycle" tooltipContent={cycle?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{cycle?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -102,21 +112,24 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{cycle?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<Tooltip tooltipHeading="Cycle" tooltipContent={cycle?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ContrastIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{cycle?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -135,8 +148,9 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
placeholder = "Cycle",
|
||||
placement,
|
||||
projectId,
|
||||
value,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -254,6 +268,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
@@ -264,6 +279,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
@@ -273,6 +289,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
@@ -283,6 +300,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
@@ -292,6 +310,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
@@ -302,6 +321,7 @@ export const CycleDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { CalendarDays, X } from "lucide-react";
|
||||
// hooks
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "helpers/date-time.helper";
|
||||
import { cn } from "helpers/common.helper";
|
||||
@@ -33,6 +35,7 @@ type ButtonProps = {
|
||||
hideText?: boolean;
|
||||
onClear: () => void;
|
||||
placeholder: string;
|
||||
tooltip: boolean;
|
||||
};
|
||||
|
||||
const BorderButton = (props: ButtonProps) => {
|
||||
@@ -46,27 +49,34 @@ const BorderButton = (props: ButtonProps) => {
|
||||
hideText = false,
|
||||
onClear,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
className
|
||||
)}
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={date ? renderFormattedDate(date) : "None"}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
{!hideIcon && icon}
|
||||
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
|
||||
{isClearable && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && icon}
|
||||
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
|
||||
{isClearable && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -81,24 +91,34 @@ const BackgroundButton = (props: ButtonProps) => {
|
||||
hideText = false,
|
||||
onClear,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80", className)}
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={date ? renderFormattedDate(date) : "None"}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
{!hideIcon && icon}
|
||||
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
|
||||
{isClearable && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && icon}
|
||||
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
|
||||
{isClearable && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -113,27 +133,34 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
hideText = false,
|
||||
onClear,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={date ? renderFormattedDate(date) : "None"}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
{!hideIcon && icon}
|
||||
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
|
||||
{isClearable && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && icon}
|
||||
{!hideText && <span className="flex-grow truncate">{date ? renderFormattedDate(date) : placeholder}</span>}
|
||||
{isClearable && (
|
||||
<X
|
||||
className={cn("h-2 w-2 flex-shrink-0", clearIconClassName)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onClear();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -144,6 +171,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
buttonVariant,
|
||||
className = "",
|
||||
clearIconClassName = "",
|
||||
closeOnSelect = true,
|
||||
disabled = false,
|
||||
hideIcon = false,
|
||||
icon = <CalendarDays className="h-3 w-3 flex-shrink-0" />,
|
||||
@@ -153,9 +181,9 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
onChange,
|
||||
placeholder = "Date",
|
||||
placement,
|
||||
value,
|
||||
closeOnSelect = true,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
// refs
|
||||
@@ -218,6 +246,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
@@ -229,6 +258,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
@@ -241,6 +271,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
@@ -252,6 +283,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
@@ -264,6 +296,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
@@ -275,6 +308,7 @@ export const DateDropdown: React.FC<Props> = (props) => {
|
||||
placeholder={placeholder}
|
||||
isClearable={isClearable && isDateSelected}
|
||||
onClear={() => onChange(null)}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -8,6 +8,8 @@ import sortBy from "lodash/sortBy";
|
||||
import { useApplication, useEstimate } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
@@ -30,6 +32,7 @@ type ButtonProps = {
|
||||
hideIcon?: boolean;
|
||||
hideText?: boolean;
|
||||
placeholder: string;
|
||||
tooltip: boolean;
|
||||
};
|
||||
|
||||
type DropdownOptions =
|
||||
@@ -49,21 +52,30 @@ const BorderButton = (props: ButtonProps) => {
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
className
|
||||
)}
|
||||
<Tooltip
|
||||
tooltipHeading="Estimate"
|
||||
tooltipContent={estimatePoint !== null ? estimatePoint : placeholder}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{estimatePoint !== null ? estimatePoint : placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">{estimatePoint !== null ? estimatePoint : placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -76,18 +88,30 @@ const BackgroundButton = (props: ButtonProps) => {
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80", className)}
|
||||
<Tooltip
|
||||
tooltipHeading="Estimate"
|
||||
tooltipContent={estimatePoint !== null ? estimatePoint : placeholder}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{estimatePoint !== null ? estimatePoint : placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">{estimatePoint !== null ? estimatePoint : placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -100,21 +124,30 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
<Tooltip
|
||||
tooltipHeading="Estimate"
|
||||
tooltipContent={estimatePoint !== null ? estimatePoint : placeholder}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{estimatePoint !== null ? estimatePoint : placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <Triangle className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">{estimatePoint !== null ? estimatePoint : placeholder}</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -133,8 +166,9 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
placeholder = "Estimate",
|
||||
placement,
|
||||
projectId,
|
||||
value,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -242,6 +276,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
@@ -252,6 +287,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
@@ -261,6 +297,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
@@ -271,6 +308,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
@@ -280,6 +318,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
@@ -290,6 +329,7 @@ export const EstimateDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ChevronDown } from "lucide-react";
|
||||
// hooks
|
||||
import { useMember } from "hooks/store";
|
||||
// ui
|
||||
import { Avatar, AvatarGroup, UserGroupIcon } from "@plane/ui";
|
||||
import { Avatar, AvatarGroup, Tooltip, UserGroupIcon } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
|
||||
@@ -14,16 +14,17 @@ type ButtonProps = {
|
||||
placeholder: string;
|
||||
hideIcon?: boolean;
|
||||
hideText?: boolean;
|
||||
tooltip: boolean;
|
||||
userIds: string | string[] | null;
|
||||
};
|
||||
|
||||
const ButtonAvatars = observer(({ userIds }: { userIds: string | string[] | null }) => {
|
||||
const ButtonAvatars = observer(({ tooltip, userIds }: { tooltip: boolean; userIds: string | string[] | null }) => {
|
||||
const { getUserDetails } = useMember();
|
||||
|
||||
if (Array.isArray(userIds)) {
|
||||
if (userIds.length > 0)
|
||||
return (
|
||||
<AvatarGroup size="md">
|
||||
<AvatarGroup size="md" showTooltip={!tooltip}>
|
||||
{userIds.map((userId) => {
|
||||
const userDetails = getUserDetails(userId);
|
||||
|
||||
@@ -35,7 +36,7 @@ const ButtonAvatars = observer(({ userIds }: { userIds: string | string[] | null
|
||||
} else {
|
||||
if (userIds) {
|
||||
const userDetails = getUserDetails(userIds);
|
||||
return <Avatar src={userDetails?.avatar} name={userDetails?.display_name} size="md" />;
|
||||
return <Avatar src={userDetails?.avatar} name={userDetails?.display_name} size="md" showTooltip={!tooltip} />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +52,7 @@ export const BorderButton = observer((props: ButtonProps) => {
|
||||
hideText = false,
|
||||
placeholder,
|
||||
userIds,
|
||||
tooltip,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
@@ -58,22 +60,28 @@ export const BorderButton = observer((props: ButtonProps) => {
|
||||
const isMultiple = Array.isArray(userIds);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
className
|
||||
)}
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={`${userIds?.length ?? 0} assignee${userIds?.length !== 1 ? "s" : ""}`}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars userIds={userIds} />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">
|
||||
{userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">
|
||||
{userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -86,6 +94,7 @@ export const BackgroundButton = observer((props: ButtonProps) => {
|
||||
hideText = false,
|
||||
placeholder,
|
||||
userIds,
|
||||
tooltip,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
@@ -93,19 +102,28 @@ export const BackgroundButton = observer((props: ButtonProps) => {
|
||||
const isMultiple = Array.isArray(userIds);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80", className)}
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={`${userIds?.length ?? 0} assignee${userIds?.length !== 1 ? "s" : ""}`}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars userIds={userIds} />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">
|
||||
{userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">
|
||||
{userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -118,6 +136,7 @@ export const TransparentButton = observer((props: ButtonProps) => {
|
||||
hideText = false,
|
||||
placeholder,
|
||||
userIds,
|
||||
tooltip,
|
||||
} = props;
|
||||
// store hooks
|
||||
const { getUserDetails } = useMember();
|
||||
@@ -125,21 +144,27 @@ export const TransparentButton = observer((props: ButtonProps) => {
|
||||
const isMultiple = Array.isArray(userIds);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
<Tooltip
|
||||
tooltipHeading={placeholder}
|
||||
tooltipContent={`${userIds?.length ?? 0} assignee${userIds?.length !== 1 ? "s" : ""}`}
|
||||
disabled={!tooltip}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars userIds={userIds} />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">
|
||||
{userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <ButtonAvatars tooltip={tooltip} userIds={userIds} />}
|
||||
{!hideText && (
|
||||
<span className="flex-grow truncate">
|
||||
{userIds ? (isMultiple ? placeholder : getUserDetails(userIds)?.display_name) : placeholder}
|
||||
</span>
|
||||
)}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -36,8 +36,9 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
placeholder = "Members",
|
||||
placement,
|
||||
projectId,
|
||||
value,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -146,6 +147,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
@@ -155,6 +157,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
@@ -165,6 +168,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
@@ -174,6 +178,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
@@ -184,6 +189,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
@@ -193,6 +199,7 @@ export const ProjectMemberDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -31,8 +31,9 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
onChange,
|
||||
placeholder = "Members",
|
||||
placement,
|
||||
value,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -133,6 +134,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
@@ -142,6 +144,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
@@ -152,6 +155,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
@@ -161,6 +165,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
@@ -171,6 +176,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
@@ -180,6 +186,7 @@ export const WorkspaceMemberDropdown: React.FC<MemberDropdownProps> = observer((
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useApplication, useModule } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// icons
|
||||
import { DiceIcon } from "@plane/ui";
|
||||
import { DiceIcon, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
@@ -40,6 +40,7 @@ type ButtonProps = {
|
||||
hideText?: boolean;
|
||||
module: IModule | null;
|
||||
placeholder: string;
|
||||
tooltip: boolean;
|
||||
};
|
||||
|
||||
const BorderButton = (props: ButtonProps) => {
|
||||
@@ -51,21 +52,24 @@ const BorderButton = (props: ButtonProps) => {
|
||||
hideText = false,
|
||||
module,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{module?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<Tooltip tooltipHeading="Module" tooltipContent={module?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{module?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -78,18 +82,24 @@ const BackgroundButton = (props: ButtonProps) => {
|
||||
hideText = false,
|
||||
module,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80", className)}
|
||||
>
|
||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{module?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<Tooltip tooltipHeading="Module" tooltipContent={module?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{module?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -102,21 +112,24 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
hideText = false,
|
||||
module,
|
||||
placeholder,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{module?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<Tooltip tooltipHeading="Module" tooltipContent={module?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && <DiceIcon className="h-3 w-3 flex-shrink-0" />}
|
||||
{!hideText && <span className="flex-grow truncate">{module?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -135,8 +148,9 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
placeholder = "Module",
|
||||
placement,
|
||||
projectId,
|
||||
value,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -253,6 +267,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
@@ -263,6 +278,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
@@ -272,6 +288,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
@@ -282,6 +299,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
@@ -291,6 +309,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
@@ -301,6 +320,7 @@ export const ModuleDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
|
||||
@@ -2,11 +2,12 @@ import { Fragment, ReactNode, useRef, useState } from "react";
|
||||
import { Combobox } from "@headlessui/react";
|
||||
import { usePopper } from "react-popper";
|
||||
import { Check, ChevronDown, Search } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
// hooks
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// icons
|
||||
import { PriorityIcon } from "@plane/ui";
|
||||
import { PriorityIcon, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
@@ -14,7 +15,6 @@ import { TIssuePriorities } from "@plane/types";
|
||||
import { TDropdownProps } from "./types";
|
||||
// constants
|
||||
import { ISSUE_PRIORITIES } from "constants/issue";
|
||||
import { useTheme } from "next-themes";
|
||||
|
||||
type Props = TDropdownProps & {
|
||||
button?: ReactNode;
|
||||
@@ -33,6 +33,7 @@ type ButtonProps = {
|
||||
hideText?: boolean;
|
||||
highlightUrgent: boolean;
|
||||
priority: TIssuePriorities;
|
||||
tooltip: boolean;
|
||||
};
|
||||
|
||||
const BorderButton = (props: ButtonProps) => {
|
||||
@@ -44,6 +45,7 @@ const BorderButton = (props: ButtonProps) => {
|
||||
hideText = false,
|
||||
highlightUrgent,
|
||||
priority,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
|
||||
@@ -57,47 +59,49 @@ const BorderButton = (props: ButtonProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] rounded text-xs px-2 py-0.5",
|
||||
priorityClasses[priority],
|
||||
{
|
||||
// compact the icons if text is hidden
|
||||
"px-0.5": hideText,
|
||||
// highlight the whole button if text is hidden and priority is urgent
|
||||
"bg-red-500 border-red-500": priority === "urgent" && hideText && highlightUrgent,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<div
|
||||
className={cn({
|
||||
// highlight just the icon if text is visible and priority is urgent
|
||||
"bg-red-500 p-1 rounded": priority === "urgent" && !hideText && highlightUrgent,
|
||||
})}
|
||||
>
|
||||
<PriorityIcon
|
||||
priority={priority}
|
||||
size={12}
|
||||
className={cn("flex-shrink-0", {
|
||||
// increase the icon size if text is hidden
|
||||
"h-3.5 w-3.5": hideText,
|
||||
// centre align the icons if text is hidden
|
||||
"translate-x-[0.0625rem]": hideText && priority === "high",
|
||||
"translate-x-0.5": hideText && priority === "medium",
|
||||
"translate-x-1": hideText && priority === "low",
|
||||
// highlight the icon if priority is urgent
|
||||
"text-white": priority === "urgent" && highlightUrgent,
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] rounded text-xs px-2 py-0.5",
|
||||
priorityClasses[priority],
|
||||
{
|
||||
// compact the icons if text is hidden
|
||||
"px-0.5": hideText,
|
||||
// highlight the whole button if text is hidden and priority is urgent
|
||||
"bg-red-500 border-red-500": priority === "urgent" && hideText && highlightUrgent,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<div
|
||||
className={cn({
|
||||
// highlight just the icon if text is visible and priority is urgent
|
||||
"bg-red-500 p-1 rounded": priority === "urgent" && !hideText && highlightUrgent,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{priorityDetails?.title}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
>
|
||||
<PriorityIcon
|
||||
priority={priority}
|
||||
size={12}
|
||||
className={cn("flex-shrink-0", {
|
||||
// increase the icon size if text is hidden
|
||||
"h-3.5 w-3.5": hideText,
|
||||
// centre align the icons if text is hidden
|
||||
"translate-x-[0.0625rem]": hideText && priority === "high",
|
||||
"translate-x-0.5": hideText && priority === "medium",
|
||||
"translate-x-1": hideText && priority === "low",
|
||||
// highlight the icon if priority is urgent
|
||||
"text-white": priority === "urgent" && highlightUrgent,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{priorityDetails?.title}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -110,6 +114,7 @@ const BackgroundButton = (props: ButtonProps) => {
|
||||
hideText = false,
|
||||
highlightUrgent,
|
||||
priority,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
|
||||
@@ -123,47 +128,49 @@ const BackgroundButton = (props: ButtonProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5",
|
||||
priorityClasses[priority],
|
||||
{
|
||||
// compact the icons if text is hidden
|
||||
"px-0.5": hideText,
|
||||
// highlight the whole button if text is hidden and priority is urgent
|
||||
"bg-red-500 border-red-500": priority === "urgent" && hideText && highlightUrgent,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<div
|
||||
className={cn({
|
||||
// highlight just the icon if text is visible and priority is urgent
|
||||
"bg-red-500 p-1 rounded": priority === "urgent" && !hideText && highlightUrgent,
|
||||
})}
|
||||
>
|
||||
<PriorityIcon
|
||||
priority={priority}
|
||||
size={12}
|
||||
className={cn("flex-shrink-0", {
|
||||
// increase the icon size if text is hidden
|
||||
"h-3.5 w-3.5": hideText,
|
||||
// centre align the icons if text is hidden
|
||||
"translate-x-[0.0625rem]": hideText && priority === "high",
|
||||
"translate-x-0.5": hideText && priority === "medium",
|
||||
"translate-x-1": hideText && priority === "low",
|
||||
// highlight the icon if priority is urgent
|
||||
"text-white": priority === "urgent" && highlightUrgent,
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5",
|
||||
priorityClasses[priority],
|
||||
{
|
||||
// compact the icons if text is hidden
|
||||
"px-0.5": hideText,
|
||||
// highlight the whole button if text is hidden and priority is urgent
|
||||
"bg-red-500 border-red-500": priority === "urgent" && hideText && highlightUrgent,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<div
|
||||
className={cn({
|
||||
// highlight just the icon if text is visible and priority is urgent
|
||||
"bg-red-500 p-1 rounded": priority === "urgent" && !hideText && highlightUrgent,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{priorityDetails?.title}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
>
|
||||
<PriorityIcon
|
||||
priority={priority}
|
||||
size={12}
|
||||
className={cn("flex-shrink-0", {
|
||||
// increase the icon size if text is hidden
|
||||
"h-3.5 w-3.5": hideText,
|
||||
// centre align the icons if text is hidden
|
||||
"translate-x-[0.0625rem]": hideText && priority === "high",
|
||||
"translate-x-0.5": hideText && priority === "medium",
|
||||
"translate-x-1": hideText && priority === "low",
|
||||
// highlight the icon if priority is urgent
|
||||
"text-white": priority === "urgent" && highlightUrgent,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{priorityDetails?.title}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -176,6 +183,7 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
hideText = false,
|
||||
highlightUrgent,
|
||||
priority,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
const priorityDetails = ISSUE_PRIORITIES.find((p) => p.key === priority);
|
||||
@@ -189,47 +197,49 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
priorityClasses[priority],
|
||||
{
|
||||
// compact the icons if text is hidden
|
||||
"px-0.5": hideText,
|
||||
// highlight the whole button if text is hidden and priority is urgent
|
||||
"bg-red-500 border-red-500": priority === "urgent" && hideText && highlightUrgent,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<div
|
||||
className={cn({
|
||||
// highlight just the icon if text is visible and priority is urgent
|
||||
"bg-red-500 p-1 rounded": priority === "urgent" && !hideText && highlightUrgent,
|
||||
})}
|
||||
>
|
||||
<PriorityIcon
|
||||
priority={priority}
|
||||
size={12}
|
||||
className={cn("flex-shrink-0", {
|
||||
// increase the icon size if text is hidden
|
||||
"h-3.5 w-3.5": hideText,
|
||||
// centre align the icons if text is hidden
|
||||
"translate-x-[0.0625rem]": hideText && priority === "high",
|
||||
"translate-x-0.5": hideText && priority === "medium",
|
||||
"translate-x-1": hideText && priority === "low",
|
||||
// highlight the icon if priority is urgent
|
||||
"text-white": priority === "urgent" && highlightUrgent,
|
||||
<Tooltip tooltipHeading="Priority" tooltipContent={priorityDetails?.title ?? "None"} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
priorityClasses[priority],
|
||||
{
|
||||
// compact the icons if text is hidden
|
||||
"px-0.5": hideText,
|
||||
// highlight the whole button if text is hidden and priority is urgent
|
||||
"bg-red-500 border-red-500": priority === "urgent" && hideText && highlightUrgent,
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<div
|
||||
className={cn({
|
||||
// highlight just the icon if text is visible and priority is urgent
|
||||
"bg-red-500 p-1 rounded": priority === "urgent" && !hideText && highlightUrgent,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{priorityDetails?.title}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
>
|
||||
<PriorityIcon
|
||||
priority={priority}
|
||||
size={12}
|
||||
className={cn("flex-shrink-0", {
|
||||
// increase the icon size if text is hidden
|
||||
"h-3.5 w-3.5": hideText,
|
||||
// centre align the icons if text is hidden
|
||||
"translate-x-[0.0625rem]": hideText && priority === "high",
|
||||
"translate-x-0.5": hideText && priority === "medium",
|
||||
"translate-x-1": hideText && priority === "low",
|
||||
// highlight the icon if priority is urgent
|
||||
"text-white": priority === "urgent" && highlightUrgent,
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{priorityDetails?.title}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -247,8 +257,9 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
highlightUrgent = true,
|
||||
onChange,
|
||||
placement,
|
||||
value,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -341,6 +352,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
@@ -352,6 +364,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
@@ -364,6 +377,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
@@ -375,6 +389,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
@@ -387,6 +402,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
@@ -398,6 +414,7 @@ export const PriorityDropdown: React.FC<Props> = (props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -7,6 +7,8 @@ import { Check, ChevronDown, Search } from "lucide-react";
|
||||
import { useProject } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
@@ -30,6 +32,7 @@ type ButtonProps = {
|
||||
hideText?: boolean;
|
||||
placeholder: string;
|
||||
project: IProject | null;
|
||||
tooltip: boolean;
|
||||
};
|
||||
|
||||
const BorderButton = (props: ButtonProps) => {
|
||||
@@ -41,25 +44,28 @@ const BorderButton = (props: ButtonProps) => {
|
||||
hideText = false,
|
||||
placeholder,
|
||||
project,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<span className="grid place-items-center flex-shrink-0">
|
||||
{project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null}
|
||||
</span>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{project?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<Tooltip tooltipHeading="Project" tooltipContent={project?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<span className="grid place-items-center flex-shrink-0">
|
||||
{project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null}
|
||||
</span>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{project?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -72,22 +78,28 @@ const BackgroundButton = (props: ButtonProps) => {
|
||||
hideText = false,
|
||||
placeholder,
|
||||
project,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80", className)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<span className="grid place-items-center flex-shrink-0">
|
||||
{project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null}
|
||||
</span>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{project?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<Tooltip tooltipHeading="Project" tooltipContent={project?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<span className="grid place-items-center flex-shrink-0">
|
||||
{project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null}
|
||||
</span>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{project?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -100,25 +112,28 @@ const TransparentButton = (props: ButtonProps) => {
|
||||
hideText = false,
|
||||
placeholder,
|
||||
project,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<span className="grid place-items-center flex-shrink-0">
|
||||
{project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null}
|
||||
</span>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{project?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<Tooltip tooltipHeading="Project" tooltipContent={project?.name ?? placeholder} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<span className="grid place-items-center flex-shrink-0">
|
||||
{project?.emoji ? renderEmoji(project?.emoji) : project?.icon_prop ? renderEmoji(project?.icon_prop) : null}
|
||||
</span>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{project?.name ?? placeholder}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -136,8 +151,9 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
onChange,
|
||||
placeholder = "Project",
|
||||
placement,
|
||||
value,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -239,6 +255,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
@@ -249,6 +266,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
<BackgroundButton
|
||||
@@ -258,6 +276,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
@@ -268,6 +287,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
<TransparentButton
|
||||
@@ -277,6 +297,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
@@ -287,6 +308,7 @@ export const ProjectDropdown: React.FC<Props> = observer((props) => {
|
||||
hideIcon={hideIcon}
|
||||
hideText
|
||||
placeholder={placeholder}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : null}
|
||||
</button>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useApplication, useProjectState } from "hooks/store";
|
||||
import { useDropdownKeyDown } from "hooks/use-dropdown-key-down";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// icons
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
import { StateGroupIcon, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "helpers/common.helper";
|
||||
// types
|
||||
@@ -31,65 +31,111 @@ type ButtonProps = {
|
||||
hideIcon?: boolean;
|
||||
hideText?: boolean;
|
||||
state: IState | undefined;
|
||||
tooltip: boolean;
|
||||
};
|
||||
|
||||
const BorderButton = (props: ButtonProps) => {
|
||||
const { className, dropdownArrow, dropdownArrowClassName, hideIcon = false, hideText = false, state } = props;
|
||||
const {
|
||||
className,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
state,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} className="h-3 w-3 flex-shrink-0" />
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{state?.name ?? "State"}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<Tooltip tooltipHeading="State" tooltipContent={state?.name ?? "State"} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 border-[0.5px] border-custom-border-300 hover:bg-custom-background-80 rounded text-xs px-2 py-0.5",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<StateGroupIcon
|
||||
stateGroup={state?.group ?? "backlog"}
|
||||
color={state?.color}
|
||||
className="h-3 w-3 flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{state?.name ?? "State"}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const BackgroundButton = (props: ButtonProps) => {
|
||||
const { className, dropdownArrow, dropdownArrowClassName, hideIcon = false, hideText = false, state } = props;
|
||||
const {
|
||||
className,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
state,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80", className)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} className="h-3 w-3 flex-shrink-0" />
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{state?.name ?? "State"}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<Tooltip tooltipHeading="State" tooltipContent={state?.name ?? "State"} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<StateGroupIcon
|
||||
stateGroup={state?.group ?? "backlog"}
|
||||
color={state?.color}
|
||||
className="h-3 w-3 flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{state?.name ?? "State"}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const TransparentButton = (props: ButtonProps) => {
|
||||
const { className, dropdownArrow, dropdownArrowClassName, hideIcon = false, hideText = false, state } = props;
|
||||
const {
|
||||
className,
|
||||
dropdownArrow,
|
||||
dropdownArrowClassName,
|
||||
hideIcon = false,
|
||||
hideText = false,
|
||||
state,
|
||||
tooltip,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<StateGroupIcon stateGroup={state?.group ?? "backlog"} color={state?.color} className="h-3 w-3 flex-shrink-0" />
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{state?.name ?? "State"}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<Tooltip tooltipHeading="State" tooltipContent={state?.name ?? "State"} disabled={!tooltip}>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full flex items-center gap-1.5 rounded text-xs px-2 py-0.5 hover:bg-custom-background-80",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{!hideIcon && (
|
||||
<StateGroupIcon
|
||||
stateGroup={state?.group ?? "backlog"}
|
||||
color={state?.color}
|
||||
className="h-3 w-3 flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
{!hideText && <span className="flex-grow truncate">{state?.name ?? "State"}</span>}
|
||||
{dropdownArrow && (
|
||||
<ChevronDown className={cn("h-2.5 w-2.5 flex-shrink-0", dropdownArrowClassName)} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -107,8 +153,9 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
onChange,
|
||||
placement,
|
||||
projectId,
|
||||
value,
|
||||
tabIndex,
|
||||
tooltip = false,
|
||||
value,
|
||||
} = props;
|
||||
// states
|
||||
const [query, setQuery] = useState("");
|
||||
@@ -204,6 +251,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "border-without-text" ? (
|
||||
<BorderButton
|
||||
@@ -212,6 +260,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "background-with-text" ? (
|
||||
@@ -221,6 +270,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "background-without-text" ? (
|
||||
<BackgroundButton
|
||||
@@ -229,6 +279,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : buttonVariant === "transparent-with-text" ? (
|
||||
@@ -238,6 +289,7 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
/>
|
||||
) : buttonVariant === "transparent-without-text" ? (
|
||||
<TransparentButton
|
||||
@@ -245,6 +297,8 @@ export const StateDropdown: React.FC<Props> = observer((props) => {
|
||||
className={buttonClassName}
|
||||
dropdownArrow={dropdownArrow && !disabled}
|
||||
dropdownArrowClassName={dropdownArrowClassName}
|
||||
hideIcon={hideIcon}
|
||||
tooltip={tooltip}
|
||||
hideText
|
||||
/>
|
||||
) : null}
|
||||
|
||||
2
web/components/dropdowns/types.d.ts
vendored
2
web/components/dropdowns/types.d.ts
vendored
@@ -18,4 +18,6 @@ export type TDropdownProps = {
|
||||
placeholder?: string;
|
||||
placement?: Placement;
|
||||
tabIndex?: number;
|
||||
// TODO: rename this prop to showTooltip
|
||||
tooltip?: boolean;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
// components
|
||||
import { ComicBoxButton } from "./comic-box-button";
|
||||
// ui
|
||||
@@ -51,8 +52,6 @@ export const EmptyState: React.FC<Props> = ({
|
||||
</>
|
||||
);
|
||||
|
||||
const imageElement = <img src={image} sizes="100%" alt={primaryButton?.text || "button image"} />;
|
||||
|
||||
const secondaryButtonElement = secondaryButton && (
|
||||
<Button
|
||||
size={size === "sm" ? "md" : "lg"}
|
||||
@@ -75,7 +74,14 @@ export const EmptyState: React.FC<Props> = ({
|
||||
>
|
||||
<div className="flex flex-col gap-1.5 flex-shrink-0">{emptyStateHeader}</div>
|
||||
|
||||
{imageElement}
|
||||
<Image
|
||||
src={image}
|
||||
alt={primaryButton?.text || "button image"}
|
||||
width={384}
|
||||
height={250}
|
||||
layout="responsive"
|
||||
lazyBoundary="100%"
|
||||
/>
|
||||
|
||||
<div className="relative flex items-center justify-center gap-2 flex-shrink-0 w-full">
|
||||
{primaryButton && (
|
||||
|
||||
86
web/components/inbox/content/root.tsx
Normal file
86
web/components/inbox/content/root.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Inbox } from "lucide-react";
|
||||
// hooks
|
||||
import { useInboxIssues } from "hooks/store";
|
||||
// components
|
||||
import { InboxIssueActionsHeader } from "components/inbox";
|
||||
import { InboxIssueDetailRoot } from "components/issues/issue-detail/inbox";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
|
||||
type TInboxContentRoot = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
inboxId: string;
|
||||
inboxIssueId: string | undefined;
|
||||
};
|
||||
|
||||
export const InboxContentRoot: FC<TInboxContentRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxId, inboxIssueId } = props;
|
||||
// hooks
|
||||
const {
|
||||
issues: { loader, getInboxIssuesByInboxId },
|
||||
} = useInboxIssues();
|
||||
|
||||
const inboxIssuesList = inboxId ? getInboxIssuesByInboxId(inboxId) : undefined;
|
||||
|
||||
return (
|
||||
<>
|
||||
{loader === "init-loader" ? (
|
||||
<Loader className="flex h-full gap-5 p-5">
|
||||
<div className="basis-2/3 space-y-2">
|
||||
<Loader.Item height="30px" width="40%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
<Loader.Item height="15px" width="40%" />
|
||||
</div>
|
||||
<div className="basis-1/3 space-y-3">
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</div>
|
||||
</Loader>
|
||||
) : (
|
||||
<>
|
||||
{!inboxIssueId ? (
|
||||
<div className="grid h-full place-items-center p-4 text-custom-text-200">
|
||||
<div className="grid h-full place-items-center">
|
||||
<div className="my-5 flex flex-col items-center gap-4">
|
||||
<Inbox size={60} strokeWidth={1.5} />
|
||||
{inboxIssuesList && inboxIssuesList.length > 0 ? (
|
||||
<span className="text-custom-text-200">
|
||||
{inboxIssuesList?.length} issues found. Select an issue from the sidebar to view its details.
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-custom-text-200">No issues found</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full h-full overflow-hidden relative flex flex-col">
|
||||
<div className="flex-shrink-0 min-h-[50px] border-b border-custom-border-300">
|
||||
<InboxIssueActionsHeader
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
inboxId={inboxId}
|
||||
inboxIssueId={inboxIssueId}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full h-full">
|
||||
<InboxIssueDetailRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
inboxId={inboxId}
|
||||
issueId={inboxIssueId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FC, useEffect, useMemo, useState } from "react";
|
||||
import { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import DatePicker from "react-datepicker";
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// icons
|
||||
import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Inbox, Trash2, XCircle } from "lucide-react";
|
||||
import { CheckCircle2, ChevronDown, ChevronUp, Clock, FileStack, Trash2, XCircle } from "lucide-react";
|
||||
// types
|
||||
import type { TInboxStatus, TInboxDetailedStatus } from "@plane/types";
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
@@ -135,6 +135,44 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
]
|
||||
);
|
||||
|
||||
const handleInboxIssueNavigation = useCallback(
|
||||
(direction: "next" | "prev") => {
|
||||
if (!inboxIssues || !inboxIssueId) return;
|
||||
const nextIssueIndex =
|
||||
direction === "next"
|
||||
? (currentIssueIndex + 1) % inboxIssues.length
|
||||
: (currentIssueIndex - 1 + inboxIssues.length) % inboxIssues.length;
|
||||
const nextIssueId = inboxIssues[nextIssueIndex];
|
||||
if (!nextIssueId) return;
|
||||
router.push({
|
||||
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
||||
query: {
|
||||
inboxIssueId: nextIssueId,
|
||||
},
|
||||
});
|
||||
},
|
||||
[workspaceSlug, projectId, inboxId, inboxIssues, inboxIssueId, currentIssueIndex, router]
|
||||
);
|
||||
|
||||
const onKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === "ArrowUp") {
|
||||
handleInboxIssueNavigation("prev");
|
||||
} else if (e.key === "ArrowDown") {
|
||||
handleInboxIssueNavigation("next");
|
||||
}
|
||||
},
|
||||
[handleInboxIssueNavigation]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [onKeyDown]);
|
||||
|
||||
const isAllowed = !!currentProjectRole && currentProjectRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
const today = new Date();
|
||||
@@ -207,20 +245,14 @@ export const InboxIssueActionsHeader: FC<TInboxIssueActionsHeader> = observer((p
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-custom-border-200 bg-custom-background-90 p-1.5 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "ArrowUp" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
onClick={() => handleInboxIssueNavigation("prev")}
|
||||
>
|
||||
<ChevronUp size={14} strokeWidth={2} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border border-custom-border-200 bg-custom-background-90 p-1.5 hover:bg-custom-background-80"
|
||||
onClick={() => {
|
||||
const e = new KeyboardEvent("keydown", { key: "ArrowDown" });
|
||||
document.dispatchEvent(e);
|
||||
}}
|
||||
onClick={() => handleInboxIssueNavigation("next")}
|
||||
>
|
||||
<ChevronDown size={14} strokeWidth={2} />
|
||||
</button>
|
||||
|
||||
@@ -3,6 +3,8 @@ export * from "./modals";
|
||||
export * from "./inbox-issue-actions";
|
||||
export * from "./inbox-issue-status";
|
||||
|
||||
export * from "./content/root";
|
||||
|
||||
export * from "./sidebar/root";
|
||||
|
||||
export * from "./sidebar/filter/filter-selection";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FC } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useRouter } from "next/router";
|
||||
// mobx store
|
||||
import { useInboxIssues } from "hooks/store";
|
||||
// ui
|
||||
@@ -16,6 +17,9 @@ type TInboxIssueFilterSelection = { workspaceSlug: string; projectId: string; in
|
||||
|
||||
export const InboxIssueFilterSelection: FC<TInboxIssueFilterSelection> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxId } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { inboxIssueId } = router.query;
|
||||
// hooks
|
||||
const {
|
||||
filters: { inboxFilters, updateInboxFilters },
|
||||
@@ -49,6 +53,12 @@ export const InboxIssueFilterSelection: FC<TInboxIssueFilterSelection> = observe
|
||||
updateInboxFilters(workspaceSlug.toString(), projectId.toString(), inboxId.toString(), {
|
||||
[option.key]: [...currentValue, option.value],
|
||||
});
|
||||
|
||||
if (inboxIssueId) {
|
||||
router.push({
|
||||
pathname: `/${workspaceSlug}/projects/${projectId}/inbox/${inboxId}`,
|
||||
});
|
||||
}
|
||||
}}
|
||||
direction="right"
|
||||
height="rg"
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { FC } from "react";
|
||||
import { Inbox } from "lucide-react";
|
||||
import { observer } from "mobx-react";
|
||||
// hooks
|
||||
import { useInboxIssues } from "hooks/store";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// components
|
||||
import { InboxIssueList, InboxIssueFilterSelection, InboxIssueAppliedFilter } from "../";
|
||||
|
||||
@@ -9,19 +14,23 @@ type TInboxSidebarRoot = {
|
||||
inboxId: string;
|
||||
};
|
||||
|
||||
export const InboxSidebarRoot: FC<TInboxSidebarRoot> = (props) => {
|
||||
export const InboxSidebarRoot: FC<TInboxSidebarRoot> = observer((props) => {
|
||||
const { workspaceSlug, projectId, inboxId } = props;
|
||||
// store hooks
|
||||
const {
|
||||
issues: { loader },
|
||||
} = useInboxIssues();
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col w-full h-full">
|
||||
<div className="flex-shrink-0 w-full h-[50px] relative flex justify-between items-center gap-2 p-2 px-3 border-b border-custom-border-100">
|
||||
<div className="flex-shrink-0 w-full h-[50px] relative flex justify-between items-center gap-2 p-2 px-3 border-b border-custom-border-300">
|
||||
<div className="relative flex items-center gap-1">
|
||||
<div className="relative w-6 h-6 flex justify-center items-center rounded bg-custom-background-80">
|
||||
<Inbox className="w-4 h-4" />
|
||||
</div>
|
||||
<div className="font-medium">Inbox</div>
|
||||
</div>
|
||||
<div className="z-[999999]">
|
||||
<div className="z-20">
|
||||
<InboxIssueFilterSelection workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -30,9 +39,18 @@ export const InboxSidebarRoot: FC<TInboxSidebarRoot> = (props) => {
|
||||
<InboxIssueAppliedFilter workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
||||
</div>
|
||||
|
||||
<div className="w-full h-full overflow-hidden">
|
||||
<InboxIssueList workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
||||
</div>
|
||||
{loader && ["init-loader", "mutation"].includes(loader) ? (
|
||||
<Loader className="flex flex-col h-full gap-5 p-5">
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</Loader>
|
||||
) : (
|
||||
<div className="w-full h-full overflow-hidden">
|
||||
<InboxIssueList workspaceSlug={workspaceSlug} projectId={projectId} inboxId={inboxId} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
// components
|
||||
import { ActivityIcon, ActivityMessage } from "components/core";
|
||||
import { CommentCard } from "components/issues/comment";
|
||||
// ui
|
||||
import { Loader, Tooltip } from "@plane/ui";
|
||||
// helpers
|
||||
import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssueActivity } from "@plane/types";
|
||||
import { History } from "lucide-react";
|
||||
|
||||
type Props = {
|
||||
activity: IIssueActivity[] | undefined;
|
||||
handleCommentUpdate: (commentId: string, data: Partial<IIssueActivity>) => Promise<void>;
|
||||
handleCommentDelete: (commentId: string) => Promise<void>;
|
||||
showAccessSpecifier?: boolean;
|
||||
};
|
||||
|
||||
export const IssueActivitySection: React.FC<Props> = ({
|
||||
activity,
|
||||
handleCommentUpdate,
|
||||
handleCommentDelete,
|
||||
showAccessSpecifier = false,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
|
||||
if (!activity)
|
||||
return (
|
||||
<Loader className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Loader.Item height="30px" width="40%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Loader.Item height="30px" width="40%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Loader.Item height="30px" width="40%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
</div>
|
||||
</Loader>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flow-root">
|
||||
<ul role="list" className="-mb-4">
|
||||
{activity.map((activityItem, index) => {
|
||||
// determines what type of action is performed
|
||||
const message = activityItem.field ? <ActivityMessage activity={activityItem} /> : "created the issue.";
|
||||
|
||||
if ("field" in activityItem && activityItem.field !== "updated_by") {
|
||||
return (
|
||||
<li key={activityItem.id}>
|
||||
<div className="relative pb-1">
|
||||
{activity.length > 1 && index !== activity.length - 1 ? (
|
||||
<span
|
||||
className="absolute left-5 top-5 -ml-px h-full w-0.5 bg-custom-background-80"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<div className="relative flex items-start space-x-2">
|
||||
<div>
|
||||
<div className="relative px-1.5">
|
||||
<div className="mt-1.5">
|
||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-80 text-custom-text-200 ring-white">
|
||||
{activityItem.field ? (
|
||||
activityItem.new_value === "restore" ? (
|
||||
<History className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
) : (
|
||||
<ActivityIcon activity={activityItem} />
|
||||
)
|
||||
) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activityItem.actor_detail.avatar}
|
||||
alt={activityItem.actor_detail.display_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="h-full w-full rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
|
||||
>
|
||||
{activityItem.actor_detail.is_bot
|
||||
? activityItem.actor_detail.first_name.charAt(0)
|
||||
: activityItem.actor_detail.display_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1 py-3">
|
||||
<div className="break-words text-xs text-custom-text-200">
|
||||
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
|
||||
<span className="text-gray font-medium">Plane</span>
|
||||
) : activityItem.actor_detail.is_bot ? (
|
||||
<span className="text-gray font-medium">{activityItem.actor_detail.first_name} Bot</span>
|
||||
) : (
|
||||
<Link href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}>
|
||||
<span className="text-gray font-medium">
|
||||
{activityItem.actor_detail.is_bot
|
||||
? activityItem.actor_detail.first_name
|
||||
: activityItem.actor_detail.display_name}
|
||||
</span>
|
||||
</Link>
|
||||
)}{" "}
|
||||
{message}{" "}
|
||||
<Tooltip
|
||||
tooltipContent={`${renderFormattedDate(activityItem.created_at)}, ${renderFormattedTime(
|
||||
activityItem.created_at
|
||||
)}`}
|
||||
>
|
||||
<span className="whitespace-nowrap">{calculateTimeAgo(activityItem.created_at)}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
} else if ("comment_json" in activityItem)
|
||||
return (
|
||||
<div key={activityItem.id} className="mt-4">
|
||||
<CommentCard
|
||||
comment={activityItem as IIssueActivity}
|
||||
handleCommentDeletion={handleCommentDelete}
|
||||
onSubmit={handleCommentUpdate}
|
||||
showAccessSpecifier={showAccessSpecifier}
|
||||
workspaceSlug={workspaceSlug as string}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,126 +0,0 @@
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
// hooks
|
||||
import { useMention, useWorkspace } from "hooks/store";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// components
|
||||
import { LiteTextEditorWithRef } from "@plane/lite-text-editor";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
import { Globe2, Lock } from "lucide-react";
|
||||
// types
|
||||
import type { IIssueActivity } from "@plane/types";
|
||||
|
||||
const defaultValues: Partial<IIssueActivity> = {
|
||||
access: "INTERNAL",
|
||||
comment_html: "",
|
||||
};
|
||||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
onSubmit: (data: IIssueActivity) => Promise<void>;
|
||||
showAccessSpecifier?: boolean;
|
||||
};
|
||||
|
||||
type commentAccessType = {
|
||||
icon: any;
|
||||
key: string;
|
||||
label: "Private" | "Public";
|
||||
};
|
||||
const commentAccess: commentAccessType[] = [
|
||||
{
|
||||
icon: Lock,
|
||||
key: "INTERNAL",
|
||||
label: "Private",
|
||||
},
|
||||
{
|
||||
icon: Globe2,
|
||||
key: "EXTERNAL",
|
||||
label: "Public",
|
||||
},
|
||||
];
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit, showAccessSpecifier = false }) => {
|
||||
// refs
|
||||
const editorRef = React.useRef<any>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const workspaceStore = useWorkspace();
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string;
|
||||
|
||||
// store hooks
|
||||
const { mentionHighlights, mentionSuggestions } = useMention();
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
} = useForm<IIssueActivity>({ defaultValues });
|
||||
|
||||
const handleAddComment = async (formData: IIssueActivity) => {
|
||||
if (!formData.comment_html || isSubmitting) return;
|
||||
|
||||
await onSubmit(formData).then(() => {
|
||||
reset(defaultValues);
|
||||
editorRef.current?.clearEditor();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={handleSubmit(handleAddComment)}>
|
||||
<div>
|
||||
<Controller
|
||||
name="access"
|
||||
control={control}
|
||||
render={({ field: { onChange: onAccessChange, value: accessValue } }) => (
|
||||
<Controller
|
||||
name="comment_html"
|
||||
control={control}
|
||||
render={({ field: { onChange: onCommentChange, value: commentValue } }) => (
|
||||
<LiteTextEditorWithRef
|
||||
onEnterKeyPress={handleSubmit(handleAddComment)}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
||||
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
||||
ref={editorRef}
|
||||
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
|
||||
customClassName="p-2 h-full"
|
||||
editorContentCustomClassNames="min-h-[35px]"
|
||||
debouncedUpdatesEnabled={false}
|
||||
onChange={(comment_json: Object, comment_html: string) => onCommentChange(comment_html)}
|
||||
commentAccessSpecifier={
|
||||
showAccessSpecifier
|
||||
? { accessValue: accessValue ?? "INTERNAL", onAccessChange, showAccessSpecifier, commentAccess }
|
||||
: undefined
|
||||
}
|
||||
mentionSuggestions={mentionSuggestions}
|
||||
mentionHighlights={mentionHighlights}
|
||||
submitButton={
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="!px-2.5 !py-1.5 !text-xs"
|
||||
disabled={isSubmitting || disabled}
|
||||
>
|
||||
{isSubmitting ? "Adding..." : "Comment"}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,194 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useMention, useUser, useWorkspace } from "hooks/store";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// icons
|
||||
import { Check, Globe2, Lock, MessageSquare, Pencil, Trash2, X } from "lucide-react";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { CommentReaction } from "components/issues";
|
||||
import { LiteTextEditorWithRef, LiteReadOnlyEditorWithRef } from "@plane/lite-text-editor";
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import type { IIssueActivity } from "@plane/types";
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
type Props = {
|
||||
comment: IIssueActivity;
|
||||
handleCommentDeletion: (comment: string) => void;
|
||||
onSubmit: (commentId: string, data: Partial<IIssueActivity>) => void;
|
||||
showAccessSpecifier?: boolean;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const CommentCard: React.FC<Props> = observer((props) => {
|
||||
const { comment, handleCommentDeletion, onSubmit, showAccessSpecifier = false, workspaceSlug } = props;
|
||||
const workspaceStore = useWorkspace();
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug)?.id as string;
|
||||
|
||||
// states
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
// refs
|
||||
const editorRef = React.useRef<any>(null);
|
||||
const showEditorRef = React.useRef<any>(null);
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
const { mentionHighlights, mentionSuggestions } = useMention();
|
||||
// form info
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
setFocus,
|
||||
watch,
|
||||
setValue,
|
||||
} = useForm<IIssueActivity>({
|
||||
defaultValues: comment,
|
||||
});
|
||||
|
||||
const onEnter = (formData: Partial<IIssueActivity>) => {
|
||||
if (isSubmitting) return;
|
||||
setIsEditing(false);
|
||||
|
||||
onSubmit(comment.id, formData);
|
||||
|
||||
editorRef.current?.setEditorValue(formData.comment_html);
|
||||
showEditorRef.current?.setEditorValue(formData.comment_html);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
isEditing && setFocus("comment");
|
||||
}, [isEditing, setFocus]);
|
||||
|
||||
return (
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={comment.actor_detail.avatar}
|
||||
alt={
|
||||
comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name
|
||||
}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-custom-border-200"
|
||||
/>
|
||||
) : (
|
||||
<div className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}>
|
||||
{comment.actor_detail.is_bot
|
||||
? comment.actor_detail.first_name.charAt(0)
|
||||
: comment.actor_detail.display_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-custom-background-80 px-0.5 py-px">
|
||||
<MessageSquare className="h-3.5 w-3.5 text-custom-text-200" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="text-xs">
|
||||
{comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">commented {calculateTimeAgo(comment.created_at)}</p>
|
||||
</div>
|
||||
<div className="issue-comments-section p-0">
|
||||
<form className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}>
|
||||
<div>
|
||||
<LiteTextEditorWithRef
|
||||
onEnterKeyPress={handleSubmit(onEnter)}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
||||
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
||||
ref={editorRef}
|
||||
value={watch("comment_html") ?? ""}
|
||||
debouncedUpdatesEnabled={false}
|
||||
customClassName="min-h-[50px] p-3 shadow-sm"
|
||||
onChange={(comment_json: Object, comment_html: string) => setValue("comment_html", comment_html)}
|
||||
mentionSuggestions={mentionSuggestions}
|
||||
mentionHighlights={mentionHighlights}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-1 self-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSubmit(onEnter)}
|
||||
disabled={isSubmitting}
|
||||
className="group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
|
||||
>
|
||||
<Check className="h-3 w-3 text-green-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="group rounded border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
|
||||
onClick={() => setIsEditing(false)}
|
||||
>
|
||||
<X className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<div className={`relative ${isEditing ? "hidden" : ""}`}>
|
||||
{showAccessSpecifier && (
|
||||
<div className="absolute right-2.5 top-2.5 z-[1] text-custom-text-300">
|
||||
{comment.access === "INTERNAL" ? <Lock className="h-3 w-3" /> : <Globe2 className="h-3 w-3" />}
|
||||
</div>
|
||||
)}
|
||||
<LiteReadOnlyEditorWithRef
|
||||
ref={showEditorRef}
|
||||
value={comment.comment_html ?? ""}
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
mentionHighlights={mentionHighlights}
|
||||
/>
|
||||
<CommentReaction projectId={comment.project} commentId={comment.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentUser?.id === comment.actor && (
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem onClick={() => setIsEditing(true)} className="flex items-center gap-1">
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit comment
|
||||
</CustomMenu.MenuItem>
|
||||
{showAccessSpecifier && (
|
||||
<>
|
||||
{comment.access === "INTERNAL" ? (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => onSubmit(comment.id, { access: "EXTERNAL" })}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Globe2 className="h-3 w-3" />
|
||||
Switch to public comment
|
||||
</CustomMenu.MenuItem>
|
||||
) : (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => onSubmit(comment.id, { access: "INTERNAL" })}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Lock className="h-3 w-3" />
|
||||
Switch to private comment
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleCommentDeletion(comment.id);
|
||||
}}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete comment
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,100 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// hooks
|
||||
import { useUser } from "hooks/store";
|
||||
import useCommentReaction from "hooks/use-comment-reaction";
|
||||
// ui
|
||||
// import { ReactionSelector } from "components/core";
|
||||
// helper
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// types
|
||||
import { IssueCommentReaction } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
projectId?: string | string[];
|
||||
commentId: string;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export const CommentReaction: FC<Props> = observer((props) => {
|
||||
const { projectId, commentId, readonly = false } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
// store hooks
|
||||
const { currentUser } = useUser();
|
||||
|
||||
const { commentReactions, groupedReactions, handleReactionCreate, handleReactionDelete } = useCommentReaction(
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
commentId
|
||||
);
|
||||
|
||||
const handleReactionClick = (reaction: string) => {
|
||||
if (!workspaceSlug || !projectId || !commentId) return;
|
||||
|
||||
const isSelected = commentReactions?.some(
|
||||
(r: IssueCommentReaction) => r.actor === currentUser?.id && r.reaction === reaction
|
||||
);
|
||||
|
||||
if (isSelected) {
|
||||
handleReactionDelete(reaction);
|
||||
} else {
|
||||
handleReactionCreate(reaction);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mt-2 flex items-center gap-1.5">
|
||||
{/* FIXME: have to replace this once the issue details page is ready --issue-detail-- */}
|
||||
{/* {!readonly && (
|
||||
<ReactionSelector
|
||||
size="md"
|
||||
position="top"
|
||||
value={
|
||||
commentReactions
|
||||
?.filter((reaction: IssueCommentReaction) => reaction.actor === currentUser?.id)
|
||||
.map((r: IssueCommentReaction) => r.reaction) || []
|
||||
}
|
||||
onSelect={handleReactionClick}
|
||||
/>
|
||||
)} */}
|
||||
|
||||
{Object.keys(groupedReactions || {}).map(
|
||||
(reaction) =>
|
||||
groupedReactions?.[reaction]?.length &&
|
||||
groupedReactions[reaction].length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={readonly}
|
||||
onClick={() => {
|
||||
handleReactionClick(reaction);
|
||||
}}
|
||||
key={reaction}
|
||||
className={`flex h-full items-center gap-1 rounded-md px-2 py-1 text-sm text-custom-text-100 ${
|
||||
commentReactions?.some(
|
||||
(r: IssueCommentReaction) => r.actor === currentUser?.id && r.reaction === reaction
|
||||
)
|
||||
? "bg-custom-primary-100/10"
|
||||
: "bg-custom-background-80"
|
||||
}`}
|
||||
>
|
||||
<span>{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
className={
|
||||
commentReactions?.some(
|
||||
(r: IssueCommentReaction) => r.actor === currentUser?.id && r.reaction === reaction
|
||||
)
|
||||
? "text-custom-primary-100"
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{groupedReactions?.[reaction].length}{" "}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./add-comment";
|
||||
export * from "./comment-card";
|
||||
export * from "./comment-reaction";
|
||||
@@ -1,8 +1,6 @@
|
||||
export * from "./attachment";
|
||||
export * from "./comment";
|
||||
export * from "./issue-modal";
|
||||
export * from "./view-select";
|
||||
export * from "./activity";
|
||||
export * from "./delete-issue-modal";
|
||||
export * from "./description-form";
|
||||
export * from "./issue-layouts";
|
||||
|
||||
@@ -78,7 +78,9 @@ export const InboxIssueMainContent: React.FC<Props> = observer((props) => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={!is_editable} />
|
||||
<div className="pb-12">
|
||||
<IssueActivity workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} disabled={!is_editable} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -9,8 +9,6 @@ import useToast from "hooks/use-toast";
|
||||
// types
|
||||
import { TIssue } from "@plane/types";
|
||||
import { TIssueOperations } from "../root";
|
||||
// ui
|
||||
import { Loader } from "@plane/ui";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
|
||||
@@ -105,46 +103,28 @@ export const InboxIssueDetailRoot: FC<TInboxIssueDetailRoot> = (props) => {
|
||||
// issue details
|
||||
const issue = getIssueById(issueId);
|
||||
|
||||
if (!issue) return <></>;
|
||||
return (
|
||||
<>
|
||||
{issue ? (
|
||||
<div className="flex h-full overflow-hidden">
|
||||
<div className="h-full w-2/3 space-y-5 divide-y-2 divide-custom-border-300 overflow-y-auto p-5">
|
||||
<InboxIssueMainContent
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
inboxId={inboxId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
is_editable={is_editable}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5">
|
||||
<InboxIssueDetailsSidebar
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
is_editable={is_editable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Loader className="flex h-full gap-5 p-5">
|
||||
<div className="basis-2/3 space-y-2">
|
||||
<Loader.Item height="30px" width="40%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
<Loader.Item height="15px" width="60%" />
|
||||
<Loader.Item height="15px" width="40%" />
|
||||
</div>
|
||||
<div className="basis-1/3 space-y-3">
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
<Loader.Item height="30px" />
|
||||
</div>
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
<div className="flex h-full overflow-hidden">
|
||||
<div className="h-full w-2/3 space-y-5 divide-y-2 divide-custom-border-300 overflow-y-auto p-5">
|
||||
<InboxIssueMainContent
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
inboxId={inboxId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
is_editable={is_editable}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full w-1/3 space-y-5 overflow-hidden border-l border-custom-border-300 py-5">
|
||||
<InboxIssueDetailsSidebar
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
is_editable={is_editable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import React from "react";
|
||||
// import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { CalendarDays, Signal, Tag } from "lucide-react";
|
||||
import { CalendarCheck2, Signal, Tag } from "lucide-react";
|
||||
// hooks
|
||||
import { useIssueDetail, useProject } from "hooks/store";
|
||||
import { useIssueDetail, useProject, useProjectState } from "hooks/store";
|
||||
// components
|
||||
import { IssueLabel, TIssueOperations } from "components/issues";
|
||||
import { PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns";
|
||||
// ui
|
||||
import { CustomDatePicker } from "components/ui";
|
||||
import { DateDropdown, PriorityDropdown, ProjectMemberDropdown, StateDropdown } from "components/dropdowns";
|
||||
// icons
|
||||
import { DoubleCircleIcon, StateGroupIcon, UserGroupIcon } from "@plane/ui";
|
||||
// types
|
||||
// helper
|
||||
import { renderFormattedPayloadDate } from "helpers/date-time.helper";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
@@ -23,12 +21,9 @@ type Props = {
|
||||
|
||||
export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, issueOperations, is_editable } = props;
|
||||
// router
|
||||
// FIXME: Check if we need this. Previously it was used to render Project Identifier conditionally.
|
||||
// const router = useRouter();
|
||||
// const { inboxIssueId } = router.query;
|
||||
// store hooks
|
||||
const { getProjectById } = useProject();
|
||||
const { projectStates } = useProjectState();
|
||||
const {
|
||||
issue: { getIssueById },
|
||||
} = useIssueDetail();
|
||||
@@ -41,11 +36,15 @@ export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const minDate = issue.start_date ? new Date(issue.start_date) : null;
|
||||
minDate?.setDate(minDate.getDate());
|
||||
|
||||
const currentIssueState = projectStates?.find((s) => s.id === issue.state_id);
|
||||
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col divide-y-2 divide-custom-border-200 overflow-hidden">
|
||||
<div className="flex items-center justify-between px-5 pb-3">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<StateGroupIcon className="h-4 w-4" stateGroup="backlog" color="#ff7700" />
|
||||
{currentIssueState && (
|
||||
<StateGroupIcon className="h-4 w-4" stateGroup={currentIssueState.group} color={currentIssueState.color} />
|
||||
)}
|
||||
<h4 className="text-lg font-medium text-custom-text-300">
|
||||
{projectDetails?.identifier}-{issue?.sequence_id}
|
||||
</h4>
|
||||
@@ -53,86 +52,103 @@ export const InboxIssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
|
||||
<div className="h-full w-full overflow-y-auto px-5">
|
||||
<h5 className="text-sm font-medium my-4">Properties</h5>
|
||||
<div className={`divide-y-2 divide-custom-border-200 ${!is_editable ? "opacity-60" : ""}`}>
|
||||
<div className="py-1">
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* State */}
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>State</p>
|
||||
</div>
|
||||
<div className="h-5 sm:w-1/2">
|
||||
<StateDropdown
|
||||
value={issue?.state_id ?? undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="background-with-text"
|
||||
/>
|
||||
<span>State</span>
|
||||
</div>
|
||||
<StateDropdown
|
||||
value={issue?.state_id ?? undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="w-3/5 flex-grow group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName="text-sm"
|
||||
dropdownArrow
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
{/* Assignee */}
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Assignees</p>
|
||||
</div>
|
||||
<div className="h-5 sm:w-1/2">
|
||||
<ProjectMemberDropdown
|
||||
value={issue?.assignee_ids ?? undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
|
||||
disabled={!is_editable}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
placeholder="Assignees"
|
||||
multiple
|
||||
buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "background-with-text"}
|
||||
buttonClassName={issue?.assignee_ids?.length > 0 ? "hover:bg-transparent px-0" : ""}
|
||||
/>
|
||||
<span>Assignees</span>
|
||||
</div>
|
||||
<ProjectMemberDropdown
|
||||
value={issue?.assignee_ids ?? undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
|
||||
disabled={!is_editable}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
placeholder="Add assignees"
|
||||
multiple
|
||||
buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"}
|
||||
className="w-3/5 flex-grow group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm justify-between ${
|
||||
issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400"
|
||||
}`}
|
||||
hideIcon={issue.assignee_ids?.length === 0}
|
||||
dropdownArrow
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
{/* Priority */}
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<Signal className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Priority</p>
|
||||
</div>
|
||||
<div className="h-5 sm:w-1/2">
|
||||
<PriorityDropdown
|
||||
value={issue?.priority || undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="background-with-text"
|
||||
/>
|
||||
<span>Priority</span>
|
||||
</div>
|
||||
<PriorityDropdown
|
||||
value={issue?.priority || undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="border-with-text"
|
||||
className="w-3/5 flex-grow rounded px-2 hover:bg-custom-background-80"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName="w-min h-auto whitespace-nowrap"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`divide-y-2 divide-custom-border-200 ${!is_editable ? "opacity-60" : ""}`}>
|
||||
<div className="py-1">
|
||||
<div className={`divide-y-2 divide-custom-border-200 mt-3 ${!is_editable ? "opacity-60" : ""}`}>
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* Due Date */}
|
||||
<div className="flex flex-wrap items-center py-2">
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:basis-1/2">
|
||||
<CalendarDays className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Due date</p>
|
||||
</div>
|
||||
<div className="sm:basis-1/2">
|
||||
<CustomDatePicker
|
||||
placeholder="Due date"
|
||||
value={issue.target_date || undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { target_date: val })}
|
||||
className="border-none bg-custom-background-80"
|
||||
minDate={minDate ?? undefined}
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CalendarCheck2 className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Due date</span>
|
||||
</div>
|
||||
<DateDropdown
|
||||
placeholder="Add due date"
|
||||
value={issue.target_date}
|
||||
onChange={(val) =>
|
||||
issueOperations.update(workspaceSlug, projectId, issueId, {
|
||||
target_date: val ? renderFormattedPayloadDate(val) : null,
|
||||
})
|
||||
}
|
||||
minDate={minDate ?? undefined}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="w-3/5 flex-grow group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
|
||||
hideIcon
|
||||
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
{/* Labels */}
|
||||
<div className={`flex flex-wrap items-start py-2 ${!is_editable ? "opacity-60" : ""}`}>
|
||||
<div className="flex items-center gap-x-2 text-sm text-custom-text-200 sm:w-1/2">
|
||||
<div className="flex items-center gap-2 min-h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<Tag className="h-4 w-4 flex-shrink-0" />
|
||||
<p>Label</p>
|
||||
<span>Labels</span>
|
||||
</div>
|
||||
<div className="space-y-1 sm:w-1/2">
|
||||
<div className="w-3/5 flex-grow min-h-8 h-full pt-1">
|
||||
<IssueLabel
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { FC, useState, Fragment, useEffect } from "react";
|
||||
import { Plus, X } from "lucide-react";
|
||||
import { Plus, X, Loader } from "lucide-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { TwitterPicker } from "react-color";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
@@ -138,6 +138,7 @@ export const LabelCreate: FC<ILabelCreate> = (props) => {
|
||||
hasError={Boolean(errors.name)}
|
||||
placeholder="Title"
|
||||
className="w-full"
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
@@ -150,7 +151,7 @@ export const LabelCreate: FC<ILabelCreate> = (props) => {
|
||||
<X className="h-4 w-4 text-white" />
|
||||
</button>
|
||||
<button type="submit" className="grid place-items-center rounded bg-green-500 p-1.5" disabled={isSubmitting}>
|
||||
<Plus className="h-4 w-4 text-white" />
|
||||
{isSubmitting ? <Loader className="h-4 w-4 text-white spin" /> : <Plus className="h-4 w-4 text-white" />}
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
@@ -120,12 +120,23 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
||||
},
|
||||
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
||||
try {
|
||||
await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||
setToastAlert({
|
||||
title: "Cycle added to issue successfully",
|
||||
type: "success",
|
||||
message: "Issue added to issue successfully",
|
||||
});
|
||||
await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds)
|
||||
.then((res) => {
|
||||
updateIssue(workspaceSlug, projectId, res.id, res);
|
||||
fetchIssue(workspaceSlug, projectId, res.id);
|
||||
setToastAlert({
|
||||
title: "Cycle added to issue successfully",
|
||||
type: "success",
|
||||
message: "Issue added to issue successfully",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Selected issues could not be added to the cycle. Please try again.",
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Cycle add to issue failed",
|
||||
@@ -152,12 +163,23 @@ export const IssueDetailRoot: FC<TIssueDetailRoot> = (props) => {
|
||||
},
|
||||
addIssueToModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {
|
||||
try {
|
||||
await addIssueToModule(workspaceSlug, projectId, moduleId, issueIds);
|
||||
setToastAlert({
|
||||
title: "Module added to issue successfully",
|
||||
type: "success",
|
||||
message: "Module added to issue successfully",
|
||||
});
|
||||
await addIssueToModule(workspaceSlug, projectId, moduleId, issueIds)
|
||||
.then((res) => {
|
||||
updateIssue(workspaceSlug, projectId, res.id, res);
|
||||
fetchIssue(workspaceSlug, projectId, res.id);
|
||||
setToastAlert({
|
||||
title: "Module added to issue successfully",
|
||||
type: "success",
|
||||
message: "Module added to issue successfully",
|
||||
});
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Selected issues could not be added to the module. Please try again.",
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Module add to issue failed",
|
||||
|
||||
@@ -50,38 +50,10 @@ type Props = {
|
||||
issueOperations: TIssueOperations;
|
||||
is_archived: boolean;
|
||||
is_editable: boolean;
|
||||
fieldsToShow?: (
|
||||
| "state"
|
||||
| "assignee"
|
||||
| "priority"
|
||||
| "estimate"
|
||||
| "parent"
|
||||
| "blocker"
|
||||
| "blocked"
|
||||
| "startDate"
|
||||
| "dueDate"
|
||||
| "cycle"
|
||||
| "module"
|
||||
| "label"
|
||||
| "link"
|
||||
| "delete"
|
||||
| "all"
|
||||
| "subscribe"
|
||||
| "duplicate"
|
||||
| "relates_to"
|
||||
)[];
|
||||
};
|
||||
|
||||
export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
issueOperations,
|
||||
is_archived,
|
||||
is_editable,
|
||||
fieldsToShow = ["all"],
|
||||
} = props;
|
||||
const { workspaceSlug, projectId, issueId, issueOperations, is_archived, is_editable } = props;
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { inboxIssueId } = router.query;
|
||||
@@ -153,21 +125,19 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{currentUser && !is_archived && (fieldsToShow.includes("all") || fieldsToShow.includes("subscribe")) && (
|
||||
{currentUser && !is_archived && (
|
||||
<IssueSubscription workspaceSlug={workspaceSlug} projectId={projectId} issueId={issueId} />
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-custom-border-200 p-2 shadow-sm duration-300 hover:bg-custom-background-90 focus:border-custom-primary focus:outline-none focus:ring-1 focus:ring-custom-primary"
|
||||
onClick={handleCopyText}
|
||||
>
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-custom-border-200 p-2 shadow-sm duration-300 hover:bg-custom-background-90 focus:border-custom-primary focus:outline-none focus:ring-1 focus:ring-custom-primary"
|
||||
onClick={handleCopyText}
|
||||
>
|
||||
<LinkIcon className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
{is_editable && (fieldsToShow.includes("all") || fieldsToShow.includes("delete")) && (
|
||||
{is_editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-md border border-red-500 p-2 text-red-500 shadow-sm duration-300 hover:bg-red-500/20 focus:outline-none"
|
||||
@@ -183,150 +153,137 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
<h5 className="text-sm font-medium mt-6">Properties</h5>
|
||||
{/* TODO: render properties using a common component */}
|
||||
<div className={`mt-3 space-y-2 ${!is_editable ? "opacity-60" : ""}`}>
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("state")) && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>State</span>
|
||||
</div>
|
||||
<StateDropdown
|
||||
value={issue?.state_id ?? undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="w-3/5 flex-grow group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName="text-sm"
|
||||
dropdownArrow
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Assignees</span>
|
||||
</div>
|
||||
<ProjectMemberDropdown
|
||||
value={issue?.assignee_ids ?? undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
|
||||
disabled={!is_editable}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
placeholder="Add assignees"
|
||||
multiple
|
||||
buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"}
|
||||
className="w-3/5 flex-grow group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm justify-between ${
|
||||
issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400"
|
||||
}`}
|
||||
hideIcon={issue.assignee_ids?.length === 0}
|
||||
dropdownArrow
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<Signal className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Priority</span>
|
||||
</div>
|
||||
<PriorityDropdown
|
||||
value={issue?.priority || undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="border-with-text"
|
||||
className="w-3/5 flex-grow rounded px-2 hover:bg-custom-background-80"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName="w-min h-auto whitespace-nowrap"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CalendarClock className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Start date</span>
|
||||
</div>
|
||||
<DateDropdown
|
||||
placeholder="Add start date"
|
||||
value={issue.start_date}
|
||||
onChange={(val) =>
|
||||
issueOperations.update(workspaceSlug, projectId, issueId, {
|
||||
start_date: val ? renderFormattedPayloadDate(val) : null,
|
||||
})
|
||||
}
|
||||
maxDate={maxDate ?? undefined}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="w-3/5 flex-grow group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
|
||||
hideIcon
|
||||
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CalendarCheck2 className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Due date</span>
|
||||
</div>
|
||||
<DateDropdown
|
||||
placeholder="Add due date"
|
||||
value={issue.target_date}
|
||||
onChange={(val) =>
|
||||
issueOperations.update(workspaceSlug, projectId, issueId, {
|
||||
target_date: val ? renderFormattedPayloadDate(val) : null,
|
||||
})
|
||||
}
|
||||
minDate={minDate ?? undefined}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="w-3/5 flex-grow group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
|
||||
hideIcon
|
||||
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{areEstimatesEnabledForCurrentProject && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<DoubleCircleIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>State</span>
|
||||
<Triangle className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Estimate</span>
|
||||
</div>
|
||||
<StateDropdown
|
||||
value={issue?.state_id ?? undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { state_id: val })}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
<EstimateDropdown
|
||||
value={issue?.estimate_point !== null ? issue.estimate_point : null}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })}
|
||||
projectId={projectId}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="w-3/5 flex-grow group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName="text-sm"
|
||||
buttonClassName={`text-sm ${issue?.estimate_point !== null ? "" : "text-custom-text-400"}`}
|
||||
placeholder="None"
|
||||
hideIcon
|
||||
dropdownArrow
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("assignee")) && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<UserGroupIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Assignees</span>
|
||||
</div>
|
||||
<ProjectMemberDropdown
|
||||
value={issue?.assignee_ids ?? undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { assignee_ids: val })}
|
||||
disabled={!is_editable}
|
||||
projectId={projectId?.toString() ?? ""}
|
||||
placeholder="Add assignees"
|
||||
multiple
|
||||
buttonVariant={issue?.assignee_ids?.length > 0 ? "transparent-without-text" : "transparent-with-text"}
|
||||
className="w-3/5 flex-grow group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm justify-between ${
|
||||
issue?.assignee_ids.length > 0 ? "" : "text-custom-text-400"
|
||||
}`}
|
||||
hideIcon={issue.assignee_ids?.length === 0}
|
||||
dropdownArrow
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("priority")) && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<Signal className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Priority</span>
|
||||
</div>
|
||||
<PriorityDropdown
|
||||
value={issue?.priority || undefined}
|
||||
onChange={(val) => issueOperations.update(workspaceSlug, projectId, issueId, { priority: val })}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="border-with-text"
|
||||
className="w-3/5 flex-grow rounded px-2 hover:bg-custom-background-80"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName="w-min h-auto whitespace-nowrap"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("startDate")) && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CalendarClock className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Start date</span>
|
||||
</div>
|
||||
<DateDropdown
|
||||
placeholder="Add start date"
|
||||
value={issue.start_date}
|
||||
onChange={(val) =>
|
||||
issueOperations.update(workspaceSlug, projectId, issueId, {
|
||||
start_date: val ? renderFormattedPayloadDate(val) : null,
|
||||
})
|
||||
}
|
||||
maxDate={maxDate ?? undefined}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="w-3/5 flex-grow group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm ${issue?.start_date ? "" : "text-custom-text-400"}`}
|
||||
hideIcon
|
||||
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("dueDate")) && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CalendarCheck2 className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Due date</span>
|
||||
</div>
|
||||
<DateDropdown
|
||||
placeholder="Add due date"
|
||||
value={issue.target_date}
|
||||
onChange={(val) =>
|
||||
issueOperations.update(workspaceSlug, projectId, issueId, {
|
||||
target_date: val ? renderFormattedPayloadDate(val) : null,
|
||||
})
|
||||
}
|
||||
minDate={minDate ?? undefined}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="w-3/5 flex-grow group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm ${issue?.target_date ? "" : "text-custom-text-400"}`}
|
||||
hideIcon
|
||||
clearIconClassName="h-3 w-3 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("estimate")) &&
|
||||
areEstimatesEnabledForCurrentProject && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<Triangle className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Estimate</span>
|
||||
</div>
|
||||
<EstimateDropdown
|
||||
value={issue?.estimate_point !== null ? issue.estimate_point : null}
|
||||
onChange={(val) =>
|
||||
issueOperations.update(workspaceSlug, projectId, issueId, { estimate_point: val })
|
||||
}
|
||||
projectId={projectId}
|
||||
disabled={!is_editable}
|
||||
buttonVariant="transparent-with-text"
|
||||
className="w-3/5 flex-grow group"
|
||||
buttonContainerClassName="w-full text-left"
|
||||
buttonClassName={`text-sm ${issue?.estimate_point !== null ? "" : "text-custom-text-400"}`}
|
||||
placeholder="None"
|
||||
hideIcon
|
||||
dropdownArrow
|
||||
dropdownArrowClassName="h-3.5 w-3.5 hidden group-hover:inline"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("module")) && projectDetails?.module_view && (
|
||||
{projectDetails?.module_view && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<DiceIcon className="h-4 w-4 flex-shrink-0" />
|
||||
@@ -343,7 +300,7 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("cycle")) && projectDetails?.cycle_view && (
|
||||
{projectDetails?.cycle_view && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<ContrastIcon className="h-4 w-4 flex-shrink-0" />
|
||||
@@ -360,117 +317,103 @@ export const IssueDetailsSidebar: React.FC<Props> = observer((props) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("parent")) && (
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<LayoutPanelTop className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Parent</span>
|
||||
</div>
|
||||
<IssueParentSelect
|
||||
className="w-3/5 flex-grow h-full"
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
<div className="flex items-center gap-2 h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<LayoutPanelTop className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Parent</span>
|
||||
</div>
|
||||
)}
|
||||
<IssueParentSelect
|
||||
className="w-3/5 flex-grow h-full"
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
issueOperations={issueOperations}
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("relates_to")) && (
|
||||
<div className="flex items-center gap-2 min-h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<RelatedIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Relates to</span>
|
||||
</div>
|
||||
<IssueRelationSelect
|
||||
className="w-3/5 flex-grow min-h-8 h-full"
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
relationKey="relates_to"
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
<div className="flex items-center gap-2 min-h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<RelatedIcon className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Relates to</span>
|
||||
</div>
|
||||
)}
|
||||
<IssueRelationSelect
|
||||
className="w-3/5 flex-grow min-h-8 h-full"
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
relationKey="relates_to"
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocker")) && (
|
||||
<div className="flex items-center gap-2 min-h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<XCircle className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Blocking</span>
|
||||
</div>
|
||||
<IssueRelationSelect
|
||||
className="w-3/5 flex-grow min-h-8 h-full"
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
relationKey="blocking"
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
<div className="flex items-center gap-2 min-h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<XCircle className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Blocking</span>
|
||||
</div>
|
||||
)}
|
||||
<IssueRelationSelect
|
||||
className="w-3/5 flex-grow min-h-8 h-full"
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
relationKey="blocking"
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("blocked")) && (
|
||||
<div className="flex items-center gap-2 min-h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CircleDot className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Blocked by</span>
|
||||
</div>
|
||||
<IssueRelationSelect
|
||||
className="w-3/5 flex-grow min-h-8 h-full"
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
relationKey="blocked_by"
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
<div className="flex items-center gap-2 min-h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CircleDot className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Blocked by</span>
|
||||
</div>
|
||||
)}
|
||||
<IssueRelationSelect
|
||||
className="w-3/5 flex-grow min-h-8 h-full"
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
relationKey="blocked_by"
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("duplicate")) && (
|
||||
<div className="flex items-center gap-2 min-h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CopyPlus className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Duplicate of</span>
|
||||
</div>
|
||||
<IssueRelationSelect
|
||||
className="w-3/5 flex-grow min-h-8 h-full"
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
relationKey="duplicate"
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
<div className="flex items-center gap-2 min-h-8">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<CopyPlus className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Duplicate of</span>
|
||||
</div>
|
||||
)}
|
||||
<IssueRelationSelect
|
||||
className="w-3/5 flex-grow min-h-8 h-full"
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
relationKey="duplicate"
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("label")) && (
|
||||
<div className="flex items-center gap-2 min-h-8 py-2">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<Tag className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Labels</span>
|
||||
</div>
|
||||
<div className="w-3/5 flex-grow min-h-8 h-full">
|
||||
<IssueLabel
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 min-h-8 py-2">
|
||||
<div className="flex items-center gap-1 w-2/5 flex-shrink-0 text-sm text-custom-text-300">
|
||||
<Tag className="h-4 w-4 flex-shrink-0" />
|
||||
<span>Labels</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-3/5 flex-grow min-h-8 h-full">
|
||||
<IssueLabel
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(fieldsToShow.includes("all") || fieldsToShow.includes("link")) && (
|
||||
<IssueLinkRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
)}
|
||||
<IssueLinkRoot
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
disabled={!is_editable}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
// hooks
|
||||
import { useApplication, useIssues, useUser } from "hooks/store";
|
||||
import { useApplication, useIssueDetail, useIssues, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { EmptyState } from "components/common";
|
||||
@@ -29,6 +29,7 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
const [cycleIssuesListModal, setCycleIssuesListModal] = useState(false);
|
||||
// store hooks
|
||||
const { issues } = useIssues(EIssuesStoreType.CYCLE);
|
||||
const { updateIssue, fetchIssue } = useIssueDetail();
|
||||
const {
|
||||
commandPalette: { toggleCreateIssueModal },
|
||||
eventTracker: { setTrackElement },
|
||||
@@ -44,13 +45,19 @@ export const CycleEmptyState: React.FC<Props> = observer((props) => {
|
||||
|
||||
const issueIds = data.map((i) => i.id);
|
||||
|
||||
await issues.addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds).catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Selected issues could not be added to the cycle. Please try again.",
|
||||
await issues
|
||||
.addIssueToCycle(workspaceSlug.toString(), projectId, cycleId.toString(), issueIds)
|
||||
.then((res) => {
|
||||
updateIssue(workspaceSlug, projectId, res.id, res);
|
||||
fetchIssue(workspaceSlug, projectId, res.id);
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Selected issues could not be added to the cycle. Please try again.",
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const isEditingAllowed = !!userRole && userRole >= EUserProjectRoles.MEMBER;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
// hooks
|
||||
import { useApplication, useIssues, useUser } from "hooks/store";
|
||||
import { useApplication, useIssueDetail, useIssues, useUser } from "hooks/store";
|
||||
import useToast from "hooks/use-toast";
|
||||
// components
|
||||
import { EmptyState } from "components/common";
|
||||
@@ -29,6 +29,8 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
const [moduleIssuesListModal, setModuleIssuesListModal] = useState(false);
|
||||
// store hooks
|
||||
const { issues } = useIssues(EIssuesStoreType.MODULE);
|
||||
const { updateIssue, fetchIssue } = useIssueDetail();
|
||||
|
||||
const {
|
||||
commandPalette: { toggleCreateIssueModal },
|
||||
eventTracker: { setTrackElement },
|
||||
@@ -43,9 +45,12 @@ export const ModuleEmptyState: React.FC<Props> = observer((props) => {
|
||||
if (!workspaceSlug || !projectId || !moduleId) return;
|
||||
|
||||
const issueIds = data.map((i) => i.id);
|
||||
|
||||
await issues
|
||||
.addIssueToModule(workspaceSlug.toString(), projectId?.toString(), moduleId.toString(), issueIds)
|
||||
.then((res) => {
|
||||
updateIssue(workspaceSlug, projectId, res.id, res);
|
||||
fetchIssue(workspaceSlug, projectId, res.id);
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
|
||||
@@ -7,14 +7,14 @@ import { AppliedFiltersList } from "components/issues";
|
||||
// types
|
||||
import { IIssueFilterOptions } from "@plane/types";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { useWorskspaceIssueProperties } from "hooks/use-worskspace-issue-properties";
|
||||
import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties";
|
||||
|
||||
export const ProfileIssuesAppliedFiltersRoot: React.FC = observer(() => {
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, userId } = router.query;
|
||||
//swr hook for fetching issue properties
|
||||
useWorskspaceIssueProperties(workspaceSlug);
|
||||
useWorkspaceIssueProperties(workspaceSlug);
|
||||
// store hooks
|
||||
const {
|
||||
issuesFilter: { issueFilters, updateFilters },
|
||||
|
||||
@@ -81,6 +81,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
projectId={issue.project_id}
|
||||
disabled={isReadOnly}
|
||||
buttonVariant="border-with-text"
|
||||
tooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
@@ -94,6 +95,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
disabled={isReadOnly}
|
||||
buttonVariant="border-without-text"
|
||||
buttonClassName="border"
|
||||
tooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
@@ -121,6 +123,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
placeholder="Start date"
|
||||
buttonVariant={issue.start_date ? "border-with-text" : "border-without-text"}
|
||||
disabled={isReadOnly}
|
||||
tooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
@@ -136,6 +139,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
placeholder="Due date"
|
||||
buttonVariant={issue.target_date ? "border-with-text" : "border-without-text"}
|
||||
disabled={isReadOnly}
|
||||
tooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
@@ -151,6 +155,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
multiple
|
||||
buttonVariant={issue.assignee_ids?.length > 0 ? "transparent-without-text" : "border-without-text"}
|
||||
buttonClassName={issue.assignee_ids?.length > 0 ? "hover:bg-transparent px-0" : ""}
|
||||
tooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
@@ -165,6 +170,7 @@ export const IssueProperties: React.FC<IIssueProperties> = observer((props) => {
|
||||
projectId={issue.project_id}
|
||||
disabled={isReadOnly}
|
||||
buttonVariant="border-with-text"
|
||||
tooltip
|
||||
/>
|
||||
</div>
|
||||
</WithDisplayPropertiesHOC>
|
||||
|
||||
@@ -2,9 +2,10 @@ import React, { useCallback, useMemo } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import useSWR from "swr";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
// hooks
|
||||
import { useApplication, useGlobalView, useIssues, useProject, useUser } from "hooks/store";
|
||||
import { useWorskspaceIssueProperties } from "hooks/use-worskspace-issue-properties";
|
||||
import { useWorkspaceIssueProperties } from "hooks/use-workspace-issue-properties";
|
||||
// components
|
||||
import { GlobalViewsAppliedFiltersRoot, IssuePeekOverview } from "components/issues";
|
||||
import { SpreadsheetView } from "components/issues/issue-layouts";
|
||||
@@ -17,7 +18,7 @@ import { TIssue, IIssueDisplayFilterOptions } from "@plane/types";
|
||||
import { EIssueActions } from "../types";
|
||||
// constants
|
||||
import { EUserProjectRoles } from "constants/project";
|
||||
import { EIssueFilterType, EIssuesStoreType } from "constants/issue";
|
||||
import { EIssueFilterType, EIssuesStoreType, ISSUE_DISPLAY_FILTERS_BY_LAYOUT } from "constants/issue";
|
||||
import { ALL_ISSUES_EMPTY_STATE_DETAILS, EUserWorkspaceRoles } from "constants/workspace";
|
||||
|
||||
export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
@@ -25,7 +26,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, globalViewId } = router.query;
|
||||
//swr hook for fetching issue properties
|
||||
useWorskspaceIssueProperties(workspaceSlug);
|
||||
useWorkspaceIssueProperties(workspaceSlug);
|
||||
// store
|
||||
const { commandPalette: commandPaletteStore } = useApplication();
|
||||
const {
|
||||
@@ -47,6 +48,40 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
|
||||
const emptyStateImage = getEmptyStateImagePath("all-issues", currentView, currentUser?.theme.theme === "light");
|
||||
|
||||
// filter init from the query params
|
||||
|
||||
const routerFilterParams = () => {
|
||||
if (
|
||||
workspaceSlug &&
|
||||
globalViewId &&
|
||||
["all-issues", "assigned", "created", "subscribed"].includes(globalViewId.toString())
|
||||
) {
|
||||
const routerQueryParams = { ...router.query };
|
||||
const { ["workspaceSlug"]: _workspaceSlug, ["globalViewId"]: _globalViewId, ...filters } = routerQueryParams;
|
||||
|
||||
let issueFilters: any = {};
|
||||
Object.keys(filters).forEach((key) => {
|
||||
const filterKey: any = key;
|
||||
const filterValue = filters[key]?.toString() || undefined;
|
||||
if (
|
||||
ISSUE_DISPLAY_FILTERS_BY_LAYOUT.my_issues.spreadsheet.filters.includes(filterKey) &&
|
||||
filterKey &&
|
||||
filterValue
|
||||
)
|
||||
issueFilters = { ...issueFilters, [filterKey]: filterValue.split(",") };
|
||||
});
|
||||
|
||||
if (!isEmpty(filters))
|
||||
updateFilters(
|
||||
workspaceSlug.toString(),
|
||||
undefined,
|
||||
EIssueFilterType.FILTERS,
|
||||
issueFilters,
|
||||
globalViewId.toString()
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
useSWR(workspaceSlug ? `WORKSPACE_GLOBAL_VIEWS${workspaceSlug}` : null, async () => {
|
||||
if (workspaceSlug) {
|
||||
await fetchAllGlobalViews(workspaceSlug.toString());
|
||||
@@ -60,6 +95,7 @@ export const AllIssueLayoutRoot: React.FC = observer(() => {
|
||||
await fetchAllGlobalViews(workspaceSlug.toString());
|
||||
await fetchFilters(workspaceSlug.toString(), globalViewId.toString());
|
||||
await fetchIssues(workspaceSlug.toString(), globalViewId.toString(), issueIds ? "mutation" : "init-loader");
|
||||
routerFilterParams();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import Link from "next/link";
|
||||
import { History } from "lucide-react";
|
||||
// packages
|
||||
import { Loader, Tooltip } from "@plane/ui";
|
||||
// components
|
||||
import { ActivityIcon, ActivityMessage } from "components/core";
|
||||
import { IssueCommentCard } from "./comment-card";
|
||||
// helpers
|
||||
import { renderFormattedTime, renderFormattedDate, calculateTimeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import { IIssueActivity, IUser } from "@plane/types";
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
|
||||
interface IIssueActivityCard {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
user: IUser | null;
|
||||
issueActivity: string[] | undefined;
|
||||
issueCommentUpdate: (comment: any) => void;
|
||||
issueCommentRemove: (commentId: string) => void;
|
||||
issueCommentReactionCreate: (commentId: string, reaction: string) => void;
|
||||
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
|
||||
}
|
||||
|
||||
export const IssueActivityCard: FC<IIssueActivityCard> = (props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
user,
|
||||
issueActivity,
|
||||
issueCommentUpdate,
|
||||
issueCommentRemove,
|
||||
issueCommentReactionCreate,
|
||||
issueCommentReactionRemove,
|
||||
} = props;
|
||||
|
||||
const { activity } = useIssueDetail();
|
||||
|
||||
return (
|
||||
<div className="flow-root">
|
||||
{/* FIXME: --issue-detail-- */}
|
||||
{/* <ul role="list" className="-mb-4">
|
||||
{issueActivity ? (
|
||||
issueActivity.length > 0 &&
|
||||
issueActivity.map((activityId, index) => {
|
||||
// determines what type of action is performed
|
||||
const activityItem = activity.getActivityById(activityId) as IIssueActivity;
|
||||
const message = activityItem.field ? <ActivityMessage activity={activityItem} /> : <span>
|
||||
created <IssueLink activity={activity} />
|
||||
</span>;
|
||||
|
||||
if ("field" in activityItem && activityItem.field !== "updated_by") {
|
||||
return (
|
||||
<li key={activityItem.id}>
|
||||
<div className="relative pb-1">
|
||||
{issueActivity.length > 1 && index !== issueActivity.length - 1 ? (
|
||||
<span
|
||||
className="absolute left-5 top-5 -ml-[1.5px] h-full w-0.5 bg-custom-background-100"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : null}
|
||||
<div className="relative flex items-start space-x-2">
|
||||
<div>
|
||||
<div className="relative px-1.5">
|
||||
<div className="mt-1.5">
|
||||
<div className="ring-6 flex h-7 w-7 items-center justify-center rounded-full bg-custom-background-100 text-custom-text-200 ring-white">
|
||||
{activityItem.field ? (
|
||||
activityItem.new_value === "restore" ? (
|
||||
<History className="h-3.5 w-3.5 text-custom-text-200" />
|
||||
) : (
|
||||
<ActivityIcon activity={activityItem} />
|
||||
)
|
||||
) : activityItem.actor_detail.avatar && activityItem.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={activityItem.actor_detail.avatar}
|
||||
alt={activityItem.actor_detail.display_name}
|
||||
height={24}
|
||||
width={24}
|
||||
className="h-full w-full rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-700 text-xs text-white`}
|
||||
>
|
||||
{activityItem.actor_detail.is_bot
|
||||
? activityItem.actor_detail.first_name.charAt(0)
|
||||
: activityItem.actor_detail.display_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 py-3">
|
||||
<div className="flex gap-1 break-words text-xs text-custom-text-200">
|
||||
{activityItem.field === "archived_at" && activityItem.new_value !== "restore" ? (
|
||||
<span className="text-gray font-medium">Plane</span>
|
||||
) : activityItem.actor_detail.is_bot ? (
|
||||
<span className="text-gray font-medium">{activityItem.actor_detail.first_name} Bot</span>
|
||||
) : (
|
||||
<Link href={`/${workspaceSlug}/profile/${activityItem.actor_detail.id}`}>
|
||||
<span className="text-gray font-medium">
|
||||
{activityItem.actor_detail.is_bot
|
||||
? activityItem.actor_detail.first_name
|
||||
: activityItem.actor_detail.display_name}
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
{message}
|
||||
<Tooltip
|
||||
tooltipContent={`${renderFormattedDate(activityItem.created_at)}, ${renderFormattedTime(
|
||||
activityItem.created_at
|
||||
)}`}
|
||||
>
|
||||
<span className="whitespace-nowrap">{calculateTimeAgo(activityItem.created_at)}</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
} else if ("comment_html" in activityItem)
|
||||
return (
|
||||
<div key={activityItem.id} className="mt-4">
|
||||
<IssueCommentCard
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
user={user}
|
||||
comment={activityItem}
|
||||
onSubmit={issueCommentUpdate}
|
||||
handleCommentDeletion={issueCommentRemove}
|
||||
issueCommentReactionCreate={issueCommentReactionCreate}
|
||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
||||
// showAccessSpecifier={showAccessSpecifier}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Loader className="mb-3 space-y-3">
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
<Loader.Item height="20px" />
|
||||
</Loader>
|
||||
)}
|
||||
</ul> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,221 +0,0 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Check, Globe2, Lock, MessageSquare, Pencil, Trash2, X } from "lucide-react";
|
||||
// hooks
|
||||
import { useMention, useWorkspace } from "hooks/store";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// ui
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { LiteTextEditorWithRef, LiteReadOnlyEditorWithRef } from "@plane/lite-text-editor";
|
||||
// components
|
||||
import { IssueCommentReaction } from "./comment-reaction";
|
||||
// helpers
|
||||
import { calculateTimeAgo } from "helpers/date-time.helper";
|
||||
// types
|
||||
import type { IIssueActivity, IUser } from "@plane/types";
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
type IIssueCommentCard = {
|
||||
comment: IIssueActivity;
|
||||
handleCommentDeletion: (comment: string) => void;
|
||||
onSubmit: (data: Partial<IIssueActivity>) => void;
|
||||
showAccessSpecifier?: boolean;
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
user: IUser | null;
|
||||
issueCommentReactionCreate: (commentId: string, reaction: string) => void;
|
||||
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
|
||||
};
|
||||
|
||||
export const IssueCommentCard: React.FC<IIssueCommentCard> = (props) => {
|
||||
const {
|
||||
comment,
|
||||
handleCommentDeletion,
|
||||
onSubmit,
|
||||
showAccessSpecifier = false,
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
user,
|
||||
issueCommentReactionCreate,
|
||||
issueCommentReactionRemove,
|
||||
} = props;
|
||||
const workspaceStore = useWorkspace();
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug)?.id as string;
|
||||
|
||||
// states
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
// refs
|
||||
const editorRef = React.useRef<any>(null);
|
||||
const showEditorRef = React.useRef<any>(null);
|
||||
// store hooks
|
||||
const { mentionHighlights, mentionSuggestions } = useMention();
|
||||
// form info
|
||||
const {
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
setFocus,
|
||||
watch,
|
||||
setValue,
|
||||
} = useForm<IIssueActivity>({
|
||||
defaultValues: comment,
|
||||
});
|
||||
|
||||
const formSubmit = (formData: Partial<IIssueActivity>) => {
|
||||
if (isSubmitting) return;
|
||||
|
||||
setIsEditing(false);
|
||||
|
||||
onSubmit({ id: comment.id, ...formData });
|
||||
|
||||
editorRef.current?.setEditorValue(formData.comment_html);
|
||||
showEditorRef.current?.setEditorValue(formData.comment_html);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
isEditing && setFocus("comment");
|
||||
}, [isEditing, setFocus]);
|
||||
|
||||
return (
|
||||
<div className="relative flex items-start space-x-3">
|
||||
<div className="relative px-1">
|
||||
{comment.actor_detail.avatar && comment.actor_detail.avatar !== "" ? (
|
||||
<img
|
||||
src={comment.actor_detail.avatar}
|
||||
alt={
|
||||
comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name
|
||||
}
|
||||
height={30}
|
||||
width={30}
|
||||
className="grid h-7 w-7 place-items-center rounded-full border-2 border-custom-border-200"
|
||||
/>
|
||||
) : (
|
||||
<div className={`grid h-7 w-7 place-items-center rounded-full border-2 border-white bg-gray-500 text-white`}>
|
||||
{comment.actor_detail.is_bot
|
||||
? comment.actor_detail.first_name.charAt(0)
|
||||
: comment.actor_detail.display_name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="absolute -bottom-0.5 -right-1 rounded-tl bg-custom-background-80 px-0.5 py-px">
|
||||
<MessageSquare className="h-3.5 w-3.5 text-custom-text-200" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div>
|
||||
<div className="text-xs">
|
||||
{comment.actor_detail.is_bot ? comment.actor_detail.first_name + " Bot" : comment.actor_detail.display_name}
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-custom-text-200">commented {calculateTimeAgo(comment.created_at)}</p>
|
||||
</div>
|
||||
|
||||
<div className="issue-comments-section p-0">
|
||||
<div className={`flex-col gap-2 ${isEditing ? "flex" : "hidden"}`}>
|
||||
<div>
|
||||
<LiteTextEditorWithRef
|
||||
onEnterKeyPress={handleSubmit(formSubmit)}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
||||
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
||||
ref={editorRef}
|
||||
value={watch("comment_html") ?? ""}
|
||||
debouncedUpdatesEnabled={false}
|
||||
customClassName="min-h-[50px] p-3 shadow-sm"
|
||||
onChange={(comment_json: Object, comment_html: string) => setValue("comment_html", comment_html)}
|
||||
mentionSuggestions={mentionSuggestions}
|
||||
mentionHighlights={mentionHighlights}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-1 self-end">
|
||||
<button
|
||||
type="button"
|
||||
disabled={isSubmitting}
|
||||
className="group rounded border border-green-500 bg-green-500/20 p-2 shadow-md duration-300 hover:bg-green-500"
|
||||
onClick={handleSubmit(formSubmit)}
|
||||
>
|
||||
<Check className="h-3 w-3 text-green-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="group rounded border border-red-500 bg-red-500/20 p-2 shadow-md duration-300 hover:bg-red-500"
|
||||
onClick={() => setIsEditing(false)}
|
||||
>
|
||||
<X className="h-3 w-3 text-red-500 duration-300 group-hover:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`relative ${isEditing ? "hidden" : ""}`}>
|
||||
{showAccessSpecifier && (
|
||||
<div className="absolute right-1.5 top-1 z-[1] text-custom-text-300">
|
||||
{comment.access === "INTERNAL" ? <Lock className="h-3 w-3" /> : <Globe2 className="h-3 w-3" />}
|
||||
</div>
|
||||
)}
|
||||
<LiteReadOnlyEditorWithRef
|
||||
ref={showEditorRef}
|
||||
value={comment.comment_html ?? ""}
|
||||
customClassName="text-xs border border-custom-border-200 bg-custom-background-100"
|
||||
mentionHighlights={mentionHighlights}
|
||||
/>
|
||||
|
||||
<div className="mt-1">
|
||||
<IssueCommentReaction
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
user={user}
|
||||
comment={comment}
|
||||
issueCommentReactionCreate={issueCommentReactionCreate}
|
||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{user?.id === comment.actor && (
|
||||
<CustomMenu ellipsis>
|
||||
<CustomMenu.MenuItem onClick={() => setIsEditing(true)} className="flex items-center gap-1">
|
||||
<Pencil className="h-3 w-3" />
|
||||
Edit comment
|
||||
</CustomMenu.MenuItem>
|
||||
{showAccessSpecifier && (
|
||||
<>
|
||||
{comment.access === "INTERNAL" ? (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => onSubmit({ id: comment.id, access: "EXTERNAL" })}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Globe2 className="h-3 w-3" />
|
||||
Switch to public comment
|
||||
</CustomMenu.MenuItem>
|
||||
) : (
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => onSubmit({ id: comment.id, access: "INTERNAL" })}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Lock className="h-3 w-3" />
|
||||
Switch to private comment
|
||||
</CustomMenu.MenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<CustomMenu.MenuItem
|
||||
onClick={() => {
|
||||
handleCommentDeletion(comment.id);
|
||||
}}
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
Delete comment
|
||||
</CustomMenu.MenuItem>
|
||||
</CustomMenu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,127 +0,0 @@
|
||||
import React from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { Globe2, Lock } from "lucide-react";
|
||||
// hooks
|
||||
import { useMention, useWorkspace } from "hooks/store";
|
||||
// services
|
||||
import { FileService } from "services/file.service";
|
||||
// components
|
||||
import { LiteTextEditorWithRef } from "@plane/lite-text-editor";
|
||||
// ui
|
||||
import { Button } from "@plane/ui";
|
||||
// types
|
||||
import type { IIssueActivity } from "@plane/types";
|
||||
|
||||
const defaultValues: Partial<IIssueActivity> = {
|
||||
access: "INTERNAL",
|
||||
comment_html: "",
|
||||
};
|
||||
|
||||
type IIssueCommentEditor = {
|
||||
disabled?: boolean;
|
||||
onSubmit: (data: IIssueActivity) => Promise<void>;
|
||||
showAccessSpecifier?: boolean;
|
||||
};
|
||||
|
||||
type commentAccessType = {
|
||||
icon: any;
|
||||
key: string;
|
||||
label: "Private" | "Public";
|
||||
};
|
||||
const commentAccess: commentAccessType[] = [
|
||||
{
|
||||
icon: Lock,
|
||||
key: "INTERNAL",
|
||||
label: "Private",
|
||||
},
|
||||
{
|
||||
icon: Globe2,
|
||||
key: "EXTERNAL",
|
||||
label: "Public",
|
||||
},
|
||||
];
|
||||
|
||||
// services
|
||||
const fileService = new FileService();
|
||||
|
||||
export const IssueCommentEditor: React.FC<IIssueCommentEditor> = (props) => {
|
||||
const { disabled = false, onSubmit, showAccessSpecifier = false } = props;
|
||||
// refs
|
||||
const editorRef = React.useRef<any>(null);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = router.query;
|
||||
const workspaceStore = useWorkspace();
|
||||
const workspaceId = workspaceStore.getWorkspaceBySlug(workspaceSlug as string)?.id as string;
|
||||
|
||||
// store hooks
|
||||
const { mentionHighlights, mentionSuggestions } = useMention();
|
||||
// form info
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
reset,
|
||||
} = useForm<IIssueActivity>({ defaultValues });
|
||||
|
||||
const handleAddComment = async (formData: IIssueActivity) => {
|
||||
if (!formData.comment_html || isSubmitting) return;
|
||||
|
||||
await onSubmit(formData).then(() => {
|
||||
reset(defaultValues);
|
||||
editorRef.current?.clearEditor();
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleAddComment)}>
|
||||
<div className="space-y-2 py-2">
|
||||
<div className="h-full">
|
||||
<Controller
|
||||
name="access"
|
||||
control={control}
|
||||
render={({ field: { onChange: onAccessChange, value: accessValue } }) => (
|
||||
<Controller
|
||||
name="comment_html"
|
||||
control={control}
|
||||
render={({ field: { onChange: onCommentChange, value: commentValue } }) => (
|
||||
<LiteTextEditorWithRef
|
||||
onEnterKeyPress={handleSubmit(handleAddComment)}
|
||||
cancelUploadImage={fileService.cancelUpload}
|
||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||
deleteFile={fileService.getDeleteImageFunction(workspaceId)}
|
||||
restoreFile={fileService.getRestoreImageFunction(workspaceId)}
|
||||
ref={editorRef}
|
||||
value={!commentValue || commentValue === "" ? "<p></p>" : commentValue}
|
||||
customClassName="p-2 h-full"
|
||||
editorContentCustomClassNames="min-h-[35px]"
|
||||
debouncedUpdatesEnabled={false}
|
||||
mentionSuggestions={mentionSuggestions}
|
||||
mentionHighlights={mentionHighlights}
|
||||
onChange={(comment_json: Object, comment_html: string) => onCommentChange(comment_html)}
|
||||
commentAccessSpecifier={
|
||||
showAccessSpecifier
|
||||
? { accessValue: accessValue ?? "INTERNAL", onAccessChange, showAccessSpecifier, commentAccess }
|
||||
: undefined
|
||||
}
|
||||
submitButton={
|
||||
<Button
|
||||
variant="primary"
|
||||
type="submit"
|
||||
className="!px-2.5 !py-1.5 !text-xs"
|
||||
disabled={isSubmitting || disabled}
|
||||
>
|
||||
{isSubmitting ? "Adding..." : "Comment"}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -1,57 +0,0 @@
|
||||
import { FC } from "react";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react-lite";
|
||||
// components
|
||||
import { IssuePeekOverviewReactions } from "components/issues";
|
||||
import { useIssueDetail } from "hooks/store";
|
||||
|
||||
interface IIssueCommentReaction {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
user: any;
|
||||
|
||||
comment: any;
|
||||
issueCommentReactionCreate: (commentId: string, reaction: string) => void;
|
||||
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
|
||||
}
|
||||
|
||||
export const IssueCommentReaction: FC<IIssueCommentReaction> = observer((props) => {
|
||||
const { workspaceSlug, projectId, issueId, user, comment, issueCommentReactionCreate, issueCommentReactionRemove } =
|
||||
props;
|
||||
|
||||
const issueDetail = useIssueDetail();
|
||||
|
||||
const handleCommentReactionCreate = (reaction: string) => {
|
||||
if (issueCommentReactionCreate && comment?.id) issueCommentReactionCreate(comment?.id, reaction);
|
||||
};
|
||||
|
||||
const handleCommentReactionRemove = (reaction: string) => {
|
||||
if (issueCommentReactionRemove && comment?.id) issueCommentReactionRemove(comment?.id, reaction);
|
||||
};
|
||||
|
||||
useSWR(
|
||||
workspaceSlug && projectId && issueId && comment && comment?.id
|
||||
? `ISSUE+PEEK_OVERVIEW_COMMENT_${comment?.id}`
|
||||
: null,
|
||||
() => {
|
||||
if (workspaceSlug && projectId && issueId && comment && comment.id) {
|
||||
issueDetail.fetchCommentReactions(workspaceSlug, projectId, comment?.id);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const issueReactions = issueDetail?.commentReaction.getCommentReactionsByCommentId(comment.id) || null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<IssuePeekOverviewReactions
|
||||
issueReactions={issueReactions}
|
||||
user={user}
|
||||
issueReactionCreate={handleCommentReactionCreate}
|
||||
issueReactionRemove={handleCommentReactionRemove}
|
||||
position="top"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
export * from "./card";
|
||||
export * from "./comment-card";
|
||||
export * from "./comment-editor";
|
||||
export * from "./comment-reaction";
|
||||
export * from "./view";
|
||||
@@ -1,61 +0,0 @@
|
||||
import { FC } from "react";
|
||||
// components
|
||||
import { IssueActivityCard, IssueCommentEditor } from "components/issues";
|
||||
// types
|
||||
import { IUser } from "@plane/types";
|
||||
|
||||
type Props = {
|
||||
workspaceSlug: string;
|
||||
projectId: string;
|
||||
issueId: string;
|
||||
user: IUser | null;
|
||||
issueActivity: string[] | undefined;
|
||||
issueCommentCreate: (comment: any) => void;
|
||||
issueCommentUpdate: (comment: any) => void;
|
||||
issueCommentRemove: (commentId: string) => void;
|
||||
issueCommentReactionCreate: (commentId: string, reaction: string) => void;
|
||||
issueCommentReactionRemove: (commentId: string, reaction: string) => void;
|
||||
showCommentAccessSpecifier: boolean;
|
||||
};
|
||||
|
||||
export const IssueActivity: FC<Props> = (props) => {
|
||||
const {
|
||||
workspaceSlug,
|
||||
projectId,
|
||||
issueId,
|
||||
user,
|
||||
issueActivity,
|
||||
issueCommentCreate,
|
||||
issueCommentUpdate,
|
||||
issueCommentRemove,
|
||||
issueCommentReactionCreate,
|
||||
issueCommentReactionRemove,
|
||||
showCommentAccessSpecifier,
|
||||
} = props;
|
||||
|
||||
const handleAddComment = async (formData: any) => {
|
||||
if (!formData.comment_html) return;
|
||||
await issueCommentCreate(formData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 border-t border-custom-border-200 py-6">
|
||||
<div className="text-lg font-medium">Activity</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<IssueActivityCard
|
||||
workspaceSlug={workspaceSlug}
|
||||
projectId={projectId}
|
||||
issueId={issueId}
|
||||
user={user}
|
||||
issueActivity={issueActivity}
|
||||
issueCommentUpdate={issueCommentUpdate}
|
||||
issueCommentRemove={issueCommentRemove}
|
||||
issueCommentReactionCreate={issueCommentReactionCreate}
|
||||
issueCommentReactionRemove={issueCommentReactionRemove}
|
||||
/>
|
||||
<IssueCommentEditor onSubmit={handleAddComment} showAccessSpecifier={showCommentAccessSpecifier} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,3 @@
|
||||
export * from "./activity";
|
||||
export * from "./reactions";
|
||||
export * from "./issue-detail";
|
||||
export * from "./properties";
|
||||
export * from "./root";
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from "./preview";
|
||||
export * from "./root";
|
||||
export * from "./selector";
|
||||
@@ -1,48 +0,0 @@
|
||||
import { FC } from "react";
|
||||
// helpers
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
|
||||
interface IIssueReactionPreview {
|
||||
issueReactions: any;
|
||||
user: any;
|
||||
handleReaction: (reaction: string) => void;
|
||||
}
|
||||
|
||||
export const IssueReactionPreview: FC<IIssueReactionPreview> = (props) => {
|
||||
const { issueReactions, user, handleReaction } = props;
|
||||
|
||||
const isUserReacted = (reactions: any) => {
|
||||
const userReaction = reactions?.find((reaction: any) => reaction.actor === user?.id);
|
||||
if (userReaction) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{Object.keys(issueReactions || {}).map(
|
||||
(reaction) =>
|
||||
issueReactions[reaction]?.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleReaction(reaction)}
|
||||
key={reaction}
|
||||
className={`flex h-full items-center gap-1.5 rounded px-2 py-1 text-sm text-custom-text-100 ${
|
||||
isUserReacted(issueReactions[reaction])
|
||||
? `bg-custom-primary-100/10 hover:bg-custom-primary-100/30`
|
||||
: `bg-custom-background-90 hover:bg-custom-background-100/30`
|
||||
}`}
|
||||
>
|
||||
<span className="text-sm">{renderEmoji(reaction)}</span>
|
||||
<span
|
||||
className={`${
|
||||
isUserReacted(issueReactions[reaction]) ? `text-custom-primary-100 hover:text-custom-primary-200` : ``
|
||||
}`}
|
||||
>
|
||||
{issueReactions[reaction].length}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,32 +0,0 @@
|
||||
import { FC } from "react";
|
||||
// components
|
||||
import { IssueReactionPreview, IssueReactionSelector } from "components/issues";
|
||||
// types
|
||||
import { IUser } from "@plane/types";
|
||||
|
||||
interface IIssueReaction {
|
||||
issueReactions: any;
|
||||
user: IUser | null;
|
||||
issueReactionCreate: (reaction: string) => void;
|
||||
issueReactionRemove: (reaction: string) => void;
|
||||
position?: "top" | "bottom";
|
||||
}
|
||||
|
||||
export const IssuePeekOverviewReactions: FC<IIssueReaction> = (props) => {
|
||||
const { issueReactions, user, issueReactionCreate, issueReactionRemove, position = "bottom" } = props;
|
||||
|
||||
const handleReaction = (reaction: string) => {
|
||||
const isReactionAvailable =
|
||||
issueReactions?.[reaction].find((_reaction: any) => _reaction.actor === user?.id) ?? false;
|
||||
|
||||
if (isReactionAvailable) issueReactionRemove(reaction);
|
||||
else issueReactionCreate(reaction);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-wrap items-center gap-2">
|
||||
<IssueReactionSelector onSelect={handleReaction} position={position} />
|
||||
<IssueReactionPreview issueReactions={issueReactions} user={user} handleReaction={handleReaction} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import { FC, Fragment } from "react";
|
||||
import { Popover, Transition } from "@headlessui/react";
|
||||
// helper
|
||||
import { renderEmoji } from "helpers/emoji.helper";
|
||||
// icons
|
||||
import { SmilePlus } from "lucide-react";
|
||||
// constants
|
||||
import { issueReactionEmojis } from "constants/issue";
|
||||
|
||||
interface IIssueReactionSelector {
|
||||
size?: "sm" | "md" | "lg";
|
||||
position?: "top" | "bottom";
|
||||
onSelect: (reaction: any) => void;
|
||||
}
|
||||
|
||||
export const IssueReactionSelector: FC<IIssueReactionSelector> = (props) => {
|
||||
const { size = "md", position = "top", onSelect } = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover className="relative">
|
||||
{({ open, close: closePopover }) => (
|
||||
<>
|
||||
<Popover.Button
|
||||
className={`${
|
||||
open ? "" : "bg-custom-background-80"
|
||||
} group inline-flex items-center rounded-md bg-custom-background-80 transition-all hover:bg-custom-background-90 focus:outline-none`}
|
||||
>
|
||||
<span className={`flex items-center justify-center rounded px-2 py-1.5`}>
|
||||
<SmilePlus className={`${size === "sm" ? "h-3 w-3" : size === "md" ? "h-3.5 w-3.5" : "h-4 w-4"}`} />
|
||||
</span>
|
||||
</Popover.Button>
|
||||
<Transition
|
||||
as={Fragment}
|
||||
enter="transition ease-out duration-200"
|
||||
enterFrom="opacity-0 translate-y-1"
|
||||
enterTo="opacity-100 translate-y-0"
|
||||
leave="transition ease-in duration-150"
|
||||
leaveFrom="opacity-100 translate-y-0"
|
||||
leaveTo="opacity-0 translate-y-1"
|
||||
>
|
||||
<Popover.Panel
|
||||
className={`absolute -left-2 z-10 overflow-hidden rounded border border-custom-border-200 bg-custom-sidebar-background-100 p-1 shadow-custom-shadow-sm ${
|
||||
position === "top" ? "-top-10" : "-bottom-10"
|
||||
}`}
|
||||
>
|
||||
<div className="flex gap-x-1">
|
||||
{issueReactionEmojis.map((emoji) => (
|
||||
<button
|
||||
key={emoji}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (onSelect) onSelect(emoji);
|
||||
closePopover();
|
||||
}}
|
||||
className="flex h-6 w-6 select-none items-center justify-center rounded p-1 text-sm transition-all hover:bg-custom-sidebar-background-80"
|
||||
>
|
||||
<div className="h-4 w-4">{renderEmoji(emoji)}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Popover.Panel>
|
||||
</Transition>
|
||||
</>
|
||||
)}
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -113,12 +113,23 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
},
|
||||
addIssueToCycle: async (workspaceSlug: string, projectId: string, cycleId: string, issueIds: string[]) => {
|
||||
try {
|
||||
await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds);
|
||||
setToastAlert({
|
||||
title: "Cycle added to issue successfully",
|
||||
type: "success",
|
||||
message: "Issue added to issue successfully",
|
||||
});
|
||||
await addIssueToCycle(workspaceSlug, projectId, cycleId, issueIds)
|
||||
.then((res) => {
|
||||
updateIssue(workspaceSlug, projectId, res.id, res);
|
||||
fetchIssue(workspaceSlug, projectId, res.id);
|
||||
setToastAlert({
|
||||
title: "Cycle added to issue successfully",
|
||||
type: "success",
|
||||
message: "Issue added to issue successfully",
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Selected issues could not be added to the cycle. Please try again.",
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Cycle add to issue failed",
|
||||
@@ -145,12 +156,23 @@ export const IssuePeekOverview: FC<IIssuePeekOverview> = observer((props) => {
|
||||
},
|
||||
addIssueToModule: async (workspaceSlug: string, projectId: string, moduleId: string, issueIds: string[]) => {
|
||||
try {
|
||||
await addIssueToModule(workspaceSlug, projectId, moduleId, issueIds);
|
||||
setToastAlert({
|
||||
title: "Module added to issue successfully",
|
||||
type: "success",
|
||||
message: "Module added to issue successfully",
|
||||
});
|
||||
await addIssueToModule(workspaceSlug, projectId, moduleId, issueIds)
|
||||
.then((res) => {
|
||||
updateIssue(workspaceSlug, projectId, res.id, res);
|
||||
fetchIssue(workspaceSlug, projectId, res.id);
|
||||
setToastAlert({
|
||||
title: "Module added to issue successfully",
|
||||
type: "success",
|
||||
message: "Module added to issue successfully",
|
||||
});
|
||||
})
|
||||
.catch(() =>
|
||||
setToastAlert({
|
||||
type: "error",
|
||||
title: "Error!",
|
||||
message: "Selected issues could not be added to the module. Please try again.",
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
setToastAlert({
|
||||
title: "Module add to issue failed",
|
||||
|
||||
@@ -55,17 +55,19 @@ export const WorkspaceDashboardView = observer(() => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<IssuePeekOverview />
|
||||
{currentUser && !currentUser.is_tour_completed && (
|
||||
<div className="fixed left-0 top-0 z-20 grid h-full w-full place-items-center bg-custom-backdrop bg-opacity-50 transition-opacity">
|
||||
<TourRoot onComplete={handleTourCompleted} />
|
||||
</div>
|
||||
)}
|
||||
{homeDashboardId && joinedProjectIds ? (
|
||||
<div className="space-y-7 p-7 bg-custom-background-90 h-full w-full flex flex-col overflow-y-auto">
|
||||
{currentUser && <UserGreetingsView user={currentUser} />}
|
||||
<>
|
||||
{joinedProjectIds.length > 0 ? (
|
||||
<DashboardWidgets />
|
||||
<div className="space-y-7 p-7 bg-custom-background-90 h-full w-full flex flex-col overflow-y-auto">
|
||||
<IssuePeekOverview />
|
||||
{currentUser && <UserGreetingsView user={currentUser} />}
|
||||
{currentUser && !currentUser.is_tour_completed && (
|
||||
<div className="fixed left-0 top-0 z-20 grid h-full w-full place-items-center bg-custom-backdrop bg-opacity-50 transition-opacity">
|
||||
<TourRoot onComplete={handleTourCompleted} />
|
||||
</div>
|
||||
)}
|
||||
<DashboardWidgets />
|
||||
</div>
|
||||
) : (
|
||||
<EmptyState
|
||||
image={emptyStateImage}
|
||||
@@ -85,7 +87,7 @@ export const WorkspaceDashboardView = observer(() => {
|
||||
disabled={!isEditingAllowed}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full w-full grid place-items-center">
|
||||
<Spinner />
|
||||
|
||||
@@ -7,7 +7,7 @@ import { useApplication } from "hooks/store";
|
||||
import useOutsideClickDetector from "hooks/use-outside-click-detector";
|
||||
// icons
|
||||
import { FileText, HelpCircle, MessagesSquare, MoveLeft, Zap } from "lucide-react";
|
||||
import { DiscordIcon, GithubIcon, Button } from "@plane/ui";
|
||||
import { DiscordIcon, GithubIcon, Tooltip, Button } from "@plane/ui";
|
||||
// assets
|
||||
import packageJson from "package.json";
|
||||
// components
|
||||
@@ -82,24 +82,29 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
|
||||
</Button>
|
||||
)}
|
||||
<div className={`flex items-center gap-1 ${isCollapsed ? "flex-col justify-center" : "w-1/2 justify-evenly"}`}>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||
isCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => toggleShortcutModal(true)}
|
||||
>
|
||||
<Zap className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||
isCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
||||
>
|
||||
<HelpCircle className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<Tooltip tooltipContent="Shortcuts">
|
||||
<button
|
||||
type="button"
|
||||
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||
isCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => toggleShortcutModal(true)}
|
||||
>
|
||||
<Zap className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip tooltipContent="Help">
|
||||
<button
|
||||
type="button"
|
||||
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
|
||||
isCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
||||
>
|
||||
<HelpCircle className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:hidden"
|
||||
@@ -107,15 +112,18 @@ export const WorkspaceHelpSection: React.FC<WorkspaceHelpSectionProps> = observe
|
||||
>
|
||||
<MoveLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${
|
||||
isCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => toggleSidebar()}
|
||||
>
|
||||
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isCollapsed ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
|
||||
<Tooltip tooltipContent={`${isCollapsed ? "Expand" : "Hide"}`}>
|
||||
<button
|
||||
type="button"
|
||||
className={`hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${
|
||||
isCollapsed ? "w-full" : ""
|
||||
}`}
|
||||
onClick={() => toggleSidebar()}
|
||||
>
|
||||
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isCollapsed ? "rotate-180" : ""}`} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import useSWR from "swr";
|
||||
import { useEstimate, useLabel, useProjectState } from "./store";
|
||||
|
||||
export const useWorskspaceIssueProperties = (workspaceSlug: string | string[] | undefined) => {
|
||||
export const useWorkspaceIssueProperties = (workspaceSlug: string | string[] | undefined) => {
|
||||
const { fetchWorkspaceLabels } = useLabel();
|
||||
|
||||
const { fetchWorkspaceStates } = useProjectState();
|
||||
|
||||
const { fetchWorskpaceEstimates } = useEstimate();
|
||||
const { fetchWorkspaceEstimates } = useEstimate();
|
||||
|
||||
// fetch workspace labels
|
||||
useSWR(
|
||||
@@ -23,6 +23,6 @@ export const useWorskspaceIssueProperties = (workspaceSlug: string | string[] |
|
||||
// fetch workspace estimates
|
||||
useSWR(
|
||||
workspaceSlug ? `WORKSPACE_ESTIMATES_${workspaceSlug}` : null,
|
||||
workspaceSlug ? () => fetchWorskpaceEstimates(workspaceSlug.toString()) : null
|
||||
workspaceSlug ? () => fetchWorkspaceEstimates(workspaceSlug.toString()) : null
|
||||
);
|
||||
};
|
||||
@@ -2,17 +2,14 @@ import { ReactElement } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import useSWR from "swr";
|
||||
import { observer } from "mobx-react";
|
||||
import { Inbox } from "lucide-react";
|
||||
// hooks
|
||||
import { useProject, useInboxIssues } from "hooks/store";
|
||||
// layouts
|
||||
import { AppLayout } from "layouts/app-layout";
|
||||
// ui
|
||||
import { Spinner } from "@plane/ui";
|
||||
// components
|
||||
import { ProjectInboxHeader } from "components/headers";
|
||||
import { InboxSidebarRoot, InboxIssueActionsHeader } from "components/inbox";
|
||||
import { InboxIssueDetailRoot } from "components/issues/issue-detail/inbox";
|
||||
import { InboxSidebarRoot, InboxContentRoot } from "components/inbox";
|
||||
|
||||
// types
|
||||
import { NextPageWithLayout } from "lib/types";
|
||||
|
||||
@@ -20,13 +17,10 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
||||
const router = useRouter();
|
||||
const { workspaceSlug, projectId, inboxId, inboxIssueId } = router.query;
|
||||
// store hooks
|
||||
const {
|
||||
issues: { getInboxIssuesByInboxId },
|
||||
} = useInboxIssues();
|
||||
const { currentProjectDetails } = useProject();
|
||||
const {
|
||||
filters: { fetchInboxFilters },
|
||||
issues: { loader, fetchInboxIssues },
|
||||
issues: { fetchInboxIssues },
|
||||
} = useInboxIssues();
|
||||
|
||||
useSWR(
|
||||
@@ -41,67 +35,25 @@ const ProjectInboxPage: NextPageWithLayout = observer(() => {
|
||||
}
|
||||
);
|
||||
|
||||
// inbox issues list
|
||||
const inboxIssuesList = inboxId ? getInboxIssuesByInboxId(inboxId?.toString()) : undefined;
|
||||
|
||||
if (!workspaceSlug || !projectId || !inboxId) return <></>;
|
||||
if (!workspaceSlug || !projectId || !inboxId || !currentProjectDetails?.inbox_view) return <></>;
|
||||
return (
|
||||
<>
|
||||
{loader === "fetch" ? (
|
||||
<div className="relative flex w-full h-full items-center justify-center">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<div className="relative flex h-full overflow-hidden">
|
||||
<div className="flex-shrink-0 w-[340px] h-full border-r border-custom-border-100">
|
||||
{workspaceSlug && projectId && inboxId && (
|
||||
<InboxSidebarRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="w-full">
|
||||
{workspaceSlug && projectId && inboxId && inboxIssueId ? (
|
||||
<div className="w-full h-full overflow-hidden relative flex flex-col">
|
||||
<div className="flex-shrink-0 min-h-[50px] border-b border-custom-border-100">
|
||||
<InboxIssueActionsHeader
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
inboxIssueId={inboxIssueId?.toString() || undefined}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full h-full">
|
||||
<InboxIssueDetailRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
issueId={inboxIssueId.toString()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid h-full place-items-center p-4 text-custom-text-200">
|
||||
<div className="grid h-full place-items-center">
|
||||
<div className="my-5 flex flex-col items-center gap-4">
|
||||
<Inbox size={60} strokeWidth={1.5} />
|
||||
{inboxIssuesList && inboxIssuesList.length > 0 ? (
|
||||
<span className="text-custom-text-200">
|
||||
{inboxIssuesList?.length} issues found. Select an issue from the sidebar to view its details.
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-custom-text-200">No issues found</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
<div className="relative flex h-full overflow-hidden">
|
||||
<div className="flex-shrink-0 w-[340px] h-full border-r border-custom-border-300">
|
||||
<InboxSidebarRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<InboxContentRoot
|
||||
workspaceSlug={workspaceSlug.toString()}
|
||||
projectId={projectId.toString()}
|
||||
inboxId={inboxId.toString()}
|
||||
inboxIssueId={inboxIssueId?.toString() || undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<svg width="94" height="94" viewBox="0 0 94 94" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="46.7188" cy="46.7188" r="46.7188" fill="#F9F9FB"/>
|
||||
<path d="M74.5 34.2487V29.9987C74.5 28.4958 73.903 27.0545 72.8403 25.9918C71.7776 24.9291 70.3362 24.332 68.8333 24.332H29.1667C27.6638 24.332 26.2224 24.9291 25.1597 25.9918C24.097 27.0545 23.5 28.4958 23.5 29.9987V69.6654C23.5 71.1683 24.097 72.6096 25.1597 73.6723C26.2224 74.735 27.6638 75.332 29.1667 75.332H39.0833" stroke="#D9D9E0" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M60.332 18.668V30.0013" stroke="#CDCED6" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M37.668 18.668V30.0013" stroke="#CDCED6" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M23.5 41.332H37.6667" stroke="#D9D9E0" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M64.582 62.5846L60.332 59.1846V52.668" stroke="#B9BBC6" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M60.332 75.332C69.7209 75.332 77.332 67.7209 77.332 58.332C77.332 48.9432 69.7209 41.332 60.332 41.332C50.9432 41.332 43.332 48.9432 43.332 58.332C43.332 67.7209 50.9432 75.332 60.332 75.332Z" stroke="#B9BBC6" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M69.625 35.4362V31.7487C69.625 30.4447 69.107 29.1941 68.1849 28.2721C67.2629 27.35 66.0123 26.832 64.7083 26.832H30.2917C28.9877 26.832 27.7371 27.35 26.8151 28.2721C25.893 29.1941 25.375 30.4447 25.375 31.7487V66.1654C25.375 67.4693 25.893 68.7199 26.8151 69.642C27.7371 70.564 28.9877 71.082 30.2917 71.082H38.8958" stroke="#D9D9E0" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M57.332 21.918V31.7513" stroke="#CDCED6" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M37.668 21.918V31.7513" stroke="#CDCED6" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M25.375 41.582H37.6667" stroke="#D9D9E0" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M61.0195 60.0221L57.332 57.0721V51.418" stroke="#B9BBC6" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M57.332 71.082C65.4782 71.082 72.082 64.4782 72.082 56.332C72.082 48.1858 65.4782 41.582 57.332 41.582C49.1858 41.582 42.582 48.1858 42.582 56.332C42.582 64.4782 49.1858 71.082 57.332 71.082Z" stroke="#B9BBC6" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -23,7 +23,7 @@ export interface IEstimateStore {
|
||||
getProjectActiveEstimateDetails: (projectId: string) => IEstimate | null;
|
||||
// fetch actions
|
||||
fetchProjectEstimates: (workspaceSlug: string, projectId: string) => Promise<IEstimate[]>;
|
||||
fetchWorskpaceEstimates: (workspaceSlug: string) => Promise<IEstimate[]>;
|
||||
fetchWorkspaceEstimates: (workspaceSlug: string) => Promise<IEstimate[]>;
|
||||
// crud actions
|
||||
createEstimate: (workspaceSlug: string, projectId: string, data: IEstimateFormData) => Promise<IEstimate>;
|
||||
updateEstimate: (
|
||||
@@ -56,7 +56,7 @@ export class EstimateStore implements IEstimateStore {
|
||||
activeEstimateDetails: computed,
|
||||
// actions
|
||||
fetchProjectEstimates: action,
|
||||
fetchWorskpaceEstimates: action,
|
||||
fetchWorkspaceEstimates: action,
|
||||
createEstimate: action,
|
||||
updateEstimate: action,
|
||||
deleteEstimate: action,
|
||||
@@ -158,7 +158,7 @@ export class EstimateStore implements IEstimateStore {
|
||||
* @param workspaceSlug
|
||||
* @param projectId
|
||||
*/
|
||||
fetchWorskpaceEstimates = async (workspaceSlug: string) =>
|
||||
fetchWorkspaceEstimates = async (workspaceSlug: string) =>
|
||||
await this.estimateService.getWorkspaceEstimatesList(workspaceSlug).then((response) => {
|
||||
runInAction(() => {
|
||||
response.forEach((estimate) => {
|
||||
|
||||
@@ -115,11 +115,12 @@ export class InboxFilter implements IInboxFilter {
|
||||
};
|
||||
_filters = { ..._filters, ...filters };
|
||||
|
||||
this.rootStore.inbox.inboxIssue.fetchInboxIssues(workspaceSlug, projectId, inboxId, "mutation");
|
||||
|
||||
const response = await this.rootStore.inbox.inbox.updateInbox(workspaceSlug, projectId, inboxId, {
|
||||
view_props: { filters: _filters },
|
||||
});
|
||||
|
||||
this.rootStore.inbox.inboxIssue.fetchInboxIssues(workspaceSlug, projectId, inboxId);
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
|
||||
@@ -17,21 +17,24 @@ import type {
|
||||
TInboxDetailedStatus,
|
||||
TIssue,
|
||||
} from "@plane/types";
|
||||
// constants
|
||||
import { INBOX_ISSUE_SOURCE } from "constants/inbox";
|
||||
|
||||
type TInboxIssueLoader = "fetch" | undefined;
|
||||
type TLoader = "init-loader" | "mutation" | undefined;
|
||||
|
||||
export interface IInboxIssue {
|
||||
// observables
|
||||
loader: TInboxIssueLoader;
|
||||
loader: TLoader;
|
||||
inboxIssues: TInboxIssueDetailIdMap;
|
||||
inboxIssueMap: TInboxIssueDetailMap;
|
||||
// helper methods
|
||||
getInboxIssuesByInboxId: (inboxId: string) => string[] | undefined;
|
||||
getInboxIssueByIssueId: (inboxId: string, issueId: string) => TInboxIssueDetail | undefined;
|
||||
// actions
|
||||
fetchInboxIssues: (workspaceSlug: string, projectId: string, inboxId: string) => Promise<TInboxIssueExtendedDetail[]>;
|
||||
fetchInboxIssues: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
loaderType?: TLoader
|
||||
) => Promise<TInboxIssueExtendedDetail[]>;
|
||||
fetchInboxIssueById: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
@@ -63,7 +66,7 @@ export interface IInboxIssue {
|
||||
|
||||
export class InboxIssue implements IInboxIssue {
|
||||
// observables
|
||||
loader: TInboxIssueLoader = "fetch";
|
||||
loader: TLoader = "init-loader";
|
||||
inboxIssues: TInboxIssueDetailIdMap = {};
|
||||
inboxIssueMap: TInboxIssueDetailMap = {};
|
||||
// root store
|
||||
@@ -104,9 +107,14 @@ export class InboxIssue implements IInboxIssue {
|
||||
});
|
||||
|
||||
// actions
|
||||
fetchInboxIssues = async (workspaceSlug: string, projectId: string, inboxId: string) => {
|
||||
fetchInboxIssues = async (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
inboxId: string,
|
||||
loaderType: TLoader = "init-loader"
|
||||
) => {
|
||||
try {
|
||||
this.loader = "fetch";
|
||||
this.loader = loaderType;
|
||||
const queryParams = this.rootStore.inbox.inboxFilter.inboxAppliedFilters ?? {};
|
||||
|
||||
const response = await this.inboxIssueService.fetchInboxIssues(workspaceSlug, projectId, inboxId, queryParams);
|
||||
|
||||
@@ -90,6 +90,7 @@ export class IssueStore implements IIssueStore {
|
||||
this.rootIssueDetailStore.relation.fetchRelations(workspaceSlug, projectId, issueId);
|
||||
|
||||
// fetching states
|
||||
// TODO: check if this function is required
|
||||
this.rootIssueDetailStore.rootIssueStore.state.fetchProjectStates(workspaceSlug, projectId);
|
||||
|
||||
return issue;
|
||||
|
||||
@@ -43,7 +43,7 @@ export class IssueStore implements IIssueStore {
|
||||
if (issues && issues.length <= 0) return;
|
||||
runInAction(() => {
|
||||
issues.forEach((issue) => {
|
||||
set(this.issuesMap, issue.id, issue);
|
||||
if (!this.issuesMap[issue.id]) set(this.issuesMap, issue.id, issue);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -162,14 +162,17 @@ export class WorkspaceIssuesFilter extends IssueFilterHelperStore implements IWo
|
||||
) => {
|
||||
try {
|
||||
if (!viewId) throw new Error("View id is required");
|
||||
const issueFilters = this.getIssueFilters(viewId);
|
||||
|
||||
if (isEmpty(this.filters) || isEmpty(this.filters[viewId]) || isEmpty(filters)) return;
|
||||
console.log("issueFilters", issueFilters);
|
||||
|
||||
if (!issueFilters || isEmpty(filters)) return;
|
||||
|
||||
const _filters = {
|
||||
filters: this.filters[viewId].filters as IIssueFilterOptions,
|
||||
displayFilters: this.filters[viewId].displayFilters as IIssueDisplayFilterOptions,
|
||||
displayProperties: this.filters[viewId].displayProperties as IIssueDisplayProperties,
|
||||
kanbanFilters: this.filters[viewId].kanbanFilters as TIssueKanbanFilters,
|
||||
filters: issueFilters.filters as IIssueFilterOptions,
|
||||
displayFilters: issueFilters.displayFilters as IIssueDisplayFilterOptions,
|
||||
displayProperties: issueFilters.displayProperties as IIssueDisplayProperties,
|
||||
kanbanFilters: issueFilters.kanbanFilters as TIssueKanbanFilters,
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
|
||||
Reference in New Issue
Block a user