UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: миграция desktop sorting dropdown на общий канон

This commit is contained in:
DCCONSTRUCTIONS 2026-04-22 12:54:02 +03:00
parent e880daf588
commit 882216922e
9 changed files with 395 additions and 342 deletions

View File

@ -0,0 +1,70 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { ReactNode } from "react";
import type { Placement } from "@popperjs/core";
import { FiltersDropdown, FilterOption } from "@/components/issues/issue-layouts/filters/header";
export type TSortingDropdownOption = {
key: string;
title: ReactNode;
icon?: ReactNode;
isChecked: boolean;
onClick: () => void;
shouldRender?: boolean;
disabled?: boolean;
};
export type TSortingDropdownSection = {
key: string;
options: TSortingDropdownOption[];
};
type Props = {
disabled?: boolean;
menuButton: ReactNode;
placement?: Placement;
sections: TSortingDropdownSection[];
title?: ReactNode;
};
export function SortingDropdown(props: Props) {
const { disabled = false, menuButton, placement = "bottom-end", sections, title = "Order by" } = props;
const renderedSections = sections
.map((section) => ({
...section,
options: section.options.filter((option) => option.shouldRender !== false),
}))
.filter((section) => section.options.length > 0);
return (
<FiltersDropdown menuButton={menuButton} placement={placement} disabled={disabled}>
<div className="vertical-scrollbar relative scrollbar-sm h-full w-full overflow-y-auto px-2.5">
<div className="sticky top-0 z-[1] bg-[rgba(8,8,11,0.92)] pb-1 pt-0.5 text-caption-sm-medium text-placeholder backdrop-blur-xl">
{title}
</div>
{renderedSections.map((section, index) => (
<div
key={section.key}
className={index === 0 ? "py-2" : "border-t border-subtle-1 py-2"}
>
{section.options.map((option) => (
<FilterOption
key={option.key}
disabled={option.disabled}
icon={option.icon}
isChecked={option.isChecked}
onClick={option.onClick}
title={option.title}
/>
))}
</div>
))}
</div>
</FiltersDropdown>
);
}

View File

@ -10,7 +10,7 @@ import { INBOX_ISSUE_ORDER_BY_OPTIONS, INBOX_ISSUE_SORT_BY_OPTIONS } from "@plan
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { CheckIcon, ChevronDownIcon } from "@plane/propel/icons"; import { CheckIcon, ChevronDownIcon } from "@plane/propel/icons";
import type { TInboxIssueSortingOrderByKeys, TInboxIssueSortingSortByKeys } from "@plane/types"; import type { TInboxIssueSortingOrderByKeys, TInboxIssueSortingSortByKeys } from "@plane/types";
import { CustomMenu } from "@plane/ui"; import { SortingDropdown } from "@/components/common/sorting-dropdown";
// constants // constants
// helpers // helpers
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
@ -59,33 +59,32 @@ export const InboxIssueOrderByDropdown = observer(function InboxIssueOrderByDrop
</div> </div>
); );
return ( return (
<CustomMenu <SortingDropdown
customButton={useCompactButtons ? smallButton : largeButton} menuButton={useCompactButtons ? smallButton : largeButton}
placement="bottom-end" placement="bottom-end"
maxHeight="lg" title={t("common.order_by.label")}
closeOnSelect sections={[
> {
{INBOX_ISSUE_ORDER_BY_OPTIONS.map((option) => ( key: "field",
<CustomMenu.MenuItem options: INBOX_ISSUE_ORDER_BY_OPTIONS.map((option) => ({
key={option.key} key: option.key,
className="flex items-center justify-between gap-2" title: t(option.i18n_label),
onClick={() => handleInboxIssueSorting("order_by", option.key as TInboxIssueSortingOrderByKeys)} isChecked: !!inboxSorting?.order_by?.includes(option.key),
> onClick: () => handleInboxIssueSorting("order_by", option.key as TInboxIssueSortingOrderByKeys),
{t(option.i18n_label)} icon: inboxSorting?.order_by?.includes(option.key) ? <CheckIcon className="size-3" /> : undefined,
{inboxSorting?.order_by?.includes(option.key) && <CheckIcon className="size-3" />} })),
</CustomMenu.MenuItem> },
))} {
<hr className="my-2 border-subtle" /> key: "direction",
{INBOX_ISSUE_SORT_BY_OPTIONS.map((option) => ( options: INBOX_ISSUE_SORT_BY_OPTIONS.map((option) => ({
<CustomMenu.MenuItem key: option.key,
key={option.key} title: t(option.i18n_label),
className="flex items-center justify-between gap-2" isChecked: !!inboxSorting?.sort_by?.includes(option.key),
onClick={() => handleInboxIssueSorting("sort_by", option.key as TInboxIssueSortingSortByKeys)} onClick: () => handleInboxIssueSorting("sort_by", option.key as TInboxIssueSortingSortByKeys),
> icon: inboxSorting?.sort_by?.includes(option.key) ? <CheckIcon className="size-3" /> : undefined,
{t(option.i18n_label)} })),
{inboxSorting?.sort_by?.includes(option.key) && <CheckIcon className="size-3" />} },
</CustomMenu.MenuItem> ]}
))} />
</CustomMenu>
); );
}); });

View File

@ -7,6 +7,7 @@
import { CheckIcon } from "@plane/propel/icons"; import { CheckIcon } from "@plane/propel/icons";
type Props = { type Props = {
disabled?: boolean;
icon?: React.ReactNode; icon?: React.ReactNode;
isChecked: boolean; isChecked: boolean;
title: React.ReactNode; title: React.ReactNode;
@ -16,13 +17,14 @@ type Props = {
}; };
export function FilterOption(props: Props) { export function FilterOption(props: Props) {
const { icon, isChecked, onClick, title, activePulse = false } = props; const { disabled = false, icon, isChecked, onClick, title, activePulse = false } = props;
return ( return (
<button <button
type="button" type="button"
className="nodedc-dropdown-option" className="nodedc-dropdown-option disabled:cursor-not-allowed disabled:opacity-50"
onClick={onClick} onClick={onClick}
disabled={disabled}
> >
<div <div
className={`grid h-4 w-4 flex-shrink-0 place-items-center border-0 ${ className={`grid h-4 w-4 flex-shrink-0 place-items-center border-0 ${

View File

@ -5,15 +5,16 @@
*/ */
//ui //ui
import { ArrowDownWideNarrow, ArrowUpNarrowWide, CheckIcon, ChevronDownIcon, Eraser, MoveRight } from "lucide-react"; import { ArrowDownWideNarrow, ArrowUpNarrowWide, ChevronDownIcon, Eraser, MoveRight } from "lucide-react";
// constants // constants
import { SPREADSHEET_PROPERTY_DETAILS } from "@plane/constants"; import { SPREADSHEET_PROPERTY_DETAILS } from "@plane/constants";
// i18n // i18n
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// types // types
import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueOrderByOptions } from "@plane/types"; import type { IIssueDisplayFilterOptions, IIssueDisplayProperties, TIssueOrderByOptions } from "@plane/types";
import { CustomMenu, Row } from "@plane/ui"; import { Row } from "@plane/ui";
import useLocalStorage from "@/hooks/use-local-storage"; import useLocalStorage from "@/hooks/use-local-storage";
import { SortingDropdown } from "@/components/common/sorting-dropdown";
import { SpreadSheetPropertyIcon } from "../../utils"; import { SpreadSheetPropertyIcon } from "../../utils";
interface Props { interface Props {
@ -48,11 +49,8 @@ export function HeaderColumn(props: Props) {
if (!propertyDetails) return null; if (!propertyDetails) return null;
return ( return (
<CustomMenu <SortingDropdown
customButtonClassName="clickable !w-full" menuButton={
customButtonTabIndex={-1}
className="!w-full"
customButton={
<Row className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-13 text-secondary hover:text-primary"> <Row className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-13 text-secondary hover:text-primary">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{<SpreadSheetPropertyIcon iconKey={propertyDetails.icon} className="h-4 w-4 text-placeholder" />} {<SpreadSheetPropertyIcon iconKey={propertyDetails.icon} className="h-4 w-4 text-placeholder" />}
@ -72,63 +70,66 @@ export function HeaderColumn(props: Props) {
</div> </div>
</Row> </Row>
} }
onMenuClose={onClose}
placement="bottom-start" placement="bottom-start"
closeOnSelect title={t("common.order_by.label")}
> sections={[
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}> {
<div key: "sorting",
className={`flex items-center justify-between gap-1.5 px-1 ${ options: [
selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}` {
? "text-primary" key: "ascending",
: "text-secondary hover:text-primary" title: (
}`}
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" /> <ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.ascendingOrderTitle}</span> <span>{propertyDetails.ascendingOrderTitle}</span>
<MoveRight className="h-3 w-3" /> <MoveRight className="h-3 w-3" />
<span>{propertyDetails.descendingOrderTitle}</span> <span>{propertyDetails.descendingOrderTitle}</span>
</div> </div>
),
{selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}` && <CheckIcon className="h-3 w-3" />} isChecked: selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}`,
</div> onClick: () => {
</CustomMenu.MenuItem> handleOrderBy(propertyDetails.ascendingOrderKey, property);
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.descendingOrderKey, property)}> onClose();
<div },
className={`flex items-center justify-between gap-1.5 px-1 ${ },
selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}` {
? "text-primary" key: "descending",
: "text-secondary hover:text-primary" title: (
}`}
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" /> <ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.descendingOrderTitle}</span> <span>{propertyDetails.descendingOrderTitle}</span>
<MoveRight className="h-3 w-3" /> <MoveRight className="h-3 w-3" />
<span>{propertyDetails.ascendingOrderTitle}</span> <span>{propertyDetails.ascendingOrderTitle}</span>
</div> </div>
),
{selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}` && ( isChecked: selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}`,
<CheckIcon className="h-3 w-3" /> onClick: () => {
)} handleOrderBy(propertyDetails.descendingOrderKey, property);
</div> onClose();
</CustomMenu.MenuItem> },
{selectedMenuItem && },
selectedMenuItem !== "" && {
displayFilters?.order_by !== "-created_at" && key: "clear",
selectedMenuItem.includes(property) && ( title: (
<CustomMenu.MenuItem <div className="flex items-center gap-2">
className={`mt-0.5 ${selectedMenuItem === `-created_at_${property}` ? "bg-layer-1" : ""}`}
key={property}
onClick={() => handleOrderBy("-created_at", property)}
>
<div className="flex items-center gap-2 px-1">
<Eraser className="h-3 w-3" /> <Eraser className="h-3 w-3" />
<span>{t("common.actions.clear_sorting")}</span> <span>{t("common.actions.clear_sorting")}</span>
</div> </div>
</CustomMenu.MenuItem> ),
)} isChecked: false,
</CustomMenu> onClick: () => {
handleOrderBy("-created_at", property);
onClose();
},
shouldRender:
!!selectedMenuItem &&
selectedMenuItem !== "" &&
displayFilters?.order_by !== "-created_at" &&
selectedMenuItem.includes(property),
},
],
},
]}
/>
); );
} }

