UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: миграция desktop sorting dropdown на общий канон
This commit is contained in:
parent
e880daf588
commit
882216922e
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
})),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 ${
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
});
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue