Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • graphpolaris/frontend-v2
  • rijkheere/frontend-v-2-reordering-paoh
2 results
Show changes
Commits on Source (1)
import { Button } from '@/lib';
import type { Meta, StoryObj } from '@storybook/react';
import { useState } from 'react';
import { Menu, MenuCheckboxItem, MenuContent, MenuItem, MenuLabel, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuTrigger } from './';
const meta = {
title: 'Components/Menu',
component: Menu,
tags: ['autodocs'],
parameters: {
layout: 'centered',
docs: {
description: {
component: `
A flexible menu component that supports nested submenus, checkboxes, radio groups, section labels, and custom triggers.
Features include:
- Click or hover activation
- Keyboard navigation
- Custom placement options
- Nested submenus
- Section labels for better grouping
- Checkbox and radio items
- Custom triggers
- Disabled states
`,
},
},
},
argTypes: {
trigger: {
control: 'radio',
options: ['click', 'hover'],
description: 'The interaction method to open the menu',
table: {
defaultValue: { summary: 'click' },
},
},
placement: {
control: 'select',
options: [
'top',
'top-start',
'top-end',
'bottom',
'bottom-start',
'bottom-end',
'right',
'right-start',
'right-end',
'left',
'left-start',
'left-end',
],
description: 'The preferred placement of the menu',
table: {
defaultValue: { summary: 'bottom-start' },
},
},
isOpen: {
control: 'boolean',
description: 'Controls the open state of the menu',
},
onOpenChange: {
action: 'onOpenChange',
description: 'Callback fired when the open state changes',
},
},
args: {
trigger: 'click',
placement: 'bottom-start',
},
} satisfies Meta<typeof Menu>;
export default meta;
type Story = StoryObj<typeof Menu>;
export const Basic: Story = {
name: 'Basic Usage',
render: args => (
<Menu {...args}>
<MenuTrigger label="Open Menu" />
<MenuContent>
<MenuLabel label="File Options" />
<MenuItem label="New File" onClick={() => console.log('New File')} />
<MenuItem label="Open..." onClick={() => console.log('Open')} />
<MenuItem label="Save" onClick={() => console.log('Save')} />
<MenuSeparator />
<MenuItem label="Exit" onClick={() => console.log('Exit')} />
</MenuContent>
</Menu>
),
};
export const WithDisabledItems: Story = {
name: 'With Disabled Items',
render: args => (
<Menu {...args}>
<MenuTrigger label="Edit" />
<MenuContent>
<MenuLabel label="Editing" />
<MenuItem label="Undo" onClick={() => console.log('Undo')} />
<MenuItem label="Redo" disabled onClick={() => console.log('Redo')} />
<MenuSeparator />
<MenuItem label="Cut" disabled onClick={() => console.log('Cut')} />
<MenuItem label="Copy" onClick={() => console.log('Copy')} />
<MenuItem label="Paste" onClick={() => console.log('Paste')} />
</MenuContent>
</Menu>
),
};
export const WithCheckboxItems: Story = {
name: 'With Checkbox Items',
render: function CheckboxStory(args) {
const [items, setItems] = useState({
spellcheck: true,
readonly: false,
hidden: false,
disabled: true,
});
return (
<Menu {...args}>
<MenuTrigger label="Options" />
<MenuContent>
<MenuLabel label="Preferences" />
<MenuCheckboxItem
checked={items.spellcheck}
onCheckedChange={checked => setItems(prev => ({ ...prev, spellcheck: checked }))}
label="Spellcheck"
/>
<MenuCheckboxItem
checked={items.readonly}
onCheckedChange={checked => setItems(prev => ({ ...prev, readonly: checked }))}
label="Read-only"
/>
<MenuCheckboxItem
checked={items.hidden}
onCheckedChange={checked => setItems(prev => ({ ...prev, hidden: checked }))}
label="Hidden"
/>
<MenuCheckboxItem
disabled
checked={items.disabled}
onCheckedChange={checked => setItems(prev => ({ ...prev, disabled: checked }))}
label="Disabled"
/>
</MenuContent>
</Menu>
);
},
};
export const WithRadioItems: Story = {
name: 'With Radio Items',
render: function RadioStory(args) {
const [size, setSize] = useState('medium');
return (
<Menu {...args}>
<MenuTrigger label="Select Size" />
<MenuContent>
<MenuLabel label="Size Options" />
<MenuRadioGroup value={size} onValueChange={setSize}>
<MenuRadioItem value="small" label="Small" />
<MenuRadioItem value="medium" label="Medium" />
<MenuRadioItem value="large" label="Large" />
</MenuRadioGroup>
</MenuContent>
</Menu>
);
},
};
export const NestedSubmenus: Story = {
name: 'Nested Submenus',
render: args => (
<Menu {...args}>
<MenuTrigger label="File" />
<MenuContent>
<MenuLabel label="Actions" />
<MenuItem closeOnClick={true} iconRight="icon-[ic--outline-new-releases]" label="New" onClick={() => alert('New clicked')} />
<MenuItem closeOnClick={true} iconRight="icon-[ic--outline-new-releases]" label="New 2" />
<MenuItem closeOnClick={false} iconLeft="icon-[ic--baseline-content-copy]" rightSlot={<div>Ctrl + C</div>}>
<div>Copy</div>
</MenuItem>
<Menu>
<MenuTrigger label="Open Recent" />
<MenuContent>
<MenuItem label="Document 1.txt" />
<MenuItem label="Document 2.txt" />
<MenuSeparator />
<MenuItem label="Document 1.txt" />
<MenuItem label="Document 2.txt" />
<MenuItem label="Document 1.txt" />
<MenuItem label="Document 2.txt" />
<Menu placement="right-start">
<MenuTrigger label="More Options" />
<MenuContent>
<MenuItem label="Clear List" />
<MenuItem label="Show Folder" />
<MenuItem label="Show Folder1" />
<MenuItem label="Show Folder2" />
<MenuItem label="Show Folder3" />
<MenuItem label="Show Folder4" />
<MenuItem label="Show Folder5" />
<MenuItem label="Show Folder6" onClick={() => alert('New clicked')} />
</MenuContent>
</Menu>
</MenuContent>
</Menu>
<MenuSeparator />
<MenuItem label="Save" onClick={() => console.log('Save')} />
<MenuItem label="Save As..." onClick={() => console.log('Save As')} />
</MenuContent>
</Menu>
),
};
export const CustomTrigger: Story = {
name: 'Custom Trigger',
render: args => (
<Menu {...args}>
<MenuTrigger as="div">
<Button size="xl" rounded variantType="primary" iconComponent="icon-[ic--baseline-more-vert]" />
</MenuTrigger>
<MenuContent>
<MenuLabel label="Custom Actions" />
<MenuItem label="Option 1" />
<MenuItem label="Option 2" />
<MenuItem label="Option 3" />
</MenuContent>
</Menu>
),
};
export const HoverActivation: Story = {
name: 'Hover Activation',
args: {
trigger: 'hover',
},
render: args => (
<Menu {...args}>
<MenuTrigger label="Hover Me" />
<MenuContent>
<MenuLabel label="Quick Actions" />
<MenuItem label="Option 1" />
<MenuItem label="Option 2" />
<Menu trigger="hover">
<MenuTrigger label="More Options" />
<MenuContent>
<MenuItem label="Sub Option 1" />
<MenuItem label="Sub Option 2" />
</MenuContent>
</Menu>
</MenuContent>
</Menu>
),
};
export const RightClickAtCursor: Story = {
name: 'Right click at cursor',
render: args => (
<Menu atCursor {...args}>
<MenuTrigger rightClick>
<div className="flex justify-center items-center rounded bg-blue-300 w-52 h-52">Right click me</div>
</MenuTrigger>
<MenuContent>
<MenuLabel label="Quick Actions" />
<MenuItem label="Option 1" />
<MenuItem label="Option 2" />
<Menu trigger="hover">
<MenuTrigger label="More Options" />
<MenuContent>
<MenuItem label="Sub Option 1" />
<MenuItem label="Sub Option 2" />
</MenuContent>
</Menu>
</MenuContent>
</Menu>
),
};
import { Icon } from '@/lib';
import type { OpenChangeReason, Placement } from '@floating-ui/react';
import {
arrow,
autoUpdate,
flip,
FloatingFocusManager,
FloatingList,
FloatingPortal,
FloatingTree,
offset,
safePolygon,
shift,
useClick,
useClientPoint,
useDismiss,
useFloating,
useFloatingNodeId,
useFloatingParentNodeId,
useFloatingTree,
useHover,
useInteractions,
useListItem,
useListNavigation,
useMergeRefs,
useRole,
useTypeahead,
} from '@floating-ui/react';
import * as React from 'react';
import { useControllableState } from '../primitives';
import styles from './menu.module.scss';
// --- Menu Context and Provider ---
export interface MenuContextProps {
activeIndex: number | null;
setActiveIndex: React.Dispatch<React.SetStateAction<number | null>>;
getItemProps: (userProps?: React.HTMLProps<HTMLElement>) => Record<string, unknown>;
isOpen: boolean;
setIsOpen: (value: boolean, event?: Event, reason?: OpenChangeReason) => void;
trigger: 'rightClick' | 'click' | 'hover';
floating: {
x: number | null;
y: number | null;
strategy: 'absolute' | 'fixed';
refs: ReturnType<typeof useFloating>['refs'];
context: ReturnType<typeof useFloating>['context'];
interactions: ReturnType<typeof useInteractions>;
};
elementsRef: React.RefObject<Array<HTMLButtonElement | null>>;
labelsRef: React.RefObject<Array<string | null>>;
isNested: boolean;
parent: MenuContextProps | null;
menuNodeId?: string;
setIsChildOpen: React.Dispatch<React.SetStateAction<boolean>>;
}
const MenuContext = React.createContext<MenuContextProps | null>(null);
export interface MenuProviderProps extends React.HTMLAttributes<HTMLDivElement> {
trigger?: 'rightClick' | 'click' | 'hover';
placement?: string;
isOpen?: boolean;
defaultIsOpen?: boolean;
onOpenChange?: (open: boolean) => void;
showArrow?: boolean;
children: React.ReactNode;
atCursor?: boolean;
}
export const MenuComponent = React.forwardRef<HTMLDivElement, MenuProviderProps>((props, ref) => {
const {
trigger = 'click',
placement = '',
isOpen: controlledIsOpen,
defaultIsOpen,
onOpenChange: onOpenChangeProp,
showArrow = false,
children,
atCursor = false,
...rest
} = props;
const prevContext = React.useContext(MenuContext); // in case there is a parent Menu
const [isChildOpen, setIsChildOpen] = React.useState(false);
const [isOpen, setIsOpen] = useControllableState<boolean>({
prop: controlledIsOpen,
defaultProp: defaultIsOpen ?? false,
onChange: onOpenChangeProp,
}) as [boolean, React.Dispatch<React.SetStateAction<boolean>>];
const menuIsOpen = isOpen ?? false;
const [activeIndex, setActiveIndex] = React.useState<number | null>(null);
const elementsRef = React.useRef<Array<HTMLButtonElement | null>>([]);
const labelsRef = React.useRef<Array<string | null>>([]);
const arrowRef = React.useRef(null);
const nodeId = useFloatingNodeId();
const parentId = prevContext?.menuNodeId;
const tree = useFloatingTree();
const isNested = parentId != null;
const onOpenChange = (value: boolean, event?: Event, reason?: OpenChangeReason) => {
if (isChildOpen && !value && reason === 'hover') {
return;
}
setIsOpen(value);
};
const middleware = [offset({ mainAxis: isNested ? 0 : 4, alignmentAxis: isNested ? -4 : 0 }), flip(), shift()];
if (showArrow) {
middleware.push(arrow({ element: arrowRef }));
}
const { x, y, strategy, refs, context } = useFloating<HTMLDivElement>({
nodeId,
open: menuIsOpen,
onOpenChange: onOpenChange,
placement: placement !== '' ? (placement as Placement) : isNested ? 'right-start' : 'bottom-start',
middleware,
whileElementsMounted: autoUpdate,
});
const hover = useHover(context, {
enabled: trigger === 'hover' || isNested,
delay: { open: 75 },
handleClose: safePolygon({ requireIntent: false, blockPointerEvents: true }),
});
const click = useClick(context, {
event: 'mousedown',
enabled: trigger === 'click',
toggle: true,
});
const role = useRole(context, { role: 'menu' });
const dismiss = useDismiss(context, { bubbles: true });
const listNavigation = useListNavigation(context, {
listRef: elementsRef,
activeIndex,
onNavigate: setActiveIndex,
nested: isNested,
orientation: 'vertical',
});
const typeahead = useTypeahead(context, {
listRef: labelsRef,
onMatch: menuIsOpen ? setActiveIndex : undefined,
activeIndex,
});
const clientPoint = useClientPoint(context, { enabled: atCursor && !isOpen });
const interactions = useInteractions([hover, click, role, dismiss, listNavigation, typeahead, clientPoint]);
const contextValue: MenuContextProps = {
activeIndex,
setActiveIndex,
getItemProps: interactions.getItemProps,
isOpen: menuIsOpen,
setIsOpen: onOpenChange,
trigger,
floating: { x, y, strategy, refs, context, interactions },
elementsRef,
labelsRef,
isNested,
parent: prevContext,
menuNodeId: nodeId,
setIsChildOpen,
};
return <MenuContext.Provider value={contextValue}>{children}</MenuContext.Provider>;
});
export interface MenuProps extends MenuProviderProps {
label?: string;
showArrow?: boolean;
}
export const Menu = React.forwardRef<HTMLDivElement, MenuProps>((props, ref) => {
const parentId = useFloatingParentNodeId();
const isRoot = parentId === null;
if (isRoot) {
return (
<FloatingTree>
<MenuComponent {...props} ref={ref}>
{props.children ? (
props.children
) : props.label ? (
<>
<MenuTrigger label={props.label} />
<MenuContent>{/* Menu-items */}</MenuContent>
</>
) : null}
</MenuComponent>
</FloatingTree>
);
}
return (
<MenuComponent {...props} ref={ref}>
{props.children ? (
props.children
) : props.label ? (
<>
<MenuTrigger label={props.label} />
<MenuContent>{/* Menu-items */}</MenuContent>
</>
) : null}
</MenuComponent>
);
});
export type MenuTriggerProps = Omit<BaseMenuItemProps, 'role' | 'checked' | 'onCheckedChange' | 'closeOnClick'> & {
x?: number;
y?: number;
rightClick?: boolean;
};
export const MenuTrigger = React.forwardRef<HTMLElement, MenuTriggerProps>(
({ label, as, disabled, leftSlot, rightSlot, x = null, y = null, children, ...props }, ref) => {
const context = React.useContext(MenuContext);
if (!context) throw new Error('MenuTrigger must be used within a Menu');
const { ref: itemRef, isActive, index } = useMenuItem();
const isNested = context.isNested;
if (x != null && y != null) {
const rect = context.floating.refs.reference.current?.getBoundingClientRect() ?? { x: 0, y: 0 };
context.floating.x = x + rect.x;
context.floating.y = y + rect.y;
}
const mergedRef = useMergeRefs([context.floating.refs.setReference, ref, itemRef]);
if (children != null) {
const referenceProps = {
...context.floating.interactions.getReferenceProps({}),
};
if (props.rightClick) {
referenceProps.onContextMenu = (event: React.MouseEvent) => {
event.preventDefault();
context.setIsOpen(!context.isOpen);
};
delete referenceProps.onMouseDown;
delete referenceProps.onPointerDown;
referenceProps.onClick = (event: React.MouseEvent) => {
event.preventDefault();
console.log('Click event!!!', context.isOpen);
context.setIsOpen(false);
};
}
const element = React.cloneElement(children as React.ReactElement<{ ref?: React.Ref<HTMLElement> }>, {
...props,
...referenceProps,
ref: mergedRef,
});
return element;
}
return (
<BaseMenuItem
ref={mergedRef}
as={as ?? 'button'}
role="menuitem"
label={label}
leftSlot={leftSlot}
rightSlot={
rightSlot ??
(isNested ? <Icon size={12} component="icon-[ic--baseline-arrow-forward-ios]" className="text-secondary-500" /> : undefined)
}
isTrigger
disabled={disabled}
{...props}
{...context.floating.interactions.getReferenceProps({
onMouseEnter: () => {
context.setIsChildOpen(true);
},
})}
/>
);
},
);
export type MenuContentProps = React.HTMLProps<HTMLDivElement>;
export const MenuContent = React.forwardRef<HTMLDivElement, MenuContentProps>((props, ref) => {
const context = React.useContext(MenuContext);
if (!context) throw new Error('MenuContent must be used within a Menu');
const { floating } = context;
const { x, y, strategy, interactions } = floating;
const isNested = context.isNested;
return (
<FloatingList elementsRef={context.elementsRef} labelsRef={context.labelsRef}>
{context.isOpen && (
<FloatingPortal>
<FloatingFocusManager context={floating.context} modal={false} initialFocus={isNested ? -1 : 0} returnFocus={!isNested}>
<div
ref={node => {
floating.refs.setFloating(node);
if (ref) {
if (typeof ref === 'function') ref(node);
else (ref as React.RefObject<HTMLDivElement | null>).current = node;
}
}}
{...interactions.getFloatingProps(props)}
style={{ position: strategy, top: y ?? 0, left: x ?? 0 }}
className={`${styles.menu} ${props.className}`}
>
{props.children}
</div>
</FloatingFocusManager>
</FloatingPortal>
)}
</FloatingList>
);
});
interface BaseMenuItemProps extends React.HTMLAttributes<HTMLElement> {
as?: React.ElementType;
role?: 'menuitem' | 'menuitemcheckbox' | 'menuitemradio';
checked?: boolean;
onCheckedChange?: (checked: boolean) => void;
label?: string;
disabled?: boolean;
leftSlot?: React.ReactNode;
iconLeft?: string;
rightSlot?: React.ReactNode;
iconRight?: string;
closeOnClick?: boolean;
isTrigger?: boolean;
}
const BaseMenuItem = React.forwardRef<HTMLElement, BaseMenuItemProps>(
(
{
as: Component = 'button',
role = 'menuitem',
checked,
onCheckedChange,
label,
disabled,
children,
leftSlot,
rightSlot,
iconLeft,
iconRight,
closeOnClick = false,
className,
isTrigger = false,
...props
},
forwardedRef,
) => {
const context = React.useContext(MenuContext);
if (!context) throw new Error('BaseMenuItem must be used within a Menu');
const { ref, index } = useMenuItem();
const tree = useFloatingTree();
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
event.stopPropagation();
event.preventDefault();
props.onClick?.(event);
if (closeOnClick) {
tree?.events.emit('click');
context?.setIsOpen(false);
context?.parent?.setIsOpen(false);
context?.parent?.parent?.setIsOpen(false);
context?.parent?.parent?.parent?.setIsOpen(false);
context?.parent?.parent?.parent?.parent?.setIsOpen(false);
}
};
const isActive = index === context.activeIndex;
return (
<Component
ref={useMergeRefs([ref, forwardedRef])}
type={Component === 'button' ? 'button' : undefined}
role={role as 'menuitem' | 'menuitemcheckbox' | 'menuitemradio'}
tabIndex={isActive ? 0 : -1}
data-open={context?.isOpen && isTrigger ? '' : null}
disabled={Component === 'button' ? disabled : undefined}
{...props}
{...context.getItemProps({
onClick: handleClick,
onFocus: event => {
props.onFocus?.(event);
},
})}
className={`${styles.menuItem} ${className ?? ''}`}
>
{/* Left Slot */}
{iconLeft ? <Icon className="text-secondary-500" size={16} component={iconLeft} /> : leftSlot}
{/* Main Content */}
<span className={styles.mainSlot}>{label ? label : children}</span>
{/* Right Slot */}
{iconRight ? <Icon className="text-secondary-500" size={16} component={iconRight} /> : rightSlot}
</Component>
);
},
);
export interface MenuItemProps extends Omit<BaseMenuItemProps, 'role' | 'onCheckedChange' | 'checked'> {
closeOnClick?: boolean;
}
export const MenuItem = React.forwardRef<HTMLElement, MenuItemProps>(({ closeOnClick = true, ...props }, ref) => (
<BaseMenuItem {...props} ref={ref} closeOnClick={closeOnClick} />
));
export interface MenuCheckboxItemProps extends Omit<BaseMenuItemProps, 'role' | 'onCheckedChange' | 'checked'> {
checked: boolean;
onCheckedChange: (checked: boolean) => void;
closeOnClick?: boolean;
}
export const MenuCheckboxItem = React.forwardRef<HTMLButtonElement, MenuCheckboxItemProps>(
({ checked, onCheckedChange, leftSlot, closeOnClick = false, ...props }, ref) => (
<BaseMenuItem
{...props}
ref={ref}
role="menuitemcheckbox"
leftSlot={
leftSlot ?? (
<Icon
size={16}
className={checked ? 'text-primary' : 'opacity-50'}
component={checked ? 'icon-[ic--baseline-check-box]' : 'icon-[ic--baseline-check-box-outline-blank]'}
/>
)
}
onClick={event => {
event.stopPropagation();
onCheckedChange(!checked);
props.onClick?.(event);
}}
closeOnClick={closeOnClick}
/>
),
);
export interface MenuRadioGroupProps {
value: string;
onValueChange: (value: string) => void;
children: React.ReactNode;
}
const MenuRadioGroupContext = React.createContext<{
value: string;
onValueChange: (value: string) => void;
} | null>(null);
export const MenuRadioGroup: React.FC<MenuRadioGroupProps> = ({ value, onValueChange, children }) => {
return <MenuRadioGroupContext.Provider value={{ value, onValueChange }}>{children}</MenuRadioGroupContext.Provider>;
};
export interface MenuRadioItemProps extends Omit<BaseMenuItemProps, 'role' | 'onCheckedChange' | 'checked'> {
value: string;
}
export const MenuRadioItem = React.forwardRef<HTMLButtonElement, MenuRadioItemProps>(
({ value, leftSlot, closeOnClick = false, ...props }, ref) => {
const radioContext = React.useContext(MenuRadioGroupContext);
const selected = radioContext?.value === value;
return (
<BaseMenuItem
{...props}
ref={ref}
role="menuitemradio"
leftSlot={
leftSlot ?? (
<Icon
size={16}
className={selected ? 'text-primary' : 'opacity-50'}
component={selected ? 'icon-[ic--baseline-radio-button-checked]' : 'icon-[ic--baseline-radio-button-unchecked]'}
/>
)
}
onClick={event => {
event.stopPropagation();
radioContext?.onValueChange(value);
props.onClick?.(event);
}}
closeOnClick={closeOnClick}
/>
);
},
);
export function useMenuItem() {
const context = React.useContext(MenuContext);
if (!context) throw new Error('useMenuItem must be used within a Menu');
const item = useListItem();
return {
...item,
isActive: () => item.index === context.activeIndex,
};
}
export const MenuSeparator = () => <div className={styles.menuSeparator} />;
export interface MenuLabelProps extends React.HTMLAttributes<HTMLDivElement> {
label: string;
}
export const MenuLabel: React.FC<MenuLabelProps> = ({ label, className, ...props }) => {
return (
<div className={`${styles.menuLabel} ${className || ''}`} {...props}>
{label}
</div>
);
};
.menu {
@apply p-1 min-w-20 flex flex-col rounded outline-0 shadow-lg bg-light;
}
.menuSeparator {
@apply border-t border-secondary-200 my-1 -mx-1;
}
.menuItem {
@apply flex flex-row items-center gap-0.5 text-sm px-1 rounded min-h-6 outline-0;
}
.menuItem:disabled {
opacity: 0.5;
pointer-events: none;
}
.menuItem:focus {
@apply bg-secondary-100;
}
.menuItem[data-open] {
@apply bg-secondary-300;
}
.menuLabel {
@apply font-semibold uppercase text-2xs pointer-events-none text-secondary-600 px-1 py-1;
}
.leftSlot {
@apply flex shrink-0 min-w-5 min-h-5;
}
.mainSlot {
@apply grow text-left px-0.5;
}
.rightSlot {
@apply min-h-5 shrink-0;
}
.menu::-webkit-scrollbar {
background-color: #fff;
width: 16px;
}
.menu::-webkit-scrollbar-track {
background-color: #fff;
}
.menu::-webkit-scrollbar-thumb {
background-color: #babac0;
border-radius: 16px;
border: 5px solid #fff;
}
/* set button(top and bottom of the scrollbar) */
.scrollbar::-webkit-scrollbar-button {
display: none;
}
......@@ -243,7 +243,6 @@ export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAu
console.log('State unchanged, not updating');
return;
}
console.log('State fetched', data, status);
dispatch(addSaveState({ ss: data, select: true }));
}
});
......@@ -306,7 +305,6 @@ export const EventBus = (props: { onRunQuery: (useCached: boolean) => void; onAu
console.log('State unchanged, not updating');
return;
}
console.log('State fetched', data, status);
dispatch(addSaveState({ ss: data, select: false }));
}
}),
......
import { Button, ControlContainer, Input, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/lib/components';
import { Menu, MenuContent, MenuItem, MenuSeparator, MenuTrigger } from '@/lib/components/menu';
import { Popover, PopoverContent, PopoverTrigger } from '@/lib/components/popover';
import { Tab, Tabs } from '@/lib/components/tabs';
import { addError } from '@/lib/data-access/store/configSlice';
......@@ -162,76 +163,63 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => {
return a.order < b.order ? -1 : 1;
})
.map((query, i) => (
<Tab
text=""
activeTab={query.id === activeQuery?.id}
key={i}
data-id={query.id}
onClick={() => {
if (query.id == null) return;
dispatch(setActiveQueryID(query.id));
}}
>
<>
{editingIdx?.idx === i ? (
<Input
type="text"
size="xs"
value={editingIdx.text}
label=""
onChange={e => {
setEditingIdx({ idx: i, text: e });
}}
onBlur={() => {
updateQueryName(editingIdx.text);
}}
onKeyDown={e => {
if (e.key === 'Enter') {
updateQueryName(editingIdx.text);
}
}}
className="w-20"
style={{
border: 'none',
boxShadow: 'none',
background: 'none',
}}
autoFocus
/>
) : (
<div
onDoubleClick={e => {
e.stopPropagation();
dispatch(setActiveQueryID(query.id || -1));
setEditingIdx({ idx: i, text: query.name ?? '' });
}}
>
{query.name ?? 'Query'}
</div>
)}
{ss.queryStates.openQueryArray.filter(query => query.id != null).length > 1 && (
<Popover placement="bottom">
<PopoverTrigger>
<Menu atCursor>
<MenuTrigger className="-mb-px" rightClick>
<Tab
text=""
activeTab={query.id === activeQuery?.id}
key={i}
data-id={query.id}
onClick={() => {
if (query.id == null) return;
dispatch(setActiveQueryID(query.id));
}}
>
<>
{editingIdx?.idx === i ? (
<Input
type="text"
size="xs"
value={editingIdx.text}
label=""
onChange={e => {
setEditingIdx({ idx: i, text: e });
}}
onBlur={() => {
updateQueryName(editingIdx.text);
}}
onKeyDown={e => {
if (e.key === 'Enter') {
updateQueryName(editingIdx.text);
}
}}
className="w-20"
style={{
border: 'none',
boxShadow: 'none',
background: 'none',
}}
autoFocus
/>
) : (
<div
onDoubleClick={e => {
e.stopPropagation();
dispatch(setActiveQueryID(query.id || -1));
setEditingIdx({ idx: i, text: query.name ?? '' });
}}
>
{query.name ?? 'Query'}
</div>
)}
{ss.queryStates.openQueryArray.filter(query => query.id != null).length > 1 && (
<Button
variantType="secondary"
variant="ghost"
disabled={!ss?.authorization.database?.W}
className={
query.id == ss.queryStates.activeQueryId
? ''
: 'opacity-50 group-hover:opacity-100 group-focus-within:opacity-100'
}
disabled={!ss.authorization.database?.W}
rounded
size="3xs"
iconComponent="icon-[ic--baseline-close]"
/>
</PopoverTrigger>
<PopoverContent className="p-2 text-sm">
<p className="pb-1">Are you sure?</p>
<Button
variantType="danger"
variant="solid"
size="xs"
onClick={e => {
e.stopPropagation();
if (query.id !== undefined) {
......@@ -239,16 +227,49 @@ export const QueryBuilderNav = (props: QueryBuilderNavProps) => {
dispatch(removeQueryByID(query.id));
}
}}
>
Yes, remove
</Button>
</PopoverContent>
</Popover>
)}
</>
</Tab>
/>
)}
</>
</Tab>
</MenuTrigger>
<MenuContent>
{/* <Menu>
<MenuTrigger label="Change mode" />
<MenuContent>
<MenuRadioGroup value={'visual'} onValueChange={() => {}}>
<MenuRadioItem value="visual" label="Visual" />
<MenuRadioItem value="cypher" label="Cypher" />
</MenuRadioGroup>
</MenuContent>
</Menu> */}
<MenuItem
label="Rename"
closeOnClick={true}
onClick={e => {
e.stopPropagation();
setTimeout(() => {
dispatch(setActiveQueryID(query.id || -1));
setEditingIdx({ idx: i, text: query.name ?? '' });
}, 1);
}}
/>
<MenuSeparator />
<MenuItem
label="Remove"
className="text-danger"
onClick={() => {
if (query.id !== undefined) {
wsDeleteQuery({ saveStateID: ss.id, queryID: query.id });
dispatch(removeQueryByID(query.id));
}
}}
/>
</MenuContent>
</Menu>
))}
</Tabs>
<div className="sticky right-0 px-0.5 ml-auto items-center flex truncate">
<ControlContainer>
<TooltipProvider>
......