View File

@ -8,10 +8,10 @@ import { ArrowDownWideNarrow, ArrowUpWideNarrow } from "lucide-react";
import { MODULE_ORDER_BY_OPTIONS } from "@plane/constants"; import { MODULE_ORDER_BY_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { getButtonStyling } from "@plane/propel/button"; import { getButtonStyling } from "@plane/propel/button";
import { CheckIcon, ChevronDownIcon } from "@plane/propel/icons"; import { ChevronDownIcon } from "@plane/propel/icons";
import type { TModuleOrderByOptions } from "@plane/types"; import type { TModuleOrderByOptions } from "@plane/types";
// ui // ui
import { CustomMenu } from "@plane/ui"; import { SortingDropdown } from "@/components/common/sorting-dropdown";
// helpers // helpers
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// types // types
@ -33,8 +33,8 @@ export function ModuleOrderByDropdown(props: Props) {
const isManual = value?.includes("sort_order"); const isManual = value?.includes("sort_order");
return ( return (
<CustomMenu <SortingDropdown
customButton={ menuButton={
<div className={cn(getButtonStyling("secondary", "lg"), "px-2 text-tertiary")}> <div className={cn(getButtonStyling("secondary", "lg"), "px-2 text-tertiary")}>
{!isDescending ? <ArrowUpWideNarrow className="size-3" /> : <ArrowDownWideNarrow className="size-3" />} {!isDescending ? <ArrowUpWideNarrow className="size-3" /> : <ArrowDownWideNarrow className="size-3" />}
{orderByDetails && t(orderByDetails?.i18n_label)} {orderByDetails && t(orderByDetails?.i18n_label)}
@ -42,45 +42,44 @@ export function ModuleOrderByDropdown(props: Props) {
</div> </div>
} }
placement="bottom-end" placement="bottom-end"
maxHeight="lg" title={t("common.order_by.label")}
closeOnSelect sections={[
> {
{MODULE_ORDER_BY_OPTIONS.map((option) => ( key: "field",
<CustomMenu.MenuItem options: MODULE_ORDER_BY_OPTIONS.map((option) => ({
key={option.key} key: option.key,
className="flex items-center justify-between gap-2" title: t(option.i18n_label),
onClick={() => { isChecked: !!value?.includes(option.key),
onClick: () => {
if (isDescending && !isManual) onChange(`-${option.key}` as TModuleOrderByOptions); if (isDescending && !isManual) onChange(`-${option.key}` as TModuleOrderByOptions);
else onChange(option.key); else onChange(option.key);
}} },
> })),
{t(option.i18n_label)} },
{value?.includes(option.key) && <CheckIcon className="h-3 w-3" />} {
</CustomMenu.MenuItem> key: "direction",
))} options: [
{!isManual && ( {
<> key: "ascending",
<hr className="my-2 border-subtle" /> title: "Ascending",
<CustomMenu.MenuItem isChecked: !isDescending,
className="flex items-center justify-between gap-2" onClick: () => {
onClick={() => {
if (isDescending) onChange(value.slice(1) as TModuleOrderByOptions); if (isDescending) onChange(value.slice(1) as TModuleOrderByOptions);
}} },
> shouldRender: !isManual,
Ascending },
{!isDescending && <CheckIcon className="h-3 w-3" />} {
</CustomMenu.MenuItem> key: "descending",
<CustomMenu.MenuItem title: "Descending",
className="flex items-center justify-between gap-2" isChecked: isDescending,
onClick={() => { onClick: () => {
if (!isDescending) onChange(`-${value}` as TModuleOrderByOptions); if (!isDescending) onChange(`-${value}` as TModuleOrderByOptions);
}} },
> shouldRender: !isManual,
Descending },
{isDescending && <CheckIcon className="h-3 w-3" />} ],
</CustomMenu.MenuItem> },
</> ]}
)} />
</CustomMenu>
); );
} }

View File

@ -8,9 +8,8 @@ import { ArrowDownWideNarrow, ArrowUpWideNarrow } from "lucide-react";
// plane imports // plane imports
import { getButtonStyling } from "@plane/propel/button"; import { getButtonStyling } from "@plane/propel/button";
// types // types
import { CheckIcon } from "@plane/propel/icons";
import type { TPageFiltersSortBy, TPageFiltersSortKey } from "@plane/types"; import type { TPageFiltersSortBy, TPageFiltersSortKey } from "@plane/types";
import { CustomMenu } from "@plane/ui"; import { SortingDropdown } from "@/components/common/sorting-dropdown";
type Props = { type Props = {
onChange: (value: { key?: TPageFiltersSortKey; order?: TPageFiltersSortBy }) => void; onChange: (value: { key?: TPageFiltersSortKey; order?: TPageFiltersSortBy }) => void;
@ -34,56 +33,56 @@ export function PageOrderByDropdown(props: Props) {
const isDescending = sortBy === "desc"; const isDescending = sortBy === "desc";
return ( return (
<CustomMenu <SortingDropdown
customButton={ menuButton={
<div className={getButtonStyling("secondary", "lg")}> <div className={getButtonStyling("secondary", "lg")}>
{!isDescending ? <ArrowUpWideNarrow className="size-3" /> : <ArrowDownWideNarrow className="size-3" />} {!isDescending ? <ArrowUpWideNarrow className="size-3" /> : <ArrowDownWideNarrow className="size-3" />}
{orderByDetails?.label} {orderByDetails?.label}
</div> </div>
} }
placement="bottom-end" placement="bottom-end"
maxHeight="lg" title="Order by"
closeOnSelect sections={[
> {
{PAGE_SORTING_KEY_OPTIONS.map((option) => ( key: "field",
<CustomMenu.MenuItem options: PAGE_SORTING_KEY_OPTIONS.map((option) => ({
key={option.key} key: option.key,
className="flex items-center justify-between gap-2" title: option.label,
onClick={() => isChecked: sortKey === option.key,
onClick: () =>
onChange({ onChange({
key: option.key, key: option.key,
}) }),
} })),
> },
{option.label} {
{sortKey === option.key && <CheckIcon className="h-3 w-3" />} key: "direction",
</CustomMenu.MenuItem> options: [
))} {
<hr className="my-2 border-subtle" /> key: "ascending",
<CustomMenu.MenuItem title: "Ascending",
className="flex items-center justify-between gap-2" isChecked: !isDescending,
onClick={() => { onClick: () => {
if (isDescending) if (isDescending)
onChange({ onChange({
order: "asc", order: "asc",
}); });
}} },
> },
Ascending {
{!isDescending && <CheckIcon className="h-3 w-3" />} key: "descending",
</CustomMenu.MenuItem> title: "Descending",
<CustomMenu.MenuItem isChecked: isDescending,
className="flex items-center justify-between gap-2" onClick: () => {
onClick={() => {
if (!isDescending) if (!isDescending)
onChange({ onChange({
order: "desc", order: "desc",
}); });
}} },
> },
Descending ],
{isDescending && <CheckIcon className="h-3 w-3" />} },
</CustomMenu.MenuItem> ]}
</CustomMenu> />
); );
} }

View File

@ -9,9 +9,8 @@ import { ArrowDownWideNarrow } from "lucide-react";
import { PROJECT_ORDER_BY_OPTIONS } from "@plane/constants"; import { PROJECT_ORDER_BY_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { getButtonStyling } from "@plane/propel/button"; import { getButtonStyling } from "@plane/propel/button";
import { CheckIcon } from "@plane/propel/icons";
import type { TProjectOrderByOptions } from "@plane/types"; import type { TProjectOrderByOptions } from "@plane/types";
import { CustomMenu } from "@plane/ui"; import { SortingDropdown } from "@/components/common/sorting-dropdown";
type Props = { type Props = {
onChange: (value: TProjectOrderByOptions) => void; onChange: (value: TProjectOrderByOptions) => void;
@ -31,61 +30,55 @@ export function ProjectOrderByDropdown(props: Props) {
const isOrderingDisabled = !!value && DISABLED_ORDERING_OPTIONS.includes(value); const isOrderingDisabled = !!value && DISABLED_ORDERING_OPTIONS.includes(value);
return ( return (
<CustomMenu <SortingDropdown
className={`${isMobile ? "flex w-full justify-center" : ""}`} menuButton={
customButton={ <div className={`${isMobile ? "flex w-full justify-center" : ""}`}>
<>
{isMobile ? (
<div className={getButtonStyling("secondary", "lg")}> <div className={getButtonStyling("secondary", "lg")}>
<ArrowDownWideNarrow className="size-3.5 shrink-0" strokeWidth={2} /> <ArrowDownWideNarrow className="size-3.5 shrink-0" strokeWidth={2} />
{orderByDetails && t(orderByDetails?.i18n_label)} {orderByDetails && t(orderByDetails?.i18n_label)}
</div> </div>
) : (
<div className={getButtonStyling("secondary", "lg")}>
<ArrowDownWideNarrow className="size-3.5 shrink-0" strokeWidth={2} />
{orderByDetails && t(orderByDetails?.i18n_label)}
</div> </div>
)}
</>
} }
placement="bottom-end" placement="bottom-end"
closeOnSelect title={t("common.order_by.label")}
> sections={[
{PROJECT_ORDER_BY_OPTIONS.map((option) => ( {
<CustomMenu.MenuItem key: "field",
key={option.key} options: PROJECT_ORDER_BY_OPTIONS.map((option) => ({
className="flex items-center justify-between gap-2" key: option.key,
onClick={() => { title: option ? t(option.i18n_label) : option.key,
isChecked: !!value?.includes(option.key),
onClick: () => {
if (isDescending) if (isDescending)
onChange(option.key == "sort_order" ? option.key : (`-${option.key}` as TProjectOrderByOptions)); onChange(option.key === "sort_order" ? option.key : (`-${option.key}` as TProjectOrderByOptions));
else onChange(option.key); else onChange(option.key);
}} },
> })),
{option && t(option?.i18n_label)} },
{value?.includes(option.key) && <CheckIcon className="h-3 w-3" />} {
</CustomMenu.MenuItem> key: "direction",
))} options: [
<hr className="my-2 border-subtle" /> {
<CustomMenu.MenuItem key: "ascending",
className="flex items-center justify-between gap-2" title: "Ascending",
onClick={() => { isChecked: !isOrderingDisabled && !isDescending,
onClick: () => {
if (isDescending) onChange(value.slice(1) as TProjectOrderByOptions); if (isDescending) onChange(value.slice(1) as TProjectOrderByOptions);
}} },
disabled={isOrderingDisabled} disabled: isOrderingDisabled,
> },
Ascending {
{!isOrderingDisabled && !isDescending && <CheckIcon className="h-3 w-3" />} key: "descending",
</CustomMenu.MenuItem> title: "Descending",
<CustomMenu.MenuItem isChecked: !isOrderingDisabled && isDescending,
className="flex items-center justify-between gap-2" onClick: () => {
onClick={() => {
if (!isDescending) onChange(`-${value}` as TProjectOrderByOptions); if (!isDescending) onChange(`-${value}` as TProjectOrderByOptions);
}} },
disabled={isOrderingDisabled} disabled: isOrderingDisabled,
> },
Descending ],
{!isOrderingDisabled && isDescending && <CheckIcon className="h-3 w-3" />} },
</CustomMenu.MenuItem> ]}
</CustomMenu> />
); );
} }

View File

@ -6,14 +6,14 @@
// ui // ui
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { ArrowDownWideNarrow, ArrowUpNarrowWide, CheckIcon, ChevronDownIcon, Eraser, MoveRight } from "lucide-react"; import { ArrowDownWideNarrow, ArrowUpNarrowWide, ChevronDownIcon, Eraser, MoveRight } from "lucide-react";
// constants // constants
import type { IProjectMemberDisplayProperties, TMemberOrderByOptions } from "@plane/constants"; import type { IProjectMemberDisplayProperties, TMemberOrderByOptions } from "@plane/constants";
import { MEMBER_PROPERTY_DETAILS } from "@plane/constants"; import { MEMBER_PROPERTY_DETAILS } from "@plane/constants";
// i18n // i18n
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// types // types
import { CustomMenu } from "@plane/ui"; import { SortingDropdown } from "@/components/common/sorting-dropdown";
import type { IMemberFilters } from "@/store/member/utils"; import type { IMemberFilters } from "@/store/member/utils";
interface Props { interface Props {
@ -42,11 +42,8 @@ export const MemberHeaderColumn = observer(function MemberHeaderColumn(props: Pr
if (!propertyDetails) return null; if (!propertyDetails) return null;
return ( return (
<CustomMenu <SortingDropdown
customButtonClassName="clickable !w-full" menuButton={
customButtonTabIndex={-1}
className="!w-full"
customButton={
<div className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-13 text-secondary hover:text-primary"> <div className="flex w-full cursor-pointer items-center justify-between gap-1.5 py-2 text-13 text-secondary hover:text-primary">
<span>{t(propertyDetails.i18n_title)}</span> <span>{t(propertyDetails.i18n_title)}</span>
<div className="ml-3 flex"> <div className="ml-3 flex">
@ -65,57 +62,56 @@ export const MemberHeaderColumn = observer(function MemberHeaderColumn(props: Pr
</div> </div>
} }
placement="bottom-end" placement="bottom-end"
closeOnSelect title={t("common.order_by.label")}
> sections={[
{propertyDetails.isSortingAllowed && ( {
<> key: "sorting",
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}> options: propertyDetails.isSortingAllowed
<div ? [
className={`flex items-center justify-between gap-1.5 px-1 ${ {
activeSortingProperty === propertyDetails.ascendingOrderKey key: "ascending",
? "text-primary" title: (
: "text-secondary hover:text-primary"
}`}
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" /> <ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.ascendingOrderTitle}</span> <span>{propertyDetails.ascendingOrderTitle}</span>
<MoveRight className="h-3 w-3" /> <MoveRight className="h-3 w-3" />
<span>{propertyDetails.descendingOrderTitle}</span> <span>{propertyDetails.descendingOrderTitle}</span>
</div> </div>
{activeSortingProperty === propertyDetails.ascendingOrderKey && <CheckIcon className="h-3 w-3" />} ),
</div> isChecked: activeSortingProperty === propertyDetails.ascendingOrderKey,
</CustomMenu.MenuItem> onClick: () => handleOrderBy(propertyDetails.ascendingOrderKey, property),
},
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.descendingOrderKey, property)}> {
<div key: "descending",
className={`flex items-center justify-between gap-1.5 px-1 ${ title: (
activeSortingProperty === propertyDetails.descendingOrderKey
? "text-primary"
: "text-secondary hover:text-primary"
}`}
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" /> <ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.descendingOrderTitle}</span> <span>{propertyDetails.descendingOrderTitle}</span>
<MoveRight className="h-3 w-3" /> <MoveRight className="h-3 w-3" />
<span>{propertyDetails.ascendingOrderTitle}</span> <span>{propertyDetails.ascendingOrderTitle}</span>
</div> </div>
{activeSortingProperty === propertyDetails.descendingOrderKey && <CheckIcon className="h-3 w-3" />} ),
</div> isChecked: activeSortingProperty === propertyDetails.descendingOrderKey,
</CustomMenu.MenuItem> onClick: () => handleOrderBy(propertyDetails.descendingOrderKey, property),
},
{(activeSortingProperty === propertyDetails.ascendingOrderKey || {
activeSortingProperty === propertyDetails.descendingOrderKey) && ( key: "clear",
<CustomMenu.MenuItem className="mt-0.5" key={property} onClick={handleClearSorting}> title: (
<div className="flex items-center gap-2 px-1"> <div className="flex items-center gap-2">
<Eraser className="h-3 w-3" /> <Eraser className="h-3 w-3" />
<span>{t("common.actions.clear_sorting")}</span> <span>{t("common.actions.clear_sorting")}</span>
</div> </div>
</CustomMenu.MenuItem> ),
)} isChecked: false,
</> onClick: handleClearSorting,
)} shouldRender:
</CustomMenu> activeSortingProperty === propertyDetails.ascendingOrderKey ||
activeSortingProperty === propertyDetails.descendingOrderKey,
},
]
: [],
},
]}
/>
); );
}); });

