АРХ - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: канонизация labels, editor и ui dropdown-оберток
This commit is contained in:
parent
5cf2c2130a
commit
bc8081c0f1
|
|
@ -21,7 +21,7 @@ export type TSelectionDropdownOption = {
|
|||
|
||||
type Props = {
|
||||
disabled?: boolean;
|
||||
menuButton: ReactNode;
|
||||
menuButton: ReactNode | ((props: { open: boolean }) => ReactNode);
|
||||
menuButtonWrapperClassName?: string;
|
||||
options: TSelectionDropdownOption[];
|
||||
placement?: Placement;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { observer } from "mobx-react";
|
|||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import type { TDescriptionVersion } from "@plane/types";
|
||||
import { Avatar, CustomMenu } from "@plane/ui";
|
||||
import { Avatar } from "@plane/ui";
|
||||
import { calculateTimeAgo, getFileURL } from "@plane/utils";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
|
|
@ -28,7 +28,7 @@ export const DescriptionVersionsDropdownItem = observer(function DescriptionVers
|
|||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<CustomMenu.MenuItem key={version.id} className="flex items-center gap-1" onClick={() => onClick(version.id)}>
|
||||
<button type="button" className="flex items-center gap-1" onClick={() => onClick(version.id)}>
|
||||
<span className="flex-shrink-0">
|
||||
<Avatar
|
||||
name={versionCreator?.display_name ?? t("common.deactivated_user")}
|
||||
|
|
@ -40,6 +40,6 @@ export const DescriptionVersionsDropdownItem = observer(function DescriptionVers
|
|||
<span className="font-medium">{versionCreator?.display_name ?? t("common.deactivated_user")}</span>
|
||||
<span>{calculateTimeAgo(version.last_saved_at)}</span>
|
||||
</p>
|
||||
</CustomMenu.MenuItem>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -9,12 +9,11 @@ import { History } from "lucide-react";
|
|||
// plane imports
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import type { TDescriptionVersion } from "@plane/types";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { calculateTimeAgo } from "@plane/utils";
|
||||
import { Avatar } from "@plane/ui";
|
||||
import { calculateTimeAgo, cn, getFileURL } from "@plane/utils";
|
||||
// hooks
|
||||
import { useMember } from "@/hooks/store/use-member";
|
||||
// local imports
|
||||
import { DescriptionVersionsDropdownItem } from "./dropdown-item";
|
||||
import { SelectionDropdown } from "@/components/common/selection-dropdown";
|
||||
import type { TDescriptionVersionEntityInformation } from "./root";
|
||||
|
||||
type Props = {
|
||||
|
|
@ -34,13 +33,21 @@ export const DescriptionVersionsDropdown = observer(function DescriptionVersions
|
|||
const lastUpdatedByUserDisplayName = latestVersion?.owned_by
|
||||
? getUserDetails(latestVersion?.owned_by)?.display_name
|
||||
: entityInformation.createdByDisplayName;
|
||||
const latestVersionId = latestVersion?.id;
|
||||
// translation
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<CustomMenu
|
||||
label={
|
||||
<div className="flex items-center gap-1 text-tertiary">
|
||||
<SelectionDropdown
|
||||
disabled={disabled}
|
||||
placement="bottom-end"
|
||||
title={t("description_versions.previously_edited_by")}
|
||||
menuButton={({ open }) => (
|
||||
<div
|
||||
className={cn("flex items-center gap-1 text-tertiary transition-colors", {
|
||||
"text-primary": open,
|
||||
})}
|
||||
>
|
||||
<span className="grid size-4 flex-shrink-0 place-items-center">
|
||||
<History className="size-3.5" />
|
||||
</span>
|
||||
|
|
@ -50,18 +57,32 @@ export const DescriptionVersionsDropdown = observer(function DescriptionVersions
|
|||
{calculateTimeAgo(lastUpdatedAt)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
menuButtonWrapperClassName="rounded-sm"
|
||||
options={
|
||||
versions?.map((version) => {
|
||||
const versionCreator = version.owned_by ? getUserDetails(version.owned_by) : null;
|
||||
|
||||
return {
|
||||
key: version.id,
|
||||
isChecked: version.id === latestVersionId,
|
||||
onClick: () => onVersionClick(version.id),
|
||||
icon: (
|
||||
<Avatar
|
||||
name={versionCreator?.display_name ?? t("common.deactivated_user")}
|
||||
size="sm"
|
||||
src={getFileURL(versionCreator?.avatar_url ?? "")}
|
||||
/>
|
||||
),
|
||||
title: (
|
||||
<p className="flex items-center gap-1.5 text-11 text-secondary">
|
||||
<span className="font-medium">{versionCreator?.display_name ?? t("common.deactivated_user")}</span>
|
||||
<span>{calculateTimeAgo(version.last_saved_at)}</span>
|
||||
</p>
|
||||
),
|
||||
};
|
||||
}) ?? []
|
||||
}
|
||||
noBorder
|
||||
noChevron={disabled}
|
||||
placement="bottom-end"
|
||||
optionsClassName="w-[300px]"
|
||||
disabled={disabled}
|
||||
closeOnSelect
|
||||
>
|
||||
<p className="mb-1 text-11 font-medium text-tertiary">{t("description_versions.previously_edited_by")}</p>
|
||||
{versions?.map((version) => (
|
||||
<DescriptionVersionsDropdownItem key={version.id} onClick={onVersionClick} version={version} />
|
||||
))}
|
||||
</CustomMenu>
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ type Props = {
|
|||
placement?: Placement;
|
||||
disabled?: boolean;
|
||||
tabIndex?: number;
|
||||
menuButton?: React.ReactNode;
|
||||
menuButton?: React.ReactNode | ((props: { open: boolean }) => React.ReactNode);
|
||||
menuButtonWrapperClassName?: string;
|
||||
isFiltersApplied?: boolean;
|
||||
};
|
||||
|
|
@ -58,7 +58,7 @@ export function FiltersDropdown(props: Props) {
|
|||
className={menuButtonWrapperClassName}
|
||||
disabled={disabled}
|
||||
>
|
||||
{menuButton}
|
||||
{typeof menuButton === "function" ? menuButton({ open }) : menuButton}
|
||||
</button>
|
||||
) : (
|
||||
<div ref={setReferenceElement}>
|
||||
|
|
|
|||
|
|
@ -5,17 +5,17 @@
|
|||
*/
|
||||
|
||||
import type { MutableRefObject } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
// plane helpers
|
||||
import { PROJECT_SETTINGS_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
import type { ISvgIcons } from "@plane/propel/icons";
|
||||
import { CloseIcon } from "@plane/propel/icons";
|
||||
// types
|
||||
import type { IIssueLabel } from "@plane/types";
|
||||
// ui
|
||||
import { CustomMenu, DragHandle } from "@plane/ui";
|
||||
import type { TContextMenuItem } from "@plane/ui";
|
||||
import { ActionDropdown, DragHandle } from "@plane/ui";
|
||||
// helpers
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
|
|
@ -52,11 +52,20 @@ export function LabelItemBlock(props: ILabelItemBlock) {
|
|||
draggable = true,
|
||||
} = props;
|
||||
// states
|
||||
const [isMenuActive, setIsMenuActive] = useState(true);
|
||||
// refs
|
||||
const actionSectionRef = useRef<HTMLDivElement | null>(null);
|
||||
const [isMenuActive, setIsMenuActive] = useState(false);
|
||||
|
||||
useOutsideClickDetector(actionSectionRef, () => setIsMenuActive(false));
|
||||
const actionMenuItems: TContextMenuItem[] = customMenuItems
|
||||
.filter(({ isVisible }) => isVisible)
|
||||
.map(({ onClick, CustomIcon, text, key }) => ({
|
||||
key,
|
||||
action: () => onClick(label),
|
||||
customContent: (
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<CustomIcon className="size-4" />
|
||||
<span>{text}</span>
|
||||
</span>
|
||||
),
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="group flex items-center">
|
||||
|
|
@ -74,26 +83,13 @@ export function LabelItemBlock(props: ILabelItemBlock) {
|
|||
|
||||
{!disabled && (
|
||||
<div
|
||||
ref={actionSectionRef}
|
||||
className={`absolute right-2.5 flex items-center gap-2 px-4 ${
|
||||
isMenuActive || isLabelGroup
|
||||
? "opacity-100"
|
||||
: "opacity-0 group-hover:pointer-events-auto group-hover:opacity-100"
|
||||
} ${isLabelGroup && "-top-0.5"}`}
|
||||
>
|
||||
<CustomMenu ellipsis menuButtonOnClick={() => setIsMenuActive(!isMenuActive)} useCaptureForOutsideClick>
|
||||
{customMenuItems.map(
|
||||
({ isVisible, onClick, CustomIcon, text, key }) =>
|
||||
isVisible && (
|
||||
<CustomMenu.MenuItem key={key} onClick={() => onClick(label)}>
|
||||
<span className="flex items-center justify-start gap-2">
|
||||
<CustomIcon className="size-4" />
|
||||
<span>{text}</span>
|
||||
</span>
|
||||
</CustomMenu.MenuItem>
|
||||
)
|
||||
)}
|
||||
</CustomMenu>
|
||||
<ActionDropdown placement="bottom-end" onOpenChange={setIsMenuActive} items={actionMenuItems} />
|
||||
{!isLabelGroup && (
|
||||
<div className="py-0.5">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@
|
|||
import React, { useEffect, useState, useCallback } from "react";
|
||||
import type { EditorRefApi } from "@plane/editor";
|
||||
// plane imports
|
||||
import { CheckIcon, ChevronDownIcon } from "@plane/propel/icons";
|
||||
import { ChevronDownIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { CustomMenu } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// constants
|
||||
import type { ToolbarMenuItem } from "@/constants/editor";
|
||||
import { TOOLBAR_ITEMS, TYPOGRAPHY_ITEMS } from "@/constants/editor";
|
||||
import { SelectionDropdown } from "@/components/common/selection-dropdown";
|
||||
// local imports
|
||||
import { ColorDropdown } from "./color-dropdown";
|
||||
|
||||
|
|
@ -86,8 +86,6 @@ export function PageToolbar(props: Props) {
|
|||
return initialStates;
|
||||
});
|
||||
|
||||
const [isTypographyMenuOpen, setIsTypographyMenuOpen] = useState(false);
|
||||
|
||||
const updateActiveStates = useCallback(() => {
|
||||
const newActiveStates: Record<string, boolean> = {};
|
||||
Object.values(toolbarItems)
|
||||
|
|
@ -117,52 +115,39 @@ export function PageToolbar(props: Props) {
|
|||
|
||||
return (
|
||||
<div className="animate-in fade-in flex items-center divide-x divide-subtle-1 overflow-x-scroll duration-200">
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-7 w-24 items-center justify-between gap-2 rounded-sm border-[0.5px] border-strong px-2 text-left text-13 whitespace-nowrap",
|
||||
{
|
||||
"bg-layer-1-selected text-primary": isTypographyMenuOpen,
|
||||
"text-tertiary hover:bg-layer-1-hover": !isTypographyMenuOpen,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{activeTypography?.name || "Text"}
|
||||
<ChevronDownIcon className="size-3 shrink-0" />
|
||||
</span>
|
||||
}
|
||||
className="pr-2"
|
||||
placement="bottom-start"
|
||||
closeOnSelect
|
||||
maxHeight="lg"
|
||||
menuButtonOnClick={() => setIsTypographyMenuOpen((prev) => !prev)}
|
||||
onMenuClose={() => setIsTypographyMenuOpen(false)}
|
||||
>
|
||||
{TYPOGRAPHY_ITEMS.map((item) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={item.renderKey}
|
||||
className={cn("flex items-center justify-between gap-2", {
|
||||
"bg-layer-transparent-selected text-primary": activeTypography?.itemKey === item.itemKey,
|
||||
"hover:bg-layer-transparent-hover": !(activeTypography?.itemKey === item.itemKey),
|
||||
})}
|
||||
onClick={() => {
|
||||
<div className="pr-2">
|
||||
<SelectionDropdown
|
||||
placement="bottom-start"
|
||||
menuButton={({ open }) => (
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-7 w-24 items-center justify-between gap-2 rounded-sm border-[0.5px] border-strong px-2 text-left text-13 whitespace-nowrap",
|
||||
{
|
||||
"bg-layer-1-selected text-primary": open,
|
||||
"text-tertiary hover:bg-layer-1-hover": !open,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{activeTypography?.name || "Text"}
|
||||
<ChevronDownIcon className="size-3 shrink-0" />
|
||||
</span>
|
||||
)}
|
||||
options={TYPOGRAPHY_ITEMS.map((item) => ({
|
||||
key: item.renderKey,
|
||||
isChecked: activeTypography?.itemKey === item.itemKey,
|
||||
icon: <item.icon className="size-3" />,
|
||||
title: item.name,
|
||||
onClick: () => {
|
||||
if (activeTypography?.itemKey !== item.itemKey) {
|
||||
editorRef.executeMenuItemCommand({
|
||||
itemKey: item.itemKey,
|
||||
...item.extraProps,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<item.icon className="size-3" />
|
||||
{item.name}
|
||||
</span>
|
||||
{activeTypography?.itemKey === item.itemKey && <CheckIcon className="size-3 shrink-0 text-tertiary" />}
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
},
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
<div className="shrink-0">
|
||||
<ColorDropdown
|
||||
handleColorSelect={(key, color) =>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import * as React from "react";
|
|||
// ui
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import type { TContextMenuItem } from "../dropdowns";
|
||||
import { CustomMenu } from "../dropdowns";
|
||||
import { ActionDropdown } from "../dropdowns";
|
||||
import { cn } from "../utils";
|
||||
import { Breadcrumbs } from "./breadcrumbs";
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ export function BreadcrumbNavigationDropdown(props: TBreadcrumbNavigationDropdow
|
|||
function NavigationButton() {
|
||||
return (
|
||||
<Tooltip tooltipContent={selectedItem?.title} position="bottom" disabled={isOpen}>
|
||||
<button
|
||||
<div
|
||||
onClick={(e) => {
|
||||
if (!isLast) {
|
||||
e.preventDefault();
|
||||
|
|
@ -56,7 +56,7 @@ export function BreadcrumbNavigationDropdown(props: TBreadcrumbNavigationDropdow
|
|||
{selectedItemIcon && <Breadcrumbs.Icon>{selectedItemIcon}</Breadcrumbs.Icon>}
|
||||
<Breadcrumbs.Label>{selectedItem?.title}</Breadcrumbs.Label>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
@ -66,8 +66,8 @@ export function BreadcrumbNavigationDropdown(props: TBreadcrumbNavigationDropdow
|
|||
}
|
||||
|
||||
return (
|
||||
<CustomMenu
|
||||
customButton={
|
||||
<ActionDropdown
|
||||
button={
|
||||
<>
|
||||
<NavigationButton />
|
||||
<Breadcrumbs.Separator
|
||||
|
|
@ -86,40 +86,21 @@ export function BreadcrumbNavigationDropdown(props: TBreadcrumbNavigationDropdow
|
|||
}
|
||||
placement="bottom-start"
|
||||
className="h-full rounded-sm"
|
||||
customButtonClassName={cn(
|
||||
buttonClassName={cn(
|
||||
"group flex h-full cursor-pointer items-center gap-0.5 rounded-sm outline-none hover:bg-surface-2",
|
||||
{
|
||||
"bg-surface-2": isOpen,
|
||||
}
|
||||
)}
|
||||
closeOnSelect
|
||||
menuButtonOnClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
}}
|
||||
onMenuClose={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{navigationItems.map((item) => {
|
||||
if (item.shouldRender === false) return null;
|
||||
return (
|
||||
<CustomMenu.MenuItem
|
||||
key={item.key}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (item.key === selectedItemKey) return;
|
||||
item.action();
|
||||
}}
|
||||
className={cn(
|
||||
"flex items-center gap-2",
|
||||
{
|
||||
"text-placeholder": item.disabled,
|
||||
},
|
||||
item.className
|
||||
)}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
onOpenChange={setIsOpen}
|
||||
items={navigationItems.map((item) => ({
|
||||
...item,
|
||||
action: () => {
|
||||
if (item.key === selectedItemKey) return;
|
||||
item.action();
|
||||
},
|
||||
customContent: (
|
||||
<div className="flex w-full items-center gap-2">
|
||||
{item.icon && <item.icon className={cn("size-4 flex-shrink-0", item.iconClassName)} />}
|
||||
<div className="w-full">
|
||||
<h5>{item.title}</h5>
|
||||
|
|
@ -134,9 +115,9 @@ export function BreadcrumbNavigationDropdown(props: TBreadcrumbNavigationDropdow
|
|||
)}
|
||||
</div>
|
||||
{item.key === selectedItemKey && <CheckIcon className="size-3.5 flex-shrink-0" />}
|
||||
</CustomMenu.MenuItem>
|
||||
);
|
||||
})}
|
||||
</CustomMenu>
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,12 @@
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
import { MoreVertical } from "lucide-react";
|
||||
// plane utils
|
||||
import { calculateTimeAgo, cn, getIconForLink } from "@plane/utils";
|
||||
// plane ui
|
||||
import type { TContextMenuItem } from "../dropdowns/context-menu/root";
|
||||
import { CustomMenu } from "../dropdowns/custom-menu";
|
||||
import { ActionDropdown } from "../dropdowns/action-dropdown";
|
||||
|
||||
export type TLinkItemBlockProps = {
|
||||
title: string;
|
||||
|
|
@ -38,36 +39,17 @@ export function LinkItemBlock(props: TLinkItemBlockProps) {
|
|||
</div>
|
||||
{menuItems && (
|
||||
<div className="hidden group-hover:block">
|
||||
<CustomMenu placement="bottom-end" menuItemsClassName="z-20" closeOnSelect verticalEllipsis>
|
||||
{menuItems.map((item) => (
|
||||
<CustomMenu.MenuItem
|
||||
key={item.key}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
item.action();
|
||||
}}
|
||||
className={cn("flex w-full items-center gap-2", {
|
||||
"text-placeholder": item.disabled,
|
||||
})}
|
||||
disabled={item.disabled}
|
||||
>
|
||||
{item.icon && <item.icon className={cn("h-3 w-3", item.iconClassName)} />}
|
||||
<div>
|
||||
<h5>{item.title}</h5>
|
||||
{item.description && (
|
||||
<p
|
||||
className={cn("whitespace-pre-line text-tertiary", {
|
||||
"text-placeholder": item.disabled,
|
||||
})}
|
||||
>
|
||||
{item.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</CustomMenu.MenuItem>
|
||||
))}
|
||||
</CustomMenu>
|
||||
<ActionDropdown
|
||||
placement="bottom-end"
|
||||
menuClassName="z-20"
|
||||
button={
|
||||
<span className="grid place-items-center rounded-sm p-1 text-secondary hover:bg-white/6">
|
||||
<MoreVertical className="h-3.5 w-3.5" />
|
||||
</span>
|
||||
}
|
||||
buttonClassName="grid place-items-center"
|
||||
items={menuItems}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue