diff --git a/plane-src/apps/web/core/components/common/selection-dropdown.tsx b/plane-src/apps/web/core/components/common/selection-dropdown.tsx new file mode 100644 index 0000000..2bf0dff --- /dev/null +++ b/plane-src/apps/web/core/components/common/selection-dropdown.tsx @@ -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 TSelectionDropdownOption = { + key: string; + title: ReactNode; + icon?: ReactNode; + isChecked: boolean; + onClick: () => void; + shouldRender?: boolean; + disabled?: boolean; + activePulse?: boolean; +}; + +type Props = { + disabled?: boolean; + menuButton: ReactNode; + menuButtonWrapperClassName?: string; + options: TSelectionDropdownOption[]; + placement?: Placement; + title?: ReactNode; +}; + +export function SelectionDropdown(props: Props) { + const { disabled = false, menuButton, menuButtonWrapperClassName, options, placement = "bottom-start", title } = props; + + const renderedOptions = options.filter((option) => option.shouldRender !== false); + + return ( + + {({ closeDropdown }) => ( +
+ {title && ( +
+ {title} +
+ )} +
+ {renderedOptions.map((option) => ( + { + option.onClick(); + closeDropdown(); + }} + title={option.title} + /> + ))} +
+
+ )} +
+ ); +} diff --git a/plane-src/apps/web/core/components/common/sorting-dropdown.tsx b/plane-src/apps/web/core/components/common/sorting-dropdown.tsx index b9297d5..e5a469a 100644 --- a/plane-src/apps/web/core/components/common/sorting-dropdown.tsx +++ b/plane-src/apps/web/core/components/common/sorting-dropdown.tsx @@ -43,28 +43,33 @@ export function SortingDropdown(props: Props) { return ( -
-
- {title} -
- {renderedSections.map((section, index) => ( -
- {section.options.map((option) => ( - - ))} + {({ closeDropdown }) => ( +
+
+ {title}
- ))} -
+ {renderedSections.map((section, index) => ( +
+ {section.options.map((option) => ( + { + option.onClick(); + closeDropdown(); + }} + title={option.title} + /> + ))} +
+ ))} +
+ )} ); } diff --git a/plane-src/apps/web/core/components/home/widgets/recents/filters.tsx b/plane-src/apps/web/core/components/home/widgets/recents/filters.tsx index 8a71060..4c1d106 100644 --- a/plane-src/apps/web/core/components/home/widgets/recents/filters.tsx +++ b/plane-src/apps/web/core/components/home/widgets/recents/filters.tsx @@ -8,8 +8,8 @@ import { observer } from "mobx-react"; import { useTranslation } from "@plane/i18n"; import { ChevronDownIcon } from "@plane/propel/icons"; import type { TRecentActivityFilterKeys } from "@plane/types"; -import { CustomMenu } from "@plane/ui"; import { cn } from "@plane/utils"; +import { SelectionDropdown } from "@/components/common/selection-dropdown"; export type TFiltersDropdown = { className?: string; @@ -22,36 +22,24 @@ export const FiltersDropdown = observer(function FiltersDropdown(props: TFilters const { className, activeFilter, setActiveFilter, filters } = props; const { t } = useTranslation(); - function DropdownOptions() { - return filters?.map((filter) => ( - { - setActiveFilter(filter.name); - }} - > -
{t(filter.i18n_key)}
-
- )); - } - const title = activeFilter ? filters?.find((filter) => filter.name === activeFilter)?.i18n_key : ""; return ( - + {t(title || "")} - +
} - customButtonClassName="flex justify-center" - closeOnSelect - > - - + menuButtonWrapperClassName={cn("flex w-fit justify-center text-11 text-secondary", className)} + placement="bottom-start" + options={filters.map((filter) => ({ + key: filter.name, + title:
{t(filter.i18n_key)}
, + icon: filter.icon, + isChecked: activeFilter === filter.name, + onClick: () => setActiveFilter(filter.name), + }))} + /> ); }); diff --git a/plane-src/apps/web/core/components/inbox/modals/create-modal/issue-properties.tsx b/plane-src/apps/web/core/components/inbox/modals/create-modal/issue-properties.tsx index 1102756..26a654f 100644 --- a/plane-src/apps/web/core/components/inbox/modals/create-modal/issue-properties.tsx +++ b/plane-src/apps/web/core/components/inbox/modals/create-modal/issue-properties.tsx @@ -4,12 +4,14 @@ * See the LICENSE file for details. */ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react"; +import { PencilLine, Unlink2 } from "lucide-react"; import { ETabIndices } from "@plane/constants"; +import { useTranslation } from "@plane/i18n"; import { ParentPropertyIcon } from "@plane/propel/icons"; import type { ISearchIssueResponse, TIssue } from "@plane/types"; -import { CustomMenu } from "@plane/ui"; +import { ActionDropdown, type TContextMenuItem } from "@plane/ui"; import { cn, getDate, getTabIndex, renderFormattedPayloadDate } from "@plane/utils"; // components import { CycleDropdown } from "@/components/dropdowns/cycle"; @@ -40,6 +42,7 @@ export const InboxIssueProperties = observer(function InboxIssueProperties(props // hooks const { areEstimateEnabledByProjectId } = useProjectEstimates(); const { isMobile } = usePlatformOS(); + const { t } = useTranslation(); // states const [parentIssueModalOpen, setParentIssueModalOpen] = useState(false); const [selectedParentIssue, setSelectedParentIssue] = useState(undefined); @@ -54,6 +57,26 @@ export const InboxIssueProperties = observer(function InboxIssueProperties(props const maxDate = getDate(targetDate); maxDate?.setDate(maxDate.getDate()); + const parentMenuItems = useMemo( + () => [ + { + key: "change-parent", + title: t("change_parent_issue"), + icon: PencilLine, + action: () => setParentIssueModalOpen(true), + }, + { + key: "remove-parent", + title: t("remove_parent_issue"), + icon: Unlink2, + action: () => { + handleData("parent_id", ""); + setSelectedParentIssue(undefined); + }, + }, + ], + [handleData, t] + ); return (
@@ -185,8 +208,8 @@ export const InboxIssueProperties = observer(function InboxIssueProperties(props {isVisible && (
{selectedParentIssue ? ( - {selectedParentIssue ? `${selectedParentIssue.project__identifier}-${selectedParentIssue.sequence_id}` - : `Add parent`} + : t("add_parent")} } + buttonAsChild + buttonClassName="h-full" + items={parentMenuItems} placement="bottom-start" - className="h-full w-full" - customButtonClassName="h-full" - tabIndex={getIndex("parent_id")} - > - <> - setParentIssueModalOpen(true)}> - Change parent work item - - { - handleData("parent_id", ""); - setSelectedParentIssue(undefined); - }} - > - Remove parent work item - - - + /> ) : ( )} diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx index c790d05..1cf5207 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/helpers/dropdown.tsx @@ -13,7 +13,7 @@ import { Popover, Portal, Transition } from "@headlessui/react"; import { Button } from "@plane/propel/button"; type Props = { - children: React.ReactNode; + children: React.ReactNode | ((props: { closeDropdown: () => void }) => React.ReactNode); icon?: React.ReactElement; miniIcon?: React.ReactNode; title?: string; @@ -21,6 +21,7 @@ type Props = { disabled?: boolean; tabIndex?: number; menuButton?: React.ReactNode; + menuButtonWrapperClassName?: string; isFiltersApplied?: boolean; }; @@ -34,6 +35,7 @@ export function FiltersDropdown(props: Props) { disabled = false, tabIndex, menuButton, + menuButtonWrapperClassName, isFiltersApplied = false, } = props; @@ -46,11 +48,16 @@ export function FiltersDropdown(props: Props) { return ( - {({ open }) => ( + {({ open, close }) => ( <> {menuButton ? ( - ) : ( @@ -109,7 +116,7 @@ export function FiltersDropdown(props: Props) { {...attributes.popper} >
- {children} + {typeof children === "function" ? children({ closeDropdown: close }) : children}
diff --git a/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/mobile-layout-selection.tsx b/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/mobile-layout-selection.tsx index 31e50bb..c59cfbb 100644 --- a/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/mobile-layout-selection.tsx +++ b/plane-src/apps/web/core/components/issues/issue-layouts/filters/header/mobile-layout-selection.tsx @@ -6,10 +6,10 @@ import { ISSUE_LAYOUTS } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; -import { Button } from "@plane/propel/button"; import { ChevronDownIcon } from "@plane/propel/icons"; import type { EIssueLayoutTypes } from "@plane/types"; -import { CustomMenu } from "@plane/ui"; +import { cn } from "@plane/utils"; +import { SelectionDropdown } from "@/components/common/selection-dropdown"; import { IssueLayoutIcon } from "../../layout-icon"; export function MobileLayoutSelection({ @@ -24,35 +24,26 @@ export function MobileLayoutSelection({ }) { const { t } = useTranslation(); return ( - + {activeLayout && ( - + )} - +
} - customButtonClassName="flex flex-grow justify-center text-secondary text-13" - closeOnSelect - > - {ISSUE_LAYOUTS.filter((l) => layouts.includes(l.key)).map((layout, index) => ( - { - onChange(layout.key); - }} - className="flex items-center gap-2" - > - -
{t(layout.i18n_label)}
-
- ))} - + menuButtonWrapperClassName="flex flex-grow justify-center text-13 text-secondary" + placement="bottom-start" + options={ISSUE_LAYOUTS.filter((layout) => layouts.includes(layout.key)).map((layout) => ({ + key: layout.key, + title:
{t(layout.i18n_label)}
, + icon: , + isChecked: activeLayout === layout.key, + onClick: () => onChange(layout.key), + }))} + /> ); } diff --git a/plane-src/apps/web/core/components/issues/issue-modal/components/default-properties.tsx b/plane-src/apps/web/core/components/issues/issue-modal/components/default-properties.tsx index f740065..2caae7d 100644 --- a/plane-src/apps/web/core/components/issues/issue-modal/components/default-properties.tsx +++ b/plane-src/apps/web/core/components/issues/issue-modal/components/default-properties.tsx @@ -4,17 +4,18 @@ * See the LICENSE file for details. */ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { observer } from "mobx-react"; import type { Control } from "react-hook-form"; import { Controller } from "react-hook-form"; +import { PencilLine, Unlink2 } from "lucide-react"; import { ETabIndices, EUserPermissions, EUserPermissionsLevel } from "@plane/constants"; import { useTranslation } from "@plane/i18n"; import { ParentPropertyIcon } from "@plane/propel/icons"; // types import type { ISearchIssueResponse, TIssue } from "@plane/types"; // ui -import { CustomMenu } from "@plane/ui"; +import { ActionDropdown, type TContextMenuItem } from "@plane/ui"; import { getDate, renderFormattedPayloadDate, getTabIndex } from "@plane/utils"; // components import { CycleDropdown } from "@/components/dropdowns/cycle"; @@ -46,7 +47,7 @@ type TIssueDefaultPropertiesProps = { parentId: string | null; isDraft: boolean; handleFormChange: () => void; - setSelectedParentIssue: (issue: ISearchIssueResponse) => void; + setSelectedParentIssue: (issue: ISearchIssueResponse | null) => void; }; export const IssueDefaultProperties = observer(function IssueDefaultProperties(props: TIssueDefaultPropertiesProps) { @@ -85,6 +86,23 @@ export const IssueDefaultProperties = observer(function IssueDefaultProperties(p const maxDate = getDate(targetDate); maxDate?.setDate(maxDate.getDate()); const propertyButtonClassName = "nodedc-work-item-property-button"; + const parentMenuItems = useMemo( + () => [ + { + key: "change-parent", + title: t("change_parent_issue"), + icon: PencilLine, + action: () => setParentIssueListModalOpen(true), + }, + { + key: "remove-parent", + title: t("remove_parent_issue"), + icon: Unlink2, + action: () => setSelectedParentIssue(null), + }, + ], + [setSelectedParentIssue, t] + ); return (
@@ -276,49 +294,44 @@ export const IssueDefaultProperties = observer(function IssueDefaultProperties(p )}
{parentId ? ( - - {selectedParentIssue?.project_id && ( - - )} - - } - placement="bottom-start" - className="h-full w-full" - customButtonClassName="h-full" - tabIndex={getIndex("parent_id")} - > - <> - setParentIssueListModalOpen(true)}> - {t("change_parent_issue")} - - ( - { + ( + + {selectedParentIssue?.project_id && ( + + )} + + } + buttonAsChild + buttonClassName="h-full" + items={[ + parentMenuItems[0], + { + ...parentMenuItems[1], + action: () => { onChange(null); handleFormChange(); - }} - > - {t("remove_parent_issue")} - - )} + setSelectedParentIssue(null); + }, + }, + ]} + placement="bottom-start" /> - - + )} + /> ) : (