chore: optimised issue activity and updated the popover component in issue detail and peek overview (#5208)
This commit is contained in:
parent
31fe9a1a02
commit
f5027f4268
8 changed files with 94 additions and 81 deletions
28
web/ce/components/issues/worklog/activity/filter-root.tsx
Normal file
28
web/ce/components/issues/worklog/activity/filter-root.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { FC } from "react";
|
||||||
|
// components
|
||||||
|
import { ActivityFilter } from "@/components/issues";
|
||||||
|
// plane web constants
|
||||||
|
import { TActivityFilters, ACTIVITY_FILTER_TYPE_OPTIONS, TActivityFilterOption } from "@/plane-web/constants/issues";
|
||||||
|
|
||||||
|
export type TActivityFilterRoot = {
|
||||||
|
selectedFilters: TActivityFilters[];
|
||||||
|
toggleFilter: (filter: TActivityFilters) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActivityFilterRoot: FC<TActivityFilterRoot> = (props) => {
|
||||||
|
const { selectedFilters, toggleFilter } = props;
|
||||||
|
|
||||||
|
const filters: TActivityFilterOption[] = Object.entries(ACTIVITY_FILTER_TYPE_OPTIONS).map(([key, value]) => {
|
||||||
|
const filterKey = key as TActivityFilters;
|
||||||
|
return {
|
||||||
|
key: filterKey,
|
||||||
|
label: value.label,
|
||||||
|
isSelected: selectedFilters.includes(filterKey),
|
||||||
|
onClick: () => toggleFilter(filterKey),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return <ActivityFilter selectedFilters={selectedFilters} filterOptions={filters} />;
|
||||||
|
};
|
||||||
|
|
@ -1,2 +1,4 @@
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
export * from "./worklog-create-button";
|
export * from "./worklog-create-button";
|
||||||
|
|
||||||
|
export * from "./filter-root";
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,13 @@ export const ACTIVITY_FILTER_TYPE_OPTIONS: Record<EActivityFilterType, { label:
|
||||||
|
|
||||||
export const defaultActivityFilters: TActivityFilters[] = [EActivityFilterType.ACTIVITY, EActivityFilterType.COMMENT];
|
export const defaultActivityFilters: TActivityFilters[] = [EActivityFilterType.ACTIVITY, EActivityFilterType.COMMENT];
|
||||||
|
|
||||||
|
export type TActivityFilterOption = {
|
||||||
|
key: EActivityFilterType;
|
||||||
|
label: string;
|
||||||
|
isSelected: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
export const filterActivityOnSelectedFilters = (
|
export const filterActivityOnSelectedFilters = (
|
||||||
activity: TIssueActivityComment[],
|
activity: TIssueActivityComment[],
|
||||||
filter: TActivityFilters[]
|
filter: TActivityFilters[]
|
||||||
|
|
|
||||||
|
|
@ -1,85 +1,59 @@
|
||||||
import React, { FC, Fragment } from "react";
|
import React, { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Check, ListFilter } from "lucide-react";
|
import { Check, ListFilter } from "lucide-react";
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Button, PopoverMenu } from "@plane/ui";
|
||||||
// ui
|
|
||||||
import { Button } from "@plane/ui";
|
|
||||||
// helper
|
// helper
|
||||||
import { cn } from "@/helpers/common.helper";
|
import { cn } from "@/helpers/common.helper";
|
||||||
// constants
|
// constants
|
||||||
import { TActivityFilters, ACTIVITY_FILTER_TYPE_OPTIONS } from "@/plane-web/constants/issues";
|
import { TActivityFilterOption, TActivityFilters } from "@/plane-web/constants/issues";
|
||||||
|
|
||||||
type Props = {
|
type TActivityFilter = {
|
||||||
selectedFilters: TActivityFilters[];
|
selectedFilters: TActivityFilters[];
|
||||||
toggleFilter: (filter: TActivityFilters) => void;
|
filterOptions: TActivityFilterOption[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ActivityFilter: FC<Props> = observer((props) => {
|
export const ActivityFilter: FC<TActivityFilter> = observer((props) => {
|
||||||
const { selectedFilters, toggleFilter } = props;
|
const { selectedFilters = [], filterOptions } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover as="div" className="relative">
|
<PopoverMenu
|
||||||
{({ open }) => (
|
buttonClassName="outline-none"
|
||||||
<>
|
button={
|
||||||
<Popover.Button as={React.Fragment}>
|
|
||||||
<Button
|
<Button
|
||||||
variant="neutral-primary"
|
variant="neutral-primary"
|
||||||
size="sm"
|
size="sm"
|
||||||
prependIcon={<ListFilter className="h-3 w-3" />}
|
prependIcon={<ListFilter className="h-3 w-3" />}
|
||||||
className="relative"
|
className="relative"
|
||||||
>
|
>
|
||||||
<span className={`${open ? "text-custom-text-100" : "text-custom-text-200"}`}>Filters</span>
|
<span className="text-custom-text-200">Filters</span>
|
||||||
</Button>
|
</Button>
|
||||||
</Popover.Button>
|
}
|
||||||
|
panelClassName="p-2 rounded-md border border-custom-border-200 bg-custom-background-100"
|
||||||
<Transition
|
data={filterOptions}
|
||||||
as={Fragment}
|
keyExtractor={(item) => item.key}
|
||||||
enter="transition ease-out duration-200"
|
render={(item) => (
|
||||||
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 mt-2 right-0 z-10 min-w-40">
|
|
||||||
<div className="p-2 rounded-md border border-custom-border-200 bg-custom-background-100">
|
|
||||||
{Object.keys(ACTIVITY_FILTER_TYPE_OPTIONS).map((key) => {
|
|
||||||
const filterKey = key as TActivityFilters;
|
|
||||||
const filter = ACTIVITY_FILTER_TYPE_OPTIONS[filterKey];
|
|
||||||
const isSelected = selectedFilters.includes(filterKey);
|
|
||||||
return (
|
|
||||||
<div
|
<div
|
||||||
key={filterKey}
|
key={item.key}
|
||||||
className="flex items-center gap-2 text-sm cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
|
className="flex items-center gap-2 text-sm cursor-pointer px-2 p-1 transition-all hover:bg-custom-background-80 rounded-sm"
|
||||||
onClick={() => toggleFilter(filterKey)}
|
onClick={item.onClick}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-shrink-0 w-3 h-3 flex justify-center items-center rounded-sm transition-all bg-custom-background-90",
|
"flex-shrink-0 w-3 h-3 flex justify-center items-center rounded-sm transition-all bg-custom-background-90",
|
||||||
{
|
{
|
||||||
"bg-custom-primary text-white": isSelected,
|
"bg-custom-primary text-white": item.isSelected,
|
||||||
"bg-custom-background-80 text-custom-text-400": isSelected && selectedFilters.length === 1,
|
"bg-custom-background-80 text-custom-text-400": item.isSelected && selectedFilters.length === 1,
|
||||||
"bg-custom-background-90": !isSelected,
|
"bg-custom-background-90": !item.isSelected,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isSelected && <Check className="h-2.5 w-2.5" />}
|
{item.isSelected && <Check className="h-2.5 w-2.5" />}
|
||||||
|
</div>
|
||||||
|
<div className={cn("whitespace-nowrap", item.isSelected ? "text-custom-text-100" : "text-custom-text-200")}>
|
||||||
|
{item.label}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"whitespace-nowrap",
|
|
||||||
isSelected ? "text-custom-text-100" : "text-custom-text-200"
|
|
||||||
)}
|
)}
|
||||||
>
|
/>
|
||||||
{filter.label}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Popover.Panel>
|
|
||||||
</Transition>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Popover>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,12 @@ import { TIssueComment } from "@plane/types";
|
||||||
// ui
|
// ui
|
||||||
import { TOAST_TYPE, setToast } from "@plane/ui";
|
import { TOAST_TYPE, setToast } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { ActivityFilter, IssueCommentCreate } from "@/components/issues";
|
import { IssueCommentCreate } from "@/components/issues";
|
||||||
import { IssueActivityCommentRoot } from "@/components/issues/issue-detail";
|
import { IssueActivityCommentRoot } from "@/components/issues/issue-detail";
|
||||||
// hooks
|
// hooks
|
||||||
import { useIssueDetail, useProject } from "@/hooks/store";
|
import { useIssueDetail, useProject } from "@/hooks/store";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { IssueActivityWorklogCreateButton } from "@/plane-web/components/issues/worklog";
|
import { ActivityFilterRoot, IssueActivityWorklogCreateButton } from "@/plane-web/components/issues/worklog";
|
||||||
// plane web constants
|
// plane web constants
|
||||||
import { TActivityFilters, defaultActivityFilters } from "@/plane-web/constants/issues";
|
import { TActivityFilters, defaultActivityFilters } from "@/plane-web/constants/issues";
|
||||||
|
|
||||||
|
|
@ -120,7 +120,7 @@ export const IssueActivity: FC<TIssueActivity> = observer((props) => {
|
||||||
issueId={issueId}
|
issueId={issueId}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
<ActivityFilter selectedFilters={selectedFilters} toggleFilter={toggleFilter} />
|
<ActivityFilterRoot selectedFilters={selectedFilters} toggleFilter={toggleFilter} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export * from "ce/components/issues/worklog/activity/filter-root";
|
||||||
|
|
@ -1,2 +1,4 @@
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
export * from "./worklog-create-button";
|
export * from "./worklog-create-button";
|
||||||
|
|
||||||
|
export * from "./filter-root";
|
||||||
|
|
|
||||||
|
|
@ -323,14 +323,13 @@ export const convertMinutesToHoursAndMinutes = (mins: number): { hours: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description converts minutes to days, hours and minutes
|
* @description converts minutes to hours and minutes string
|
||||||
* @param { number } totalMinutes
|
* @param { number } totalMinutes
|
||||||
* @returns { string } days, hours and minutes
|
* @returns { string } 0h 0m
|
||||||
|
* @example convertMinutesToHoursAndMinutes(150) // Output: 2h 10m
|
||||||
*/
|
*/
|
||||||
export const convertMinutesToDaysHoursMinutes = (totalMinutes: number): string => {
|
export const convertMinutesToHoursMinutesString = (totalMinutes: number): string => {
|
||||||
const days = Math.floor(totalMinutes / (60 * 24));
|
const { hours, minutes } = convertMinutesToHoursAndMinutes(totalMinutes);
|
||||||
const hours = Math.floor((totalMinutes % (60 * 24)) / 60);
|
|
||||||
const minutes = totalMinutes % 60;
|
|
||||||
|
|
||||||
return `${days ? `${days}d ` : ``}${hours ? `${hours}h ` : ``}${minutes ? `${minutes}m ` : ``} `;
|
return `${hours ? `${hours}h ` : ``}${minutes ? `${minutes}m ` : ``}`;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue