- {itemModel === 'folder' && (
- ,
- onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
- },
- { type: 'separator', label: itemName },
- {
- key: 'rename',
- label: 'Rename',
- leftSlot: ,
- onSelect: async () => {
- const name = await prompt({
- title: 'Rename Folder',
- description: (
- <>
- Enter a new name for {itemName}
- >
- ),
- name: 'name',
- label: 'Name',
- defaultValue: itemName,
- });
- updateAnyFolder.mutate({ id: itemId, update: (f) => ({ ...f, name }) });
- },
- },
- {
- key: 'deleteFolder',
- label: 'Delete',
- variant: 'danger',
- leftSlot: ,
- onSelect: () => deleteFolder.mutate(),
- },
- { type: 'separator' },
- {
- key: 'createRequest',
- label: 'New Request',
- leftSlot: ,
- onSelect: () => createRequest.mutate({ folderId: itemId, sortPriority: -1 }),
- },
- {
- key: 'createFolder',
- label: 'New Folder',
- leftSlot: ,
- onSelect: () => createFolder.mutate({ folderId: itemId, sortPriority: -1 }),
- },
- ]}
- >
-
-
- )}
+ ,
+ onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
+ },
+ { type: 'separator', label: itemName },
+ {
+ key: 'rename',
+ label: 'Rename',
+ leftSlot: ,
+ onSelect: async () => {
+ const name = await prompt({
+ title: 'Rename Folder',
+ description: (
+ <>
+ Enter a new name for {itemName}
+ >
+ ),
+ name: 'name',
+ label: 'Name',
+ defaultValue: itemName,
+ });
+ updateAnyFolder.mutate({ id: itemId, update: (f) => ({ ...f, name }) });
+ },
+ },
+ {
+ key: 'deleteFolder',
+ label: 'Delete',
+ variant: 'danger',
+ leftSlot: ,
+ onSelect: () => deleteFolder.mutate(),
+ },
+ { type: 'separator' },
+ {
+ key: 'createRequest',
+ label: 'New Request',
+ hotkeyAction: 'request.create',
+ leftSlot: ,
+ onSelect: () => createRequest.mutate({ folderId: itemId, sortPriority: -1 }),
+ },
+ {
+ key: 'createFolder',
+ label: 'New Folder',
+ leftSlot: ,
+ onSelect: () => createFolder.mutate({ folderId: itemId, sortPriority: -1 }),
+ },
+ ]
+ : [
+ {
+ key: 'duplicateRequest',
+ label: 'Duplicate',
+ hotkeyAction: 'request.duplicate',
+ leftSlot: ,
+ onSelect: () => duplicateRequest.mutate(),
+ },
+ {
+ key: 'deleteRequest',
+ variant: 'danger',
+ label: 'Delete',
+ leftSlot: ,
+ onSelect: () => deleteRequest.mutate(),
+ },
+ ]
+ }
+ onClose={() => setShowContextMenu(null)}
+ />
-
+
-
+
);
diff --git a/src-web/components/core/Button.tsx b/src-web/components/core/Button.tsx
index b859dd18..de9c9b1d 100644
--- a/src-web/components/core/Button.tsx
+++ b/src-web/components/core/Button.tsx
@@ -2,7 +2,7 @@ import classNames from 'classnames';
import type { HTMLAttributes, ReactNode } from 'react';
import { forwardRef, memo, useImperativeHandle, useMemo, useRef } from 'react';
import type { HotkeyAction } from '../../hooks/useHotkey';
-import { useHotkey } from '../../hooks/useHotkey';
+import { useFormattedHotkey, useHotkey } from '../../hooks/useHotkey';
import { Icon } from './Icon';
const colorStyles = {
@@ -47,11 +47,15 @@ const _Button = forwardRef(function Button(
rightSlot,
disabled,
hotkeyAction,
+ title,
onClick,
...props
}: ButtonProps,
ref,
) {
+ const hotkeyTrigger = useFormattedHotkey(hotkeyAction ?? null);
+ const fullTitle = hotkeyTrigger ? `${title} ${hotkeyTrigger}` : title;
+
const classes = useMemo(
() =>
classNames(
@@ -88,6 +92,7 @@ const _Button = forwardRef(function Button(
className={classes}
disabled={disabled}
onClick={onClick}
+ title={fullTitle}
{...props}
>
{isLoading ? (
diff --git a/src-web/components/core/Dropdown.tsx b/src-web/components/core/Dropdown.tsx
index e56d6bea..954ea894 100644
--- a/src-web/components/core/Dropdown.tsx
+++ b/src-web/components/core/Dropdown.tsx
@@ -20,8 +20,10 @@ import React, {
useState,
} from 'react';
import { useKey, useKeyPressEvent, useWindowSize } from 'react-use';
+import type { HotkeyAction } from '../../hooks/useHotkey';
import { Overlay } from '../Overlay';
import { Button } from './Button';
+import { HotKey } from './HotKey';
import { Separator } from './Separator';
import { VStack } from './Stacks';
@@ -30,19 +32,20 @@ export type DropdownItemSeparator = {
label?: string;
};
-export type DropdownItem =
- | {
- key: string;
- type?: 'default';
- label: ReactNode;
- variant?: 'danger';
- disabled?: boolean;
- hidden?: boolean;
- leftSlot?: ReactNode;
- rightSlot?: ReactNode;
- onSelect?: () => void;
- }
- | DropdownItemSeparator;
+export type DropdownItemDefault = {
+ key: string;
+ type?: 'default';
+ label: ReactNode;
+ hotkeyAction?: HotkeyAction;
+ variant?: 'danger';
+ disabled?: boolean;
+ hidden?: boolean;
+ leftSlot?: ReactNode;
+ rightSlot?: ReactNode;
+ onSelect?: () => void;
+};
+
+export type DropdownItem = DropdownItemDefault | DropdownItemSeparator;
export interface DropdownProps {
children: ReactElement>;
@@ -126,9 +129,10 @@ export const Dropdown = forwardRef(function Dropdown
{open && triggerRect && (
)}
@@ -136,16 +140,53 @@ export const Dropdown = forwardRef(function Dropdown
);
});
+interface ContextMenuProps {
+ show: { x: number; y: number } | null;
+ className?: string;
+ items: DropdownProps['items'];
+ onClose: () => void;
+}
+
+export const ContextMenu = forwardRef(function ContextMenu(
+ { show, className, items, onClose },
+ ref,
+) {
+ const triggerShape = useMemo(
+ () => ({
+ top: show?.y ?? 0,
+ bottom: show?.y ?? 0,
+ left: show?.x ?? 0,
+ right: show?.x ?? 0,
+ }),
+ [show],
+ );
+
+ if (show === null) {
+ return null;
+ }
+
+ return (
+
+ );
+});
+
interface MenuProps {
className?: string;
defaultSelectedIndex?: number;
items: DropdownProps['items'];
- triggerRect: DOMRect;
+ triggerShape: Pick;
onClose: () => void;
+ showTriangle?: boolean;
}
const Menu = forwardRef, MenuProps>(function Menu(
- { className, items, onClose, triggerRect, defaultSelectedIndex }: MenuProps,
+ { className, items, onClose, triggerShape, defaultSelectedIndex, showTriangle }: MenuProps,
ref,
) {
const containerRef = useRef(null);
@@ -248,21 +289,27 @@ const Menu = forwardRef, MenuPro
const { containerStyles, triangleStyles } = useMemo<{
containerStyles: CSSProperties;
- triangleStyles: CSSProperties;
+ triangleStyles: CSSProperties | null;
}>(() => {
- const docWidth = document.documentElement.getBoundingClientRect().width;
- const spaceRemaining = docWidth - triggerRect.left;
- const top = triggerRect?.bottom + 5;
- const onRight = spaceRemaining < 200;
- const containerStyles = onRight
- ? { top, right: docWidth - triggerRect?.right }
- : { top, left: triggerRect?.left };
+ const docRect = document.documentElement.getBoundingClientRect();
+ const width = triggerShape.right - triggerShape.left;
+ const hSpaceRemaining = docRect.width - triggerShape.left;
+ const vSpaceRemaining = docRect.height - triggerShape.bottom;
+ const top = triggerShape?.bottom + 5;
+ const onRight = hSpaceRemaining < 200;
+ const upsideDown = vSpaceRemaining < 200;
+ const containerStyles = {
+ top: !upsideDown ? top : undefined,
+ bottom: upsideDown ? top : undefined,
+ right: onRight ? docRect.width - triggerShape?.right : undefined,
+ left: !onRight ? triggerShape?.left : undefined,
+ };
const size = { top: '-0.2rem', width: '0.4rem', height: '0.4rem' };
const triangleStyles = onRight
- ? { right: triggerRect.width / 2, marginRight: '-0.2rem', ...size }
- : { left: triggerRect.width / 2, marginLeft: '-0.2rem', ...size };
+ ? { right: width / 2, marginRight: '-0.2rem', ...size }
+ : { left: width / 2, marginLeft: '-0.2rem', ...size };
return { containerStyles, triangleStyles };
- }, [triggerRect]);
+ }, [triggerShape]);
const handleFocus = useCallback(
(i: DropdownItem) => {
@@ -290,11 +337,13 @@ const Menu = forwardRef, MenuPro
style={containerStyles}
className={classNames(className, 'outline-none mt-1 pointer-events-auto fixed z-50')}
>
-
+ {triangleStyles && showTriangle && (
+
+ )}
{containerStyles && (
, MenuPro
interface MenuItemProps {
className?: string;
- item: DropdownItem;
- onSelect: (item: DropdownItem) => void;
- onFocus: (item: DropdownItem) => void;
+ item: DropdownItemDefault;
+ onSelect: (item: DropdownItemDefault) => void;
+ onFocus: (item: DropdownItemDefault) => void;
focused: boolean;
}
@@ -359,7 +408,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
[focused],
);
- if (item.type === 'separator') return ;
+ const rightSlot = item.rightSlot ?? ;
return (