View File

@ -9,9 +9,8 @@ import { ArrowDownWideNarrow, ArrowUpWideNarrow } from "lucide-react";
import { VIEW_SORT_BY_OPTIONS, VIEW_SORTING_KEY_OPTIONS } from "@plane/constants"; import { VIEW_SORT_BY_OPTIONS, VIEW_SORTING_KEY_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { getButtonStyling } from "@plane/propel/button"; import { getButtonStyling } from "@plane/propel/button";
import { CheckIcon } from "@plane/propel/icons";
import type { TViewFiltersSortBy, TViewFiltersSortKey } from "@plane/types"; import type { TViewFiltersSortBy, TViewFiltersSortKey } from "@plane/types";
import { CustomMenu } from "@plane/ui"; import { SortingDropdown } from "@/components/common/sorting-dropdown";
type Props = { type Props = {
onChange: (value: { key?: TViewFiltersSortKey; order?: TViewFiltersSortBy }) => void; onChange: (value: { key?: TViewFiltersSortKey; order?: TViewFiltersSortBy }) => void;
@ -35,51 +34,46 @@ export function ViewOrderByDropdown(props: Props) {
<>{!isDescending ? <ArrowUpWideNarrow className="size-3" /> : <ArrowDownWideNarrow className="size-3" />}</> <>{!isDescending ? <ArrowUpWideNarrow className="size-3" /> : <ArrowDownWideNarrow className="size-3" />}</>
); );
return ( return (
<CustomMenu <SortingDropdown
customButton={ menuButton={
<span className={buttonClassName}> <span className={buttonClassName}>
{!isMobile && icon} {!isMobile && icon}
<span className="shrink-0"> {orderByDetails?.i18n_label && t(orderByDetails?.i18n_label)}</span> <span className="shrink-0"> {orderByDetails?.i18n_label && t(orderByDetails?.i18n_label)}</span>
</span> </span>
} }
placement="bottom-end" placement="bottom-end"
className="flex w-full justify-center" title={t("common.order_by.label")}
maxHeight="lg" sections={[
closeOnSelect {
> key: "field",
{VIEW_SORTING_KEY_OPTIONS.map((option) => ( options: VIEW_SORTING_KEY_OPTIONS.map((option) => ({
<CustomMenu.MenuItem key: option.key,
key={option.key} title: t(option.i18n_label),
className="flex items-center justify-between gap-2" isChecked: sortKey === option.key,
onClick={() => onClick: () =>
onChange({ onChange({
key: option.key as TViewFiltersSortKey, key: option.key as TViewFiltersSortKey,
}) }),
} })),
> },
{t(option.i18n_label)} {
{sortKey === option.key && <CheckIcon className="h-3 w-3" />} key: "direction",
</CustomMenu.MenuItem> options: VIEW_SORT_BY_OPTIONS.map((option) => {
))}
<hr className="my-2 border-subtle" />
{VIEW_SORT_BY_OPTIONS.map((option) => {
const isSelected = (option.key === "asc" && !isDescending) || (option.key === "desc" && isDescending); const isSelected = (option.key === "asc" && !isDescending) || (option.key === "desc" && isDescending);
return ( return {
<CustomMenu.MenuItem key: option.key,
key={option.key} title: t(option.i18n_label),
className="flex items-center justify-between gap-2" isChecked: isSelected,
onClick={() => { onClick: () => {
if (!isSelected) if (!isSelected)
onChange({ onChange({
order: option.key as TViewFiltersSortBy, order: option.key as TViewFiltersSortBy,
}); });
}} },
> };
{t(option.i18n_label)} }),
{isSelected && <CheckIcon className="h-3 w-3" />} },
</CustomMenu.MenuItem> ]}
); />
})}
</CustomMenu>
); );
} }