From 4dcbda8c13e267a2da29d626f9f99c58fc86e177 Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Sat, 1 Mar 2025 13:07:03 -0700 Subject: [PATCH] make a context menu --- web/package-lock.json | 82 +++-- web/package.json | 1 + web/src/components/ui/context/ContextBase.tsx | 219 +++++++++++ .../ui/context/ContextMenu.stories.tsx | 341 ++++++++++++++++++ web/src/components/ui/context/ContextMenu.tsx | 176 +++++++++ web/src/components/ui/context/index.ts | 0 web/src/components/ui/dropdown/Dropdown.tsx | 4 - .../list/BusterList/BusterListContentMenu.tsx | 6 +- .../ui/list/BusterList/CheckboxColumn.tsx | 25 +- .../ui/list/BusterList/interfaces.ts | 31 +- 10 files changed, 806 insertions(+), 79 deletions(-) create mode 100644 web/src/components/ui/context/ContextBase.tsx create mode 100644 web/src/components/ui/context/ContextMenu.stories.tsx create mode 100644 web/src/components/ui/context/ContextMenu.tsx create mode 100644 web/src/components/ui/context/index.ts diff --git a/web/package-lock.json b/web/package-lock.json index a806e2791..6f1d947e1 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -12,7 +12,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", - "@faker-js/faker": "^9.5.0", + "@faker-js/faker": "^9.5.1", "@fluentui/react-context-selector": "^9.1.72", "@manufac/echarts-simple-transform": "^2.0.11", "@million/lint": "^1.0.14", @@ -20,6 +20,7 @@ "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", + "@radix-ui/react-context-menu": "^2.2.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-select": "^2.1.6", @@ -32,8 +33,8 @@ "@supabase/ssr": "^0.5.2", "@supabase/supabase-js": "^2.49.1", "@tailwindcss/forms": "^0.5.10", - "@tanstack/react-query": "^5.66.9", - "@tanstack/react-query-devtools": "^5.66.9", + "@tanstack/react-query": "^5.66.11", + "@tanstack/react-query-devtools": "^5.66.11", "@vercel/speed-insights": "^1.2.0", "ahooks": "^3.8.4", "antd": "5.23.3", @@ -93,7 +94,7 @@ "react-markdown": "^9.0.3", "react-material-symbols": "^4.4.0", "react-monaco-editor": "^0.58.0", - "react-scan": "^0.2.3", + "react-scan": "^0.2.8", "react-syntax-highlighter": "^15.6.1", "react-virtualized-auto-sizer": "^1.0.25", "rehype-raw": "^7.0.0", @@ -133,7 +134,7 @@ "@types/react-syntax-highlighter": "^15.5.13", "@types/react-window": "^1.8.8", "@types/uuid": "^10.0.0", - "chromatic": "^11.25.2", + "chromatic": "^11.26.1", "cypress": "^13.17.0", "eslint": "^8", "eslint-config-next": "14.2.3", @@ -145,7 +146,7 @@ "monaco-editor-webpack-plugin": "^7.1.0", "postcss": "8.5.3", "sass": "^1.85.1", - "storybook": "^8.6.0", + "storybook": "^8.6.2", "tailwind-scrollbar": "^4.0.1", "tailwindcss": "^4.0.9", "tailwindcss-animate": "^1.0.7", @@ -2427,6 +2428,7 @@ }, "node_modules/@clack/prompts/node_modules/is-unicode-supported": { "version": "1.3.0", + "extraneous": true, "inBundle": true, "license": "MIT", "engines": { @@ -3207,9 +3209,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.5.0.tgz", - "integrity": "sha512-3qbjLv+fzuuCg3umxc9/7YjrEXNaKwHgmig949nfyaTx8eL4FAsvFbu+1JcFUj1YAXofhaDn6JdEUBTYuk0Ssw==", + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.5.1.tgz", + "integrity": "sha512-0fzMEDxkExR2cn731kpDaCCnBGBUOIXEi2S1N5l8Hltp6aPf4soTMJ+g4k8r2sI5oB+rpwIW8Uy/6jkwGpnWPg==", "funding": [ { "type": "opencollective", @@ -5770,6 +5772,34 @@ } } }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.6.tgz", + "integrity": "sha512-aUP99QZ3VU84NPsHeaFt4cQUNgJqFsLLOt/RbbWXszZ6MP0DpDyjkFZORr4RpAEx3sUBk+Kc8h13yGtC5Qw8dg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-direction": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", @@ -7916,9 +7946,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.66.4", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.4.tgz", - "integrity": "sha512-skM/gzNX4shPkqmdTCSoHtJAPMTtmIJNS0hE+xwTTUVYwezArCT34NMermABmBVUg5Ls5aiUXEDXfqwR1oVkcA==", + "version": "5.66.11", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.66.11.tgz", + "integrity": "sha512-ZEYxgHUcohj3sHkbRaw0gYwFxjY5O6M3IXOYXEun7E1rqNhsP8fOtqjJTKPZpVHcdIdrmX4lzZctT4+pts0OgA==", "license": "MIT", "funding": { "type": "github", @@ -7936,12 +7966,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.66.9", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.66.9.tgz", - "integrity": "sha512-NRI02PHJsP5y2gAuWKP+awamTIBFBSKMnO6UVzi03GTclmHHHInH5UzVgzi5tpu4+FmGfsdT7Umqegobtsp23A==", + "version": "5.66.11", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.66.11.tgz", + "integrity": "sha512-uPDiQbZScWkAeihmZ9gAm3wOBA1TmLB1KCB1fJ1hIiEKq3dTT+ja/aYM7wGUD+XiEsY4sDSE7p8VIz/21L2Dow==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.66.4" + "@tanstack/query-core": "5.66.11" }, "funding": { "type": "github", @@ -7952,9 +7982,9 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.66.9", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.66.9.tgz", - "integrity": "sha512-70G6AR35he53SYUcUK6EdqNR18zejCv1rM6900gjZP408EAex56YLwVSeijzk9lWeU2J42G9Fjh0i1WngUTsgw==", + "version": "5.66.11", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.66.11.tgz", + "integrity": "sha512-a+zr2TN4dKpxVlJ9YBOC5YmpGWp2Ez2ZfIzsorVbrs/u2R+bVkLrU1u5e8WHzLdf6tXYueATqgeXWLHrvi4Dig==", "license": "MIT", "dependencies": { "@tanstack/query-devtools": "5.65.0" @@ -7964,7 +7994,7 @@ "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.66.9", + "@tanstack/react-query": "^5.66.11", "react": "^18 || ^19" } }, @@ -11420,9 +11450,9 @@ } }, "node_modules/chromatic": { - "version": "11.25.2", - "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.25.2.tgz", - "integrity": "sha512-/9eQWn6BU1iFsop86t8Au21IksTRxwXAl7if8YHD05L2AbuMjClLWZo5cZojqrJHGKDhTqfrC2X2xE4uSm0iKw==", + "version": "11.26.1", + "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-11.26.1.tgz", + "integrity": "sha512-kVMTigrKI7TOOV04i1lTTIVJsmQ+fj6ZFXyZ3LcdCioOrxO/zCVB1y74iX0iKS++cpi3bJcG+UszkmvptGDEuA==", "dev": true, "license": "MIT", "bin": { @@ -22523,9 +22553,9 @@ } }, "node_modules/react-scan": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/react-scan/-/react-scan-0.2.3.tgz", - "integrity": "sha512-hLUj/yIMCUI/v6Mcym5qABFM1wC4R9TEVzDc6qWWckmEQnMMMPcOApKaq95NpKY+qpP1HPZyb7R25uR/rCipEw==", + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/react-scan/-/react-scan-0.2.8.tgz", + "integrity": "sha512-+6Gvu9b0UMmzV0JkigA7Y2YcjQABiNrweP9l9j8nrutN5OAYLRe4JgfwiUohPFngMD+Y6I5N0kW+okXhvVLGUw==", "license": "MIT", "dependencies": { "@babel/core": "^7.26.0", diff --git a/web/package.json b/web/package.json index 3c4a799ce..b97607c90 100644 --- a/web/package.json +++ b/web/package.json @@ -29,6 +29,7 @@ "@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-collapsible": "^1.1.3", + "@radix-ui/react-context-menu": "^2.2.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-select": "^2.1.6", diff --git a/web/src/components/ui/context/ContextBase.tsx b/web/src/components/ui/context/ContextBase.tsx new file mode 100644 index 000000000..afae319f6 --- /dev/null +++ b/web/src/components/ui/context/ContextBase.tsx @@ -0,0 +1,219 @@ +'use client'; + +import * as React from 'react'; +import * as ContextMenuPrimitive from '@radix-ui/react-context-menu'; +import { Check, ShapeCircle as Circle } from '@/components/ui/icons'; +import { CaretRight } from '@/components/ui/icons/NucleoIconFilled'; + +import { cn } from '@/lib/utils'; +import { DropdownMenuLink } from '../dropdown/DropdownBase'; + +/* +The styling of this and the dropdown is the same. TODO: Refactor to share styles +*/ + +const ContextMenu = ContextMenuPrimitive.Root; + +const ContextMenuTrigger = ContextMenuPrimitive.Trigger; + +const ContextMenuGroup = ContextMenuPrimitive.Group; + +const ContextMenuPortal = ContextMenuPrimitive.Portal; + +const ContextMenuSub = ContextMenuPrimitive.Sub; + +const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup; + +const ContextMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} +
+ +
+
+)); +ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName; + +const baseContentClass = cn( + `data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden `, + 'bg-background text-foreground ', + 'rounded-md border p-1' +); + +const ContextMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName; + +const ContextMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName; + +const ContextMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + truncate?: boolean; + } +>(({ className, inset, truncate, ...props }, ref) => ( + +)); +ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName; + +const itemClass = cn( + 'focus:bg-item-hover focus:text-foreground', + 'relative flex cursor-pointer items-center rounded-sm py-1.5 text-sm transition-colors outline-none select-none', + 'data-[disabled]:pointer-events-none data-[disabled]:opacity-60', + 'gap-1.5' +); + +const ContextMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + truncate?: boolean; + } +>(({ className, children, checked, truncate, ...props }, ref) => ( + + + +
+ +
+
+
+ {children} +
+)); +ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName; + +const ContextMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + +
+ +
+
+
+ {children} +
+)); +ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName; + +const ContextMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName; + +const ContextMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName; + +const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => { + return ( + + ); +}; +ContextMenuShortcut.displayName = 'ContextMenuShortcut'; + +const ContextMenuLink: React.FC<{ + className?: string; + link: string; + linkIcon?: 'arrow-right' | 'arrow-external' | 'caret-right'; +}> = ({ className, link, linkIcon = 'arrow-external' }) => { + return ; +}; + +export { + ContextMenu, + ContextMenuTrigger, + ContextMenuContent, + ContextMenuItem, + ContextMenuCheckboxItem, + ContextMenuRadioItem, + ContextMenuLabel, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuGroup, + ContextMenuPortal, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuRadioGroup, + ContextMenuLink +}; diff --git a/web/src/components/ui/context/ContextMenu.stories.tsx b/web/src/components/ui/context/ContextMenu.stories.tsx new file mode 100644 index 000000000..c9b49a119 --- /dev/null +++ b/web/src/components/ui/context/ContextMenu.stories.tsx @@ -0,0 +1,341 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import { ContextMenu, ContextProps } from './ContextMenu'; +import { + Window, + WindowSettings, + WindowUser, + WindowEdit, + File, + WindowDownload, + CircleCopy +} from '../icons/NucleoIconOutlined'; +import React from 'react'; + +const meta: Meta = { + title: 'UI/Context/ContextMenu', + component: ContextMenu, + parameters: { + layout: 'centered' + }, + argTypes: { + disabled: { + control: 'boolean', + defaultValue: false + } + }, + tags: ['autodocs'] +}; + +export default meta; +type Story = StoryObj; + +// Basic example with simple items +export const Basic: Story = { + args: { + items: [ + { + label: 'Edit', + onClick: () => alert('Edit clicked'), + icon: + }, + { + label: 'Settings', + onClick: () => alert('Settings clicked'), + icon: + }, + { + label: 'Logout', + onClick: () => alert('Logout clicked'), + icon: + } + ] + }, + render: (args) => ( +
+ +
+ Right-click here to open context menu +
+
+
+ ) +}; + +// Example with dividers and shortcuts +export const WithDividersAndShortcuts: Story = { + args: { + items: [ + { + label: 'Profile', + onClick: () => alert('Profile clicked'), + icon: , + shortcut: '⌘P' + }, + { + label: 'Settings', + onClick: () => alert('Settings clicked'), + icon: , + shortcut: '⌘S' + }, + { type: 'divider' }, + { + label: 'Logout', + onClick: () => alert('Logout clicked'), + icon: , + shortcut: '⌘L' + } + ] + }, + render: (args) => ( +
+ +
+ Right-click here to open context menu +
+
+
+ ) +}; + +// Example with nested items +export const WithNestedItems: Story = { + args: { + items: [ + { + label: 'File', + icon: , + items: [ + { + label: 'New', + onClick: () => alert('New file clicked') + }, + { + label: 'Open', + onClick: () => alert('Open file clicked') + }, + { + label: 'Save', + onClick: () => alert('Save file clicked'), + shortcut: '⌘S' + } + ] + }, + { + label: 'Edit', + icon: , + items: [ + { + label: 'Copy', + onClick: () => alert('Copy clicked'), + icon: , + shortcut: '⌘C' + }, + { + label: 'Delete', + onClick: () => alert('Delete clicked'), + icon: , + shortcut: '⌫' + } + ] + } + ] + }, + render: (args) => ( +
+ +
+ Right-click here to open context menu +
+
+
+ ) +}; + +// Example with disabled items +export const WithDisabledItems: Story = { + args: { + items: [ + { + label: 'Edit', + onClick: () => alert('Edit clicked'), + icon: + }, + { + label: 'Delete', + onClick: () => alert('Delete clicked'), + icon: , + disabled: true + }, + { + label: 'Download', + onClick: () => alert('Download clicked'), + icon: + } + ] + }, + render: (args) => ( +
+ +
+ Right-click here to open context menu +
+
+
+ ) +}; + +// Example with loading state +export const WithLoadingItems: Story = { + args: { + items: [ + { + label: 'Normal Item', + onClick: () => alert('Normal clicked') + }, + { + label: 'Loading Item', + loading: true, + onClick: () => alert('Loading clicked') + }, + { type: 'divider' }, + { + label: 'Another Item', + onClick: () => alert('Another clicked') + } + ] + }, + render: (args) => ( +
+ +
+ Right-click here to open context menu +
+
+
+ ) +}; + +// Example with selection +export const WithSelection: Story = { + args: { + items: [ + { + label: 'Option 1', + onClick: () => alert('Option 1 clicked'), + selected: false + }, + { + label: 'Option 2', + onClick: () => alert('Option 2 clicked'), + selected: true + }, + { + label: 'Option 3', + onClick: () => alert('Option 3 clicked'), + selected: false + } + ] + }, + render: (args) => ( +
+ +
+ Right-click here to open context menu +
+
+
+ ) +}; + +// Example with secondary labels and truncation +export const WithSecondaryLabels: Story = { + args: { + items: [ + { + label: 'Document 1', + secondaryLabel: 'Last edited 2 days ago', + onClick: () => alert('Document 1 clicked'), + icon: + }, + { + label: 'Document with a very long name that should be truncated', + secondaryLabel: 'Last edited yesterday', + truncate: true, + onClick: () => alert('Document 2 clicked'), + icon: + }, + { + label: 'Document 3', + secondaryLabel: 'Last edited just now', + onClick: () => alert('Document 3 clicked'), + icon: + } + ] + }, + render: (args) => ( +
+ +
+ Right-click here to open context menu +
+
+
+ ) +}; + +// Example with links +export const WithLinks: Story = { + args: { + items: [ + { + label: 'Documentation', + link: 'https://example.com/docs', + linkIcon: 'arrow-external' + }, + { + label: 'Settings', + link: '/settings', + linkIcon: 'arrow-right' + }, + { + label: 'Profile', + link: '/profile', + linkIcon: 'caret-right' + } + ] + }, + render: (args) => ( +
+ +
+ Right-click here to open context menu +
+
+
+ ) +}; + +// Example with custom width +export const CustomWidth: Story = { + args: { + items: [ + { + label: 'This is a menu item with a very long label that might need to be constrained', + onClick: () => alert('Long item clicked') + }, + { + label: 'Short item', + onClick: () => alert('Short item clicked') + } + ] + }, + render: (args) => ( +
+ +
+ Right-click here to open context menu +
+
+
+ ) +}; diff --git a/web/src/components/ui/context/ContextMenu.tsx b/web/src/components/ui/context/ContextMenu.tsx new file mode 100644 index 000000000..864aa777d --- /dev/null +++ b/web/src/components/ui/context/ContextMenu.tsx @@ -0,0 +1,176 @@ +import React from 'react'; +import { + ContextMenu as ContextMenuPrimitive, + ContextMenuCheckboxItem, + ContextMenuContent, + ContextMenuItem as ContextMenuItemPrimitive, + ContextMenuLabel, + ContextMenuRadioGroup, + ContextMenuRadioItem, + ContextMenuSeparator, + ContextMenuShortcut, + ContextMenuSub, + ContextMenuSubContent, + ContextMenuSubTrigger, + ContextMenuTrigger, + ContextMenuLink, + ContextMenuPortal +} from './ContextBase'; +import { ContextMenuProps } from '@radix-ui/react-context-menu'; +import CircleSpinnerLoader from '../loaders/CircleSpinnerLoader'; +import { cn } from '@/lib/classMerge'; + +export interface ContextMenuItem { + label: React.ReactNode | string; + truncate?: boolean; + secondaryLabel?: string; + showIndex?: boolean; + shortcut?: string; + onClick?: () => void; + icon?: React.ReactNode; + disabled?: boolean; + loading?: boolean; + selected?: boolean; //if a boolean is provided, it will render a checkboxitem component + items?: ContextMenuItem[]; + link?: string; + linkIcon?: 'arrow-right' | 'arrow-external' | 'caret-right'; +} + +export interface ContextMenuDivider { + type: 'divider'; +} + +export type ContextMenuItems = (ContextMenuItem | ContextMenuDivider | React.ReactNode)[]; + +export interface ContextProps extends ContextMenuProps { + items: ContextMenuItems; + className?: string; + disabled?: boolean; +} + +const contextMenuItemKey = (item: ContextMenuItems[number], index: number) => { + if ((item as ContextMenuDivider).type === 'divider') return `divider-${index}`; + return `item-${index}`; +}; + +export const ContextMenu: React.FC = React.memo( + ({ items, className, disabled, children, dir, modal }) => { + return ( + + + {children} + + + {items.map((item, index) => ( + + ))} + + + ); + } +); + +const ContextMenuItemSelector: React.FC<{ + item: ContextMenuItems[number]; + index: number; +}> = React.memo(({ item, index }) => { + if ((item as ContextMenuDivider).type === 'divider') { + return ; + } + + if (typeof item === 'object' && React.isValidElement(item)) { + return item; + } + + return ; +}); + +ContextMenuItemSelector.displayName = 'ContextMenuItemSelector'; + +const ContextMenuItemComponent: React.FC = ({ + label, + showIndex, + shortcut, + onClick, + items, + icon, + disabled, + loading, + selected, + secondaryLabel, + truncate, + link, + linkIcon, + index +}) => { + const isSubItem = items && items.length > 0; + + const content = ( + <> + {showIndex && {index}} + {icon && !loading && {icon}} + {icon && loading && } +
+ + {label} + {!icon && loading && } + + {secondaryLabel && {secondaryLabel}} +
+ {shortcut && {shortcut}} + {link && ( + + )} + + ); + + if (isSubItem) { + return {content}; + } + + const isSelectable = typeof selected === 'boolean'; + + if (isSelectable) { + return ( + + {content} + + ); + } + + return ( + + {content} + + ); +}; + +const ContextSubMenuWrapper = React.memo( + ({ items, children }: { items: ContextMenuItems | undefined; children: React.ReactNode }) => { + return ( + + {children} + + + {items?.map((item, index) => ( + + ))} + + + + ); + } +); +ContextSubMenuWrapper.displayName = 'ContextSubMenuWrapper'; diff --git a/web/src/components/ui/context/index.ts b/web/src/components/ui/context/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/web/src/components/ui/dropdown/Dropdown.tsx b/web/src/components/ui/dropdown/Dropdown.tsx index c625cebd1..b76b991e0 100644 --- a/web/src/components/ui/dropdown/Dropdown.tsx +++ b/web/src/components/ui/dropdown/Dropdown.tsx @@ -51,8 +51,6 @@ export interface DropdownProps extends DropdownMenuProps { items: DropdownItems; selectType?: 'single' | 'multiple' | 'none'; menuHeader?: string | React.ReactNode; //if string it will render a search box - minWidth?: number; - maxWidth?: number; closeOnSelect?: boolean; onSelect?: (itemId: string) => void; align?: 'start' | 'center' | 'end'; @@ -73,8 +71,6 @@ export const Dropdown: React.FC = React.memo( items = [], selectType = 'none', menuHeader, - minWidth = 240, - maxWidth, closeOnSelect = true, onSelect, children, diff --git a/web/src/components/ui/list/BusterList/BusterListContentMenu.tsx b/web/src/components/ui/list/BusterList/BusterListContentMenu.tsx index ab209ff88..96027eff0 100644 --- a/web/src/components/ui/list/BusterList/BusterListContentMenu.tsx +++ b/web/src/components/ui/list/BusterList/BusterListContentMenu.tsx @@ -2,7 +2,7 @@ import { ConfigProvider, Menu, MenuProps } from 'antd'; import { createStyles } from 'antd-style'; import { AnimatePresence, motion } from 'framer-motion'; import React, { forwardRef, useMemo } from 'react'; -import { BusterListContextMenu, BusterMenuItemType } from './interfaces'; +import { BusterListContextMenu } from './interfaces'; import { MenuItemType } from 'antd/es/menu/interface'; export interface BusterListContentMenuProps { @@ -23,7 +23,7 @@ export const BusterListContentMenu = forwardRef { - (item as BusterMenuItemType)?.onClick?.(id); + item?.onClick?.(id); } }; }) || [], @@ -67,7 +67,7 @@ export const BusterListContentMenu = forwardRef ({ +const useStyles = createStyles(({ token, css }) => ({ popUpClassMenu: css` .busterv2-menu { border: 0.5px solid ${token.colorBorder}; diff --git a/web/src/components/ui/list/BusterList/CheckboxColumn.tsx b/web/src/components/ui/list/BusterList/CheckboxColumn.tsx index cfaf6bc17..958ec0598 100644 --- a/web/src/components/ui/list/BusterList/CheckboxColumn.tsx +++ b/web/src/components/ui/list/BusterList/CheckboxColumn.tsx @@ -1,15 +1,14 @@ import { useMemoizedFn } from 'ahooks'; import React from 'react'; import { MemoizedCheckbox } from './MemoizedCheckbox'; -import { createStyles } from 'antd-style'; import { WIDTH_OF_CHECKBOX_COLUMN } from './config'; +import { cn } from '@/lib/classMerge'; export const CheckboxColumn: React.FC<{ checkStatus: 'checked' | 'unchecked' | 'indeterminate' | undefined; onChange: (v: boolean) => void; className?: string; }> = React.memo(({ checkStatus, onChange, className = '' }) => { - const { styles, cx } = useStyles(); const showBox = checkStatus === 'checked'; //|| checkStatus === 'indeterminate'; const onClickStopPropagation = useMemoizedFn((e: React.MouseEvent) => { @@ -19,11 +18,13 @@ export const CheckboxColumn: React.FC<{ return (
({ - checkboxColumn: css` - padding-left: 4px; - padding-right: 0px; - width: ${WIDTH_OF_CHECKBOX_COLUMN}px; - min-width: ${WIDTH_OF_CHECKBOX_COLUMN}px; - display: flex; - align-items: center; - justify-content: center; - ` -})); diff --git a/web/src/components/ui/list/BusterList/interfaces.ts b/web/src/components/ui/list/BusterList/interfaces.ts index 67997abee..1ade96d7d 100644 --- a/web/src/components/ui/list/BusterList/interfaces.ts +++ b/web/src/components/ui/list/BusterList/interfaces.ts @@ -1,11 +1,5 @@ -import { MenuProps } from 'antd'; -import type { MenuDividerType } from 'antd/es/menu/interface'; -import type { - MenuItemType as RcMenuItemType, - SubMenuType as RcSubMenuType -} from 'rc-menu/lib/interface'; - import React from 'react'; +import { Dropdown } from '../../dropdown'; export interface BusterListProps { columns: BusterListColumn[]; hideLastRowBorder?: boolean; @@ -49,25 +43,6 @@ export interface BusterListSectionRow { } //CONTEXT MENU INTERFACES -export interface BusterListContextMenu extends Omit { - items: BusterListMenuItemType[]; +export interface BusterListContextMenu { + items: any[]; } - -export interface BusterMenuItemType extends Omit { - danger?: boolean; - icon?: React.ReactNode; - title?: string; - onClick?: (id: string) => void; - key: string; -} -export interface SubMenuType - extends Omit { - icon?: React.ReactNode; - theme?: 'dark' | 'light'; - children: BusterListMenuItemType[]; -} -export type BusterListMenuItemType = - | T - | SubMenuType - | MenuDividerType - | null;