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

View File

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

View File

@ -5,15 +5,16 @@
*/
//ui
import { ArrowDownWideNarrow, ArrowUpNarrowWide, CheckIcon, ChevronDownIcon, Eraser, MoveRight } from "lucide-react";
import { ArrowDownWideNarrow, ArrowUpNarrowWide, ChevronDownIcon, Eraser, MoveRight } from "lucide-react";
// constants
import { SPREADSHEET_PROPERTY_DETAILS } from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
// 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 { SortingDropdown } from "@/components/common/sorting-dropdown";
import { SpreadSheetPropertyIcon } from "../../utils";
interface Props {
@ -48,11 +49,8 @@ export function HeaderColumn(props: Props) {
if (!propertyDetails) return null;
return (
<CustomMenu
customButtonClassName="clickable !w-full"
customButtonTabIndex={-1}
className="!w-full"
customButton={
<SortingDropdown
menuButton={
<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">
{<SpreadSheetPropertyIcon iconKey={propertyDetails.icon} className="h-4 w-4 text-placeholder" />}
@ -72,63 +70,66 @@ export function HeaderColumn(props: Props) {
</div>
</Row>
}
onMenuClose={onClose}
placement="bottom-start"
closeOnSelect
>
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
<div
className={`flex items-center justify-between gap-1.5 px-1 ${
selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}`
? "text-primary"
: "text-secondary hover:text-primary"
}`}
>
<div className="flex items-center gap-2">
<ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.ascendingOrderTitle}</span>
<MoveRight className="h-3 w-3" />
<span>{propertyDetails.descendingOrderTitle}</span>
</div>
{selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}` && <CheckIcon className="h-3 w-3" />}
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.descendingOrderKey, property)}>
<div
className={`flex items-center justify-between gap-1.5 px-1 ${
selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}`
? "text-primary"
: "text-secondary hover:text-primary"
}`}
>
<div className="flex items-center gap-2">
<ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.descendingOrderTitle}</span>
<MoveRight className="h-3 w-3" />
<span>{propertyDetails.ascendingOrderTitle}</span>
</div>
{selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}` && (
<CheckIcon className="h-3 w-3" />
)}
</div>
</CustomMenu.MenuItem>
{selectedMenuItem &&
selectedMenuItem !== "" &&
displayFilters?.order_by !== "-created_at" &&
selectedMenuItem.includes(property) && (
<CustomMenu.MenuItem
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" />
<span>{t("common.actions.clear_sorting")}</span>
</div>
</CustomMenu.MenuItem>
)}
</CustomMenu>
title={t("common.order_by.label")}
sections={[
{
key: "sorting",
options: [
{
key: "ascending",
title: (
<div className="flex items-center gap-2">
<ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.ascendingOrderTitle}</span>
<MoveRight className="h-3 w-3" />
<span>{propertyDetails.descendingOrderTitle}</span>
</div>
),
isChecked: selectedMenuItem === `${propertyDetails.ascendingOrderKey}_${property}`,
onClick: () => {
handleOrderBy(propertyDetails.ascendingOrderKey, property);
onClose();
},
},
{
key: "descending",
title: (
<div className="flex items-center gap-2">
<ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.descendingOrderTitle}</span>
<MoveRight className="h-3 w-3" />
<span>{propertyDetails.ascendingOrderTitle}</span>
</div>
),
isChecked: selectedMenuItem === `${propertyDetails.descendingOrderKey}_${property}`,
onClick: () => {
handleOrderBy(propertyDetails.descendingOrderKey, property);
onClose();
},
},
{
key: "clear",
title: (
<div className="flex items-center gap-2">
<Eraser className="h-3 w-3" />
<span>{t("common.actions.clear_sorting")}</span>
</div>
),
isChecked: false,
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 { useTranslation } from "@plane/i18n";
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";
// ui
import { CustomMenu } from "@plane/ui";
import { SortingDropdown } from "@/components/common/sorting-dropdown";
// helpers
import { cn } from "@plane/utils";
// types
@ -33,8 +33,8 @@ export function ModuleOrderByDropdown(props: Props) {
const isManual = value?.includes("sort_order");
return (
<CustomMenu
customButton={
<SortingDropdown
menuButton={
<div className={cn(getButtonStyling("secondary", "lg"), "px-2 text-tertiary")}>
{!isDescending ? <ArrowUpWideNarrow className="size-3" /> : <ArrowDownWideNarrow className="size-3" />}
{orderByDetails && t(orderByDetails?.i18n_label)}
@ -42,45 +42,44 @@ export function ModuleOrderByDropdown(props: Props) {
</div>
}
placement="bottom-end"
maxHeight="lg"
closeOnSelect
>
{MODULE_ORDER_BY_OPTIONS.map((option) => (
<CustomMenu.MenuItem
key={option.key}
className="flex items-center justify-between gap-2"
onClick={() => {
if (isDescending && !isManual) onChange(`-${option.key}` as TModuleOrderByOptions);
else onChange(option.key);
}}
>
{t(option.i18n_label)}
{value?.includes(option.key) && <CheckIcon className="h-3 w-3" />}
</CustomMenu.MenuItem>
))}
{!isManual && (
<>
<hr className="my-2 border-subtle" />
<CustomMenu.MenuItem
className="flex items-center justify-between gap-2"
onClick={() => {
if (isDescending) onChange(value.slice(1) as TModuleOrderByOptions);
}}
>
Ascending
{!isDescending && <CheckIcon className="h-3 w-3" />}
</CustomMenu.MenuItem>
<CustomMenu.MenuItem
className="flex items-center justify-between gap-2"
onClick={() => {
if (!isDescending) onChange(`-${value}` as TModuleOrderByOptions);
}}
>
Descending
{isDescending && <CheckIcon className="h-3 w-3" />}
</CustomMenu.MenuItem>
</>
)}
</CustomMenu>
title={t("common.order_by.label")}
sections={[
{
key: "field",
options: MODULE_ORDER_BY_OPTIONS.map((option) => ({
key: option.key,
title: t(option.i18n_label),
isChecked: !!value?.includes(option.key),
onClick: () => {
if (isDescending && !isManual) onChange(`-${option.key}` as TModuleOrderByOptions);
else onChange(option.key);
},
})),
},
{
key: "direction",
options: [
{
key: "ascending",
title: "Ascending",
isChecked: !isDescending,
onClick: () => {
if (isDescending) onChange(value.slice(1) as TModuleOrderByOptions);
},
shouldRender: !isManual,
},
{
key: "descending",
title: "Descending",
isChecked: isDescending,
onClick: () => {
if (!isDescending) onChange(`-${value}` as TModuleOrderByOptions);
},
shouldRender: !isManual,
},
],
},
]}
/>
);
}

View File

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

View File

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

View File

@ -6,14 +6,14 @@
// ui
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
import type { IProjectMemberDisplayProperties, TMemberOrderByOptions } from "@plane/constants";
import { MEMBER_PROPERTY_DETAILS } from "@plane/constants";
// i18n
import { useTranslation } from "@plane/i18n";
// types
import { CustomMenu } from "@plane/ui";
import { SortingDropdown } from "@/components/common/sorting-dropdown";
import type { IMemberFilters } from "@/store/member/utils";
interface Props {
@ -42,11 +42,8 @@ export const MemberHeaderColumn = observer(function MemberHeaderColumn(props: Pr
if (!propertyDetails) return null;
return (
<CustomMenu
customButtonClassName="clickable !w-full"
customButtonTabIndex={-1}
className="!w-full"
customButton={
<SortingDropdown
menuButton={
<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>
<div className="ml-3 flex">
@ -65,57 +62,56 @@ export const MemberHeaderColumn = observer(function MemberHeaderColumn(props: Pr
</div>
}
placement="bottom-end"
closeOnSelect
>
{propertyDetails.isSortingAllowed && (
<>
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.ascendingOrderKey, property)}>
<div
className={`flex items-center justify-between gap-1.5 px-1 ${
activeSortingProperty === propertyDetails.ascendingOrderKey
? "text-primary"
: "text-secondary hover:text-primary"
}`}
>
<div className="flex items-center gap-2">
<ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.ascendingOrderTitle}</span>
<MoveRight className="h-3 w-3" />
<span>{propertyDetails.descendingOrderTitle}</span>
</div>
{activeSortingProperty === propertyDetails.ascendingOrderKey && <CheckIcon className="h-3 w-3" />}
</div>
</CustomMenu.MenuItem>
<CustomMenu.MenuItem onClick={() => handleOrderBy(propertyDetails.descendingOrderKey, property)}>
<div
className={`flex items-center justify-between gap-1.5 px-1 ${
activeSortingProperty === propertyDetails.descendingOrderKey
? "text-primary"
: "text-secondary hover:text-primary"
}`}
>
<div className="flex items-center gap-2">
<ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.descendingOrderTitle}</span>
<MoveRight className="h-3 w-3" />
<span>{propertyDetails.ascendingOrderTitle}</span>
</div>
{activeSortingProperty === propertyDetails.descendingOrderKey && <CheckIcon className="h-3 w-3" />}
</div>
</CustomMenu.MenuItem>
{(activeSortingProperty === propertyDetails.ascendingOrderKey ||
activeSortingProperty === propertyDetails.descendingOrderKey) && (
<CustomMenu.MenuItem className="mt-0.5" key={property} onClick={handleClearSorting}>
<div className="flex items-center gap-2 px-1">
<Eraser className="h-3 w-3" />
<span>{t("common.actions.clear_sorting")}</span>
</div>
</CustomMenu.MenuItem>
)}
</>
)}
</CustomMenu>
title={t("common.order_by.label")}
sections={[
{
key: "sorting",
options: propertyDetails.isSortingAllowed
? [
{
key: "ascending",
title: (
<div className="flex items-center gap-2">
<ArrowDownWideNarrow className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.ascendingOrderTitle}</span>
<MoveRight className="h-3 w-3" />
<span>{propertyDetails.descendingOrderTitle}</span>
</div>
),
isChecked: activeSortingProperty === propertyDetails.ascendingOrderKey,
onClick: () => handleOrderBy(propertyDetails.ascendingOrderKey, property),
},
{
key: "descending",
title: (
<div className="flex items-center gap-2">
<ArrowUpNarrowWide className="h-3 w-3 stroke-[1.5]" />
<span>{propertyDetails.descendingOrderTitle}</span>
<MoveRight className="h-3 w-3" />
<span>{propertyDetails.ascendingOrderTitle}</span>
</div>
),
isChecked: activeSortingProperty === propertyDetails.descendingOrderKey,
onClick: () => handleOrderBy(propertyDetails.descendingOrderKey, property),
},
{
key: "clear",
title: (
<div className="flex items-center gap-2">
<Eraser className="h-3 w-3" />
<span>{t("common.actions.clear_sorting")}</span>
</div>
),
isChecked: false,
onClick: handleClearSorting,
shouldRender:
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 { useTranslation } from "@plane/i18n";
import { getButtonStyling } from "@plane/propel/button";
import { CheckIcon } from "@plane/propel/icons";
import type { TViewFiltersSortBy, TViewFiltersSortKey } from "@plane/types";
import { CustomMenu } from "@plane/ui";
import { SortingDropdown } from "@/components/common/sorting-dropdown";
type Props = {
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" />}</>
);
return (
<CustomMenu
customButton={
<SortingDropdown
menuButton={
<span className={buttonClassName}>
{!isMobile && icon}
<span className="shrink-0"> {orderByDetails?.i18n_label && t(orderByDetails?.i18n_label)}</span>
</span>
}
placement="bottom-end"
className="flex w-full justify-center"
maxHeight="lg"
closeOnSelect
>
{VIEW_SORTING_KEY_OPTIONS.map((option) => (
<CustomMenu.MenuItem
key={option.key}
className="flex items-center justify-between gap-2"
onClick={() =>
onChange({
key: option.key as TViewFiltersSortKey,
})
}
>
{t(option.i18n_label)}
{sortKey === option.key && <CheckIcon className="h-3 w-3" />}
</CustomMenu.MenuItem>
))}
<hr className="my-2 border-subtle" />
{VIEW_SORT_BY_OPTIONS.map((option) => {
const isSelected = (option.key === "asc" && !isDescending) || (option.key === "desc" && isDescending);
return (
<CustomMenu.MenuItem
key={option.key}
className="flex items-center justify-between gap-2"
onClick={() => {
if (!isSelected)
onChange({
order: option.key as TViewFiltersSortBy,
});
}}
>
{t(option.i18n_label)}
{isSelected && <CheckIcon className="h-3 w-3" />}
</CustomMenu.MenuItem>
);
})}
</CustomMenu>
title={t("common.order_by.label")}
sections={[
{
key: "field",
options: VIEW_SORTING_KEY_OPTIONS.map((option) => ({
key: option.key,
title: t(option.i18n_label),
isChecked: sortKey === option.key,
onClick: () =>
onChange({
key: option.key as TViewFiltersSortKey,
}),
})),
},
{
key: "direction",
options: VIEW_SORT_BY_OPTIONS.map((option) => {
const isSelected = (option.key === "asc" && !isDescending) || (option.key === "desc" && isDescending);
return {
key: option.key,
title: t(option.i18n_label),
isChecked: isSelected,
onClick: () => {
if (!isSelected)
onChange({
order: option.key as TViewFiltersSortBy,
});
},
};
}),
},
]}
/>
);
}