Feat/hide board fields (#1271)

* Renamed AuthAutoRouter

* Moved RecoilScope

* Refactored old WithTopBarContainer to make it less transclusive

* Created new add opportunity button and refactored DropdownButton

* Added tests

* Deactivated new eslint rule

* Refactored Table options with new dropdown

* Started BoardDropdown

* Fix lint

* Refactor dropdown openstate

* Fix according to PR

* Fix tests

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Lucas Bordeau
2023-08-24 13:19:42 +02:00
committed by GitHub
parent 64cef963bc
commit 252f1c655e
48 changed files with 860 additions and 580 deletions

View File

@ -3,17 +3,16 @@ import { useTheme } from '@emotion/react';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { currentPipelineState } from '@/pipeline/states/currentPipelineState'; import { currentPipelineState } from '@/pipeline/states/currentPipelineState';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader'; import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput'; import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { IconChevronDown } from '@/ui/icon'; import { IconChevronDown } from '@/ui/icon';
import { SingleEntitySelectBase } from '@/ui/input/relation-picker/components/SingleEntitySelectBase'; import { SingleEntitySelectBase } from '@/ui/input/relation-picker/components/SingleEntitySelectBase';
import { useEntitySelectSearch } from '@/ui/input/relation-picker/hooks/useEntitySelectSearch'; import { useEntitySelectSearch } from '@/ui/input/relation-picker/hooks/useEntitySelectSearch';
import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect'; import { EntityForSelect } from '@/ui/input/relation-picker/types/EntityForSelect';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useFilteredSearchCompanyQuery } from '../hooks/useFilteredSearchCompanyQuery'; import { useFilteredSearchCompanyQuery } from '../hooks/useFilteredSearchCompanyQuery';
@ -48,17 +47,6 @@ export function CompanyProgressPicker({
string | null string | null
>(null); >(null);
useListenClickOutside({
refs: [containerRef],
callback: (event) => {
event.stopImmediatePropagation();
event.stopPropagation();
event.preventDefault();
onCancel?.();
},
});
const theme = useTheme(); const theme = useTheme();
const [currentPipeline] = useRecoilState(currentPipelineState); const [currentPipeline] = useRecoilState(currentPipelineState);
@ -94,12 +82,12 @@ export function CompanyProgressPicker({
); );
return ( return (
<DropdownMenu <StyledDropdownMenu
ref={containerRef} ref={containerRef}
data-testid={`company-progress-dropdown-menu`} data-testid={`company-progress-dropdown-menu`}
> >
{isProgressSelectionUnfolded ? ( {isProgressSelectionUnfolded ? (
<DropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
{currentPipelineStages.map((pipelineStage, index) => ( {currentPipelineStages.map((pipelineStage, index) => (
<DropdownMenuItem <DropdownMenuItem
key={pipelineStage.id} key={pipelineStage.id}
@ -111,7 +99,7 @@ export function CompanyProgressPicker({
{pipelineStage.name} {pipelineStage.name}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
) : ( ) : (
<> <>
<DropdownMenuHeader <DropdownMenuHeader
@ -121,13 +109,13 @@ export function CompanyProgressPicker({
> >
{selectedPipelineStage?.name} {selectedPipelineStage?.name}
</DropdownMenuHeader> </DropdownMenuHeader>
<DropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<DropdownMenuInput <DropdownMenuInput
value={searchFilter} value={searchFilter}
onChange={handleSearchFilterChange} onChange={handleSearchFilterChange}
autoFocus autoFocus
/> />
<DropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<RecoilScope> <RecoilScope>
<SingleEntitySelectBase <SingleEntitySelectBase
onEntitySelected={handleEntitySelected} onEntitySelected={handleEntitySelected}
@ -141,6 +129,6 @@ export function CompanyProgressPicker({
</RecoilScope> </RecoilScope>
</> </>
)} )}
</DropdownMenu> </StyledDropdownMenu>
); );
} }

View File

@ -7,9 +7,9 @@ import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react';
import { IconDotsVertical, IconLinkOff, IconTrash } from '@tabler/icons-react'; import { IconDotsVertical, IconLinkOff, IconTrash } from '@tabler/icons-react';
import { IconButton } from '@/ui/button/components/IconButton'; import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem'; import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { Avatar } from '@/users/components/Avatar'; import { Avatar } from '@/users/components/Avatar';
import { import {
@ -172,8 +172,10 @@ export function PeopleCard({
icon={<IconDotsVertical size={theme.icon.size.md} />} icon={<IconDotsVertical size={theme.icon.size.md} />}
/> />
{isOptionsOpen && ( {isOptionsOpen && (
<DropdownMenu ref={refs.setFloating} style={floatingStyles}> <StyledDropdownMenu ref={refs.setFloating} style={floatingStyles}>
<DropdownMenuItemsContainer onClick={(e) => e.stopPropagation()}> <StyledDropdownMenuItemsContainer
onClick={(e) => e.stopPropagation()}
>
<DropdownMenuSelectableItem onClick={handleDetachPerson}> <DropdownMenuSelectableItem onClick={handleDetachPerson}>
<IconButton icon={<IconLinkOff size={14} />} size="small" /> <IconButton icon={<IconLinkOff size={14} />} size="small" />
Detach relation Detach relation
@ -186,8 +188,8 @@ export function PeopleCard({
/> />
<StyledRemoveOption>Delete person</StyledRemoveOption> <StyledRemoveOption>Delete person</StyledRemoveOption>
</DropdownMenuSelectableItem> </DropdownMenuSelectableItem>
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</DropdownMenu> </StyledDropdownMenu>
)} )}
</div> </div>
)} )}

View File

@ -12,7 +12,9 @@ import { useSnackBar } from '@/ui/snack-bar/hooks/useSnackBar';
export function PipelineAddButton() { export function PipelineAddButton() {
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
const { closeDropdownButton } = useDropdownButton(); const { closeDropdownButton, toggleDropdownButton } = useDropdownButton({
key: 'add-company-progress',
});
const createCompanyProgress = useCreateCompanyProgress(); const createCompanyProgress = useCreateCompanyProgress();
@ -51,6 +53,7 @@ export function PipelineAddButton() {
return ( return (
<DropdownButton <DropdownButton
dropdownKey="add-pipeline-progress"
buttonComponents={ buttonComponents={
<IconButton <IconButton
icon={<IconPlus size={16} />} icon={<IconPlus size={16} />}
@ -58,6 +61,7 @@ export function PipelineAddButton() {
data-testid="add-company-progress-button" data-testid="add-company-progress-button"
textColor={'secondary'} textColor={'secondary'}
variant="border" variant="border"
onClick={toggleDropdownButton}
/> />
} }
dropdownComponents={ dropdownComponents={
@ -71,7 +75,7 @@ export function PipelineAddButton() {
key: 'c', key: 'c',
scope: PageHotkeyScope.OpportunitiesPage, scope: PageHotkeyScope.OpportunitiesPage,
}} }}
dropdownScopeToSet={{ dropdownHotkeyScope={{
scope: RelationPickerHotkeyScope.RelationPicker, scope: RelationPickerHotkeyScope.RelationPicker,
}} }}
/> />

View File

@ -14,12 +14,12 @@ import debounce from 'lodash.debounce';
import { ReadonlyDeep } from 'type-fest'; import { ReadonlyDeep } from 'type-fest';
import type { SelectOption } from '@/spreadsheet-import/types'; import type { SelectOption } from '@/spreadsheet-import/types';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput'; import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem'; import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator'; import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { IconChevronDown } from '@/ui/icon'; import { IconChevronDown } from '@/ui/icon';
import { AppTooltip } from '@/ui/tooltip/AppTooltip'; import { AppTooltip } from '@/ui/tooltip/AppTooltip';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
@ -166,7 +166,7 @@ export const MatchColumnSelect = ({
{isOpen && {isOpen &&
createPortal( createPortal(
<StyledFloatingDropdown ref={refs.setFloating} style={floatingStyles}> <StyledFloatingDropdown ref={refs.setFloating} style={floatingStyles}>
<DropdownMenu <StyledDropdownMenu
ref={dropdownContainerRef} ref={dropdownContainerRef}
width={dropdownItemRef.current?.clientWidth} width={dropdownItemRef.current?.clientWidth}
> >
@ -175,8 +175,8 @@ export const MatchColumnSelect = ({
onChange={handleFilterChange} onChange={handleFilterChange}
autoFocus autoFocus
/> />
<DropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight> <StyledDropdownMenuItemsContainer hasMaxHeight>
{options?.map((option) => ( {options?.map((option) => (
<> <>
<DropdownMenuSelectableItem <DropdownMenuSelectableItem
@ -208,8 +208,8 @@ export const MatchColumnSelect = ({
{options?.length === 0 && ( {options?.length === 0 && (
<DropdownMenuItem>No result</DropdownMenuItem> <DropdownMenuItem>No result</DropdownMenuItem>
)} )}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</DropdownMenu> </StyledDropdownMenu>
</StyledFloatingDropdown>, </StyledFloatingDropdown>,
document.body, document.body,
)} )}

View File

@ -1,9 +1,9 @@
import { ChangeEvent, useState } from 'react'; import { ChangeEvent, useState } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem'; import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { textInputStyle } from '@/ui/theme/constants/effects'; import { textInputStyle } from '@/ui/theme/constants/effects';
import { debounce } from '~/utils/debounce'; import { debounce } from '~/utils/debounce';
@ -75,7 +75,7 @@ export function BoardColumnEditTitleMenu({
debouncedOnUpdateTitle(event.target.value); debouncedOnUpdateTitle(event.target.value);
}; };
return ( return (
<DropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
<StyledEditTitleContainer> <StyledEditTitleContainer>
<StyledEditModeInput <StyledEditModeInput
value={internalValue} value={internalValue}
@ -84,7 +84,7 @@ export function BoardColumnEditTitleMenu({
autoFocus autoFocus
/> />
</StyledEditTitleContainer> </StyledEditTitleContainer>
<DropdownMenuSeparator /> <StyledDropdownMenuSeparator />
{COLOR_OPTIONS.map((colorOption) => ( {COLOR_OPTIONS.map((colorOption) => (
<DropdownMenuSelectableItem <DropdownMenuSelectableItem
key={colorOption.name} key={colorOption.name}
@ -98,6 +98,6 @@ export function BoardColumnEditTitleMenu({
{colorOption.name} {colorOption.name}
</DropdownMenuSelectableItem> </DropdownMenuSelectableItem>
))} ))}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
); );
} }

View File

@ -3,9 +3,9 @@ import styled from '@emotion/styled';
import { IconPencil } from '@tabler/icons-react'; import { IconPencil } from '@tabler/icons-react';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem'; import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { icon } from '@/ui/theme/constants/icon'; import { icon } from '@/ui/theme/constants/icon';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
@ -50,14 +50,14 @@ export function BoardColumnMenu({
return ( return (
<StyledMenuContainer ref={boardColumnMenuRef}> <StyledMenuContainer ref={boardColumnMenuRef}>
<DropdownMenu> <StyledDropdownMenu>
{openMenu === 'actions' && ( {openMenu === 'actions' && (
<DropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
<DropdownMenuSelectableItem onClick={() => setOpenMenu('title')}> <DropdownMenuSelectableItem onClick={() => setOpenMenu('title')}>
<IconPencil size={icon.size.md} stroke={icon.stroke.sm} /> <IconPencil size={icon.size.md} stroke={icon.stroke.sm} />
Rename Rename
</DropdownMenuSelectableItem> </DropdownMenuSelectableItem>
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
)} )}
{openMenu === 'title' && ( {openMenu === 'title' && (
<BoardColumnEditTitleMenu <BoardColumnEditTitleMenu
@ -67,7 +67,7 @@ export function BoardColumnMenu({
title={title} title={title}
/> />
)} )}
</DropdownMenu> </StyledDropdownMenu>
</StyledMenuContainer> </StyledMenuContainer>
); );
} }

View File

@ -0,0 +1,14 @@
import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
import { BoardOptionsDropdownButton } from './BoardOptionsDropdownButton';
import { BoardOptionsDropdownContent } from './BoardOptionsDropdownContent';
export function BoardOptionsDropdown() {
return (
<DropdownButton
dropdownKey="options"
buttonComponents={<BoardOptionsDropdownButton />}
dropdownComponents={<BoardOptionsDropdownContent />}
></DropdownButton>
);
}

View File

@ -0,0 +1,21 @@
import { StyledHeaderDropdownButton } from '@/ui/dropdown/components/StyledHeaderDropdownButton';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
export function BoardOptionsDropdownButton() {
const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton({
key: 'options',
});
function handleClick() {
toggleDropdownButton();
}
return (
<StyledHeaderDropdownButton
isUnfolded={isDropdownButtonOpen}
onClick={handleClick}
>
Options
</StyledHeaderDropdownButton>
);
}

View File

@ -0,0 +1,55 @@
import { useState } from 'react';
import { useTheme } from '@emotion/react';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { IconChevronLeft } from '@/ui/icon';
type BoardOptionsDropdownMenu = 'options' | 'fields';
export function BoardOptionsDropdownContent() {
const theme = useTheme();
const [menuShown, setMenuShown] =
useState<BoardOptionsDropdownMenu>('options');
function handleFieldsClick() {
setMenuShown('fields');
}
function handleMenuHeaderClick() {
setMenuShown('options');
}
return (
<StyledDropdownMenu>
{menuShown === 'options' ? (
<>
<DropdownMenuHeader>Options</DropdownMenuHeader>
<StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer>
<DropdownMenuItem onClick={handleFieldsClick}>
Fields
</DropdownMenuItem>
</StyledDropdownMenuItemsContainer>
</>
) : (
menuShown === 'fields' && (
<>
<DropdownMenuHeader
startIcon={<IconChevronLeft size={theme.icon.size.md} />}
onClick={handleMenuHeaderClick}
>
Fields
</DropdownMenuHeader>
<StyledDropdownMenuSeparator />
{}
</>
)
)}
</StyledDropdownMenu>
);
}

View File

@ -4,8 +4,8 @@ import { useRecoilValue, useSetRecoilState } from 'recoil';
import { actionBarOpenState } from '@/ui/action-bar/states/actionBarIsOpenState'; import { actionBarOpenState } from '@/ui/action-bar/states/actionBarIsOpenState';
import { contextMenuPositionState } from '@/ui/context-menu/states/contextMenuPositionState'; import { contextMenuPositionState } from '@/ui/context-menu/states/contextMenuPositionState';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu'; import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { contextMenuEntriesState } from '../states/contextMenuEntriesState'; import { contextMenuEntriesState } from '../states/contextMenuEntriesState';
@ -60,11 +60,11 @@ export function ContextMenu({ selectedIds }: OwnProps) {
} }
return ( return (
<StyledContainerContextMenu ref={wrapperRef} position={position}> <StyledContainerContextMenu ref={wrapperRef} position={position}>
<DropdownMenu> <StyledDropdownMenu>
<DropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
{contextMenuEntries} {contextMenuEntries}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</DropdownMenu> </StyledDropdownMenu>
</StyledContainerContextMenu> </StyledContainerContextMenu>
); );
} }

View File

@ -1,10 +1,16 @@
import { useEffect, useRef } from 'react';
import { Keys } from 'react-hotkeys-hook'; import { Keys } from 'react-hotkeys-hook';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { flip, offset, useFloating } from '@floating-ui/react'; import { flip, offset, Placement, useFloating } from '@floating-ui/react';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useRecoilScopedFamilyState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedFamilyState';
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
import { useDropdownButton } from '../hooks/useDropdownButton'; import { useDropdownButton } from '../hooks/useDropdownButton';
import { dropdownButtonCustomHotkeyScopeScopedFamilyState } from '../states/dropdownButtonCustomHotkeyScopeScopedFamilyState';
import { DropdownRecoilScopeContext } from '../states/recoil-scope-contexts/DropdownRecoilScopeContext';
import { HotkeyEffect } from './HotkeyEffect'; import { HotkeyEffect } from './HotkeyEffect';
@ -16,38 +22,74 @@ const StyledContainer = styled.div`
type OwnProps = { type OwnProps = {
buttonComponents: JSX.Element | JSX.Element[]; buttonComponents: JSX.Element | JSX.Element[];
dropdownComponents: JSX.Element | JSX.Element[]; dropdownComponents: JSX.Element | JSX.Element[];
dropdownKey: string;
hotkey?: { hotkey?: {
key: Keys; key: Keys;
scope: string; scope: string;
}; };
dropdownScopeToSet?: HotkeyScope; dropdownHotkeyScope?: HotkeyScope;
dropdownPlacement?: Placement;
}; };
export function DropdownButton({ export function DropdownButton({
buttonComponents, buttonComponents,
dropdownComponents, dropdownComponents,
dropdownKey,
hotkey, hotkey,
dropdownScopeToSet, dropdownHotkeyScope,
dropdownPlacement = 'bottom-end',
}: OwnProps) { }: OwnProps) {
const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton(); const containerRef = useRef<HTMLDivElement>(null);
const { isDropdownButtonOpen, toggleDropdownButton, closeDropdownButton } =
useDropdownButton({
key: dropdownKey,
});
const { refs, floatingStyles } = useFloating({ const { refs, floatingStyles } = useFloating({
placement: 'bottom-end', placement: dropdownPlacement,
middleware: [flip(), offset()], middleware: [flip(), offset()],
}); });
function handleButtonClick() { function handleHotkeyTriggered() {
toggleDropdownButton(dropdownScopeToSet); toggleDropdownButton();
} }
useListenClickOutside({
refs: [containerRef],
callback: () => {
if (isDropdownButtonOpen) {
closeDropdownButton();
}
},
});
const [dropdownButtonCustomHotkeyScope, setDropdownButtonCustomHotkeyScope] =
useRecoilScopedFamilyState(
dropdownButtonCustomHotkeyScopeScopedFamilyState,
dropdownKey,
DropdownRecoilScopeContext,
);
useEffect(() => {
if (!isDeeplyEqual(dropdownButtonCustomHotkeyScope, dropdownHotkeyScope)) {
setDropdownButtonCustomHotkeyScope(dropdownHotkeyScope);
}
}, [
setDropdownButtonCustomHotkeyScope,
dropdownHotkeyScope,
dropdownButtonCustomHotkeyScope,
]);
return ( return (
<StyledContainer> <StyledContainer ref={containerRef}>
{hotkey && ( {hotkey && (
<HotkeyEffect hotkey={hotkey} onHotkeyTriggered={handleButtonClick} /> <HotkeyEffect
hotkey={hotkey}
onHotkeyTriggered={handleHotkeyTriggered}
/>
)} )}
<div ref={refs.setReference} onClick={handleButtonClick}> <div ref={refs.setReference}>{buttonComponents}</div>
{buttonComponents}
</div>
{isDropdownButtonOpen && ( {isDropdownButtonOpen && (
<div ref={refs.setFloating} style={floatingStyles}> <div ref={refs.setFloating} style={floatingStyles}>
{dropdownComponents} {dropdownComponents}

View File

@ -44,15 +44,19 @@ type DropdownMenuHeaderProps = ComponentProps<'li'> & {
endIcon?: ReactElement; endIcon?: ReactElement;
}; };
export const DropdownMenuHeader = ({ export function DropdownMenuHeader({
children, children,
startIcon, startIcon,
endIcon, endIcon,
...props ...props
}: DropdownMenuHeaderProps) => ( }: DropdownMenuHeaderProps) {
<StyledHeader {...props}> return (
{startIcon && <StyledStartIconWrapper>{startIcon}</StyledStartIconWrapper>} <StyledHeader {...props}>
{children} {startIcon && (
{endIcon && <StyledEndIconWrapper>{endIcon}</StyledEndIconWrapper>} <StyledStartIconWrapper>{startIcon}</StyledStartIconWrapper>
</StyledHeader> )}
); {children}
{endIcon && <StyledEndIconWrapper>{endIcon}</StyledEndIconWrapper>}
</StyledHeader>
);
}

View File

@ -58,22 +58,24 @@ export type DropdownMenuItemProps = ComponentProps<'li'> & {
accent?: DropdownMenuItemAccent; accent?: DropdownMenuItemAccent;
}; };
export const DropdownMenuItem = ({ export function DropdownMenuItem({
actions, actions,
children, children,
accent = 'regular', accent = 'regular',
...props ...props
}: DropdownMenuItemProps) => ( }: DropdownMenuItemProps) {
<StyledItem {...props} accent={accent}> return (
{children} <StyledItem {...props} accent={accent}>
{actions && ( {children}
<StyledActions {actions && (
className={styledIconButtonGroupClassName} <StyledActions
variant="transparent" className={styledIconButtonGroupClassName}
size="small" variant="transparent"
> size="small"
{actions} >
</StyledActions> {actions}
)} </StyledActions>
</StyledItem> )}
); </StyledItem>
);
}

View File

@ -1,7 +1,6 @@
/* eslint-disable twenty/styled-components-prefixed-with-styled */
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const DropdownMenu = styled.div<{ export const StyledDropdownMenu = styled.div<{
disableBlur?: boolean; disableBlur?: boolean;
width?: number; width?: number;
}>` }>`

View File

@ -1,7 +1,6 @@
/* eslint-disable twenty/styled-components-prefixed-with-styled */
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const DropdownMenuItemsContainer = styled.div<{ export const StyledDropdownMenuItemsContainer = styled.div<{
hasMaxHeight?: boolean; hasMaxHeight?: boolean;
}>` }>`
--padding: ${({ theme }) => theme.spacing(1)}; --padding: ${({ theme }) => theme.spacing(1)};

View File

@ -1,7 +1,6 @@
/* eslint-disable twenty/styled-components-prefixed-with-styled */
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const DropdownMenuSeparator = styled.div` export const StyledDropdownMenuSeparator = styled.div`
background-color: ${({ theme }) => theme.border.color.light}; background-color: ${({ theme }) => theme.border.color.light};
height: 1px; height: 1px;

View File

@ -1,7 +1,6 @@
/* eslint-disable twenty/styled-components-prefixed-with-styled */
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const DropdownMenuSubheader = styled.div` export const StyledDropdownMenuSubheader = styled.div`
background-color: ${({ theme }) => theme.background.transparent.lighter}; background-color: ${({ theme }) => theme.background.transparent.lighter};
color: ${({ theme }) => theme.font.color.light}; color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.xxs}; font-size: ${({ theme }) => theme.font.size.xxs};

View File

@ -0,0 +1,27 @@
import styled from '@emotion/styled';
type StyledDropdownButtonProps = {
isUnfolded?: boolean;
isActive?: boolean;
};
export const StyledHeaderDropdownButton = styled.div<StyledDropdownButtonProps>`
align-items: center;
background: ${({ theme }) => theme.background.primary};
border-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ isActive, theme, color }) =>
color ?? (isActive ? theme.color.blue : 'none')};
cursor: pointer;
display: flex;
filter: ${(props) => (props.isUnfolded ? 'brightness(0.95)' : 'none')};
padding: ${({ theme }) => theme.spacing(1)};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
user-select: none;
&:hover {
filter: brightness(0.95);
}
`;

View File

@ -8,19 +8,19 @@ import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/
import { Avatar } from '@/users/components/Avatar'; import { Avatar } from '@/users/components/Avatar';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { DropdownMenu } from '../DropdownMenu';
import { DropdownMenuCheckableItem } from '../DropdownMenuCheckableItem'; import { DropdownMenuCheckableItem } from '../DropdownMenuCheckableItem';
import { DropdownMenuHeader } from '../DropdownMenuHeader'; import { DropdownMenuHeader } from '../DropdownMenuHeader';
import { DropdownMenuInput } from '../DropdownMenuInput'; import { DropdownMenuInput } from '../DropdownMenuInput';
import { DropdownMenuItem } from '../DropdownMenuItem'; import { DropdownMenuItem } from '../DropdownMenuItem';
import { DropdownMenuItemsContainer } from '../DropdownMenuItemsContainer';
import { DropdownMenuSelectableItem } from '../DropdownMenuSelectableItem'; import { DropdownMenuSelectableItem } from '../DropdownMenuSelectableItem';
import { DropdownMenuSeparator } from '../DropdownMenuSeparator'; import { StyledDropdownMenu } from '../StyledDropdownMenu';
import { DropdownMenuSubheader } from '../DropdownMenuSubheader'; import { StyledDropdownMenuItemsContainer } from '../StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '../StyledDropdownMenuSeparator';
import { StyledDropdownMenuSubheader } from '../StyledDropdownMenuSubheader';
const meta: Meta<typeof DropdownMenu> = { const meta: Meta<typeof StyledDropdownMenu> = {
title: 'UI/Dropdown/DropdownMenu', title: 'UI/Dropdown/DropdownMenu',
component: DropdownMenu, component: StyledDropdownMenu,
decorators: [ComponentDecorator], decorators: [ComponentDecorator],
argTypes: { argTypes: {
as: { table: { disable: true } }, as: { table: { disable: true } },
@ -29,7 +29,7 @@ const meta: Meta<typeof DropdownMenu> = {
}; };
export default meta; export default meta;
type Story = StoryObj<typeof DropdownMenu>; type Story = StoryObj<typeof StyledDropdownMenu>;
const FakeContentBelow = () => ( const FakeContentBelow = () => (
<div style={{ position: 'absolute' }}> <div style={{ position: 'absolute' }}>
@ -156,9 +156,9 @@ const FakeCheckableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
export const Empty: Story = { export const Empty: Story = {
render: (args) => ( render: (args) => (
<DropdownMenu {...args}> <StyledDropdownMenu {...args}>
<StyledFakeMenuContent /> <StyledFakeMenuContent />
</DropdownMenu> </StyledDropdownMenu>
), ),
}; };
@ -179,60 +179,60 @@ export const WithContentBelow: Story = {
export const SimpleMenuItem: Story = { export const SimpleMenuItem: Story = {
...WithContentBelow, ...WithContentBelow,
render: (args) => ( render: (args) => (
<DropdownMenu {...args}> <StyledDropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight> <StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }) => ( {mockSelectArray.map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem> <DropdownMenuItem>{name}</DropdownMenuItem>
))} ))}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</DropdownMenu> </StyledDropdownMenu>
), ),
}; };
export const WithHeaders: Story = { export const WithHeaders: Story = {
...WithContentBelow, ...WithContentBelow,
render: (args) => ( render: (args) => (
<DropdownMenu {...args}> <StyledDropdownMenu {...args}>
<DropdownMenuHeader>Header</DropdownMenuHeader> <DropdownMenuHeader>Header</DropdownMenuHeader>
<DropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<DropdownMenuSubheader>Subheader 1</DropdownMenuSubheader> <StyledDropdownMenuSubheader>Subheader 1</StyledDropdownMenuSubheader>
<DropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
{mockSelectArray.slice(0, 3).map(({ name }) => ( {mockSelectArray.slice(0, 3).map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem> <DropdownMenuItem>{name}</DropdownMenuItem>
))} ))}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
<DropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<DropdownMenuSubheader>Subheader 2</DropdownMenuSubheader> <StyledDropdownMenuSubheader>Subheader 2</StyledDropdownMenuSubheader>
<DropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
{mockSelectArray.slice(3).map(({ name }) => ( {mockSelectArray.slice(3).map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem> <DropdownMenuItem>{name}</DropdownMenuItem>
))} ))}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</DropdownMenu> </StyledDropdownMenu>
), ),
}; };
export const WithIcons: Story = { export const WithIcons: Story = {
...WithContentBelow, ...WithContentBelow,
render: (args) => ( render: (args) => (
<DropdownMenu {...args}> <StyledDropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight> <StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }) => ( {mockSelectArray.map(({ name }) => (
<DropdownMenuItem> <DropdownMenuItem>
<IconUser size={16} /> <IconUser size={16} />
{name} {name}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</DropdownMenu> </StyledDropdownMenu>
), ),
}; };
export const WithActions: Story = { export const WithActions: Story = {
...WithContentBelow, ...WithContentBelow,
render: (args) => ( render: (args) => (
<DropdownMenu {...args}> <StyledDropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight> <StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }, index) => ( {mockSelectArray.map(({ name }, index) => (
<DropdownMenuItem <DropdownMenuItem
className={index === 0 ? 'hover' : undefined} className={index === 0 ? 'hover' : undefined}
@ -244,8 +244,8 @@ export const WithActions: Story = {
{name} {name}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</DropdownMenu> </StyledDropdownMenu>
), ),
parameters: { parameters: {
pseudo: { hover: ['.hover'] }, pseudo: { hover: ['.hover'] },
@ -255,71 +255,71 @@ export const WithActions: Story = {
export const LoadingMenu: Story = { export const LoadingMenu: Story = {
...WithContentBelow, ...WithContentBelow,
render: () => ( render: () => (
<DropdownMenu> <StyledDropdownMenu>
<DropdownMenuInput value={'query'} autoFocus /> <DropdownMenuInput value={'query'} autoFocus />
<DropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight> <StyledDropdownMenuItemsContainer hasMaxHeight>
<DropdownMenuSkeletonItem /> <DropdownMenuSkeletonItem />
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</DropdownMenu> </StyledDropdownMenu>
), ),
}; };
export const Search: Story = { export const Search: Story = {
...WithContentBelow, ...WithContentBelow,
render: (args) => ( render: (args) => (
<DropdownMenu {...args}> <StyledDropdownMenu {...args}>
<DropdownMenuInput /> <DropdownMenuInput />
<DropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight> <StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }) => ( {mockSelectArray.map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem> <DropdownMenuItem>{name}</DropdownMenuItem>
))} ))}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</DropdownMenu> </StyledDropdownMenu>
), ),
}; };
export const SelectableMenuItem: Story = { export const SelectableMenuItem: Story = {
...WithContentBelow, ...WithContentBelow,
render: (args) => ( render: (args) => (
<DropdownMenu {...args}> <StyledDropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight> <StyledDropdownMenuItemsContainer hasMaxHeight>
<FakeSelectableMenuItemList /> <FakeSelectableMenuItemList />
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</DropdownMenu> </StyledDropdownMenu>
), ),
}; };
export const SelectableMenuItemWithAvatar: Story = { export const SelectableMenuItemWithAvatar: Story = {
...WithContentBelow, ...WithContentBelow,
render: (args) => ( render: (args) => (
<DropdownMenu {...args}> <StyledDropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight> <StyledDropdownMenuItemsContainer hasMaxHeight>
<FakeSelectableMenuItemList hasAvatar /> <FakeSelectableMenuItemList hasAvatar />
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</DropdownMenu> </StyledDropdownMenu>
), ),
}; };
export const CheckableMenuItem: Story = { export const CheckableMenuItem: Story = {
...WithContentBelow, ...WithContentBelow,
render: (args) => ( render: (args) => (
<DropdownMenu {...args}> <StyledDropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight> <StyledDropdownMenuItemsContainer hasMaxHeight>
<FakeCheckableMenuItemList /> <FakeCheckableMenuItemList />
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</DropdownMenu> </StyledDropdownMenu>
), ),
}; };
export const CheckableMenuItemWithAvatar: Story = { export const CheckableMenuItemWithAvatar: Story = {
...WithContentBelow, ...WithContentBelow,
render: (args) => ( render: (args) => (
<DropdownMenu {...args}> <StyledDropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight> <StyledDropdownMenuItemsContainer hasMaxHeight>
<FakeCheckableMenuItemList hasAvatar /> <FakeCheckableMenuItemList hasAvatar />
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</DropdownMenu> </StyledDropdownMenu>
), ),
}; };

View File

@ -1,17 +1,27 @@
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { useRecoilScopedFamilyState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedFamilyState';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { isDropdownButtonOpenScopedState } from '../states/isDropdownButtonOpenScopedState'; import { dropdownButtonCustomHotkeyScopeScopedFamilyState } from '../states/dropdownButtonCustomHotkeyScopeScopedFamilyState';
import { isDropdownButtonOpenScopedFamilyState } from '../states/isDropdownButtonOpenScopedFamilyState';
import { DropdownRecoilScopeContext } from '../states/recoil-scope-contexts/DropdownRecoilScopeContext';
export function useDropdownButton() { export function useDropdownButton({ key }: { key: string }) {
const { const {
setHotkeyScopeAndMemorizePreviousScope, setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope, goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope(); } = usePreviousHotkeyScope();
const [isDropdownButtonOpen, setIsDropdownButtonOpen] = useRecoilScopedState( const [isDropdownButtonOpen, setIsDropdownButtonOpen] =
isDropdownButtonOpenScopedState, useRecoilScopedFamilyState(
isDropdownButtonOpenScopedFamilyState,
key,
DropdownRecoilScopeContext,
);
const [dropdownButtonCustomHotkeyScope] = useRecoilScopedFamilyState(
dropdownButtonCustomHotkeyScopeScopedFamilyState,
key,
DropdownRecoilScopeContext,
); );
function closeDropdownButton() { function closeDropdownButton() {
@ -19,22 +29,22 @@ export function useDropdownButton() {
setIsDropdownButtonOpen(false); setIsDropdownButtonOpen(false);
} }
function openDropdownButton(hotkeyScopeToSet?: HotkeyScope) { function openDropdownButton() {
setIsDropdownButtonOpen(true); setIsDropdownButtonOpen(true);
if (hotkeyScopeToSet) { if (dropdownButtonCustomHotkeyScope) {
setHotkeyScopeAndMemorizePreviousScope( setHotkeyScopeAndMemorizePreviousScope(
hotkeyScopeToSet.scope, dropdownButtonCustomHotkeyScope.scope,
hotkeyScopeToSet.customScopes, dropdownButtonCustomHotkeyScope.customScopes,
); );
} }
} }
function toggleDropdownButton(hotkeyScopeToSet?: HotkeyScope) { function toggleDropdownButton() {
if (isDropdownButtonOpen) { if (isDropdownButtonOpen) {
closeDropdownButton(); closeDropdownButton();
} else { } else {
openDropdownButton(hotkeyScopeToSet); openDropdownButton();
} }
} }

View File

@ -0,0 +1,11 @@
import { atomFamily } from 'recoil';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
export const dropdownButtonCustomHotkeyScopeScopedFamilyState = atomFamily<
HotkeyScope | null | undefined,
string
>({
key: 'dropdownButtonCustomHotkeyScopeScopedState',
default: null,
});

View File

@ -1,6 +1,9 @@
import { atomFamily } from 'recoil'; import { atomFamily } from 'recoil';
export const isDropdownButtonOpenScopedState = atomFamily<boolean, string>({ export const isDropdownButtonOpenScopedFamilyState = atomFamily<
boolean,
string
>({
key: 'isDropdownButtonOpenScopedState', key: 'isDropdownButtonOpenScopedState',
default: false, default: false,
}); });

View File

@ -0,0 +1,3 @@
import { createContext } from 'react';
export const DropdownRecoilScopeContext = createContext<string | null>(null);

View File

@ -1,7 +1,7 @@
import { type HTMLAttributes, useRef } from 'react'; import { type HTMLAttributes, useRef } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu'; import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
const StyledDropdownMenuContainer = styled.ul<{ const StyledDropdownMenuContainer = styled.ul<{
@ -38,7 +38,7 @@ export function DropdownMenuContainer({
return ( return (
<StyledDropdownMenuContainer data-select-disable {...props} anchor={anchor}> <StyledDropdownMenuContainer data-select-disable {...props} anchor={anchor}>
<DropdownMenu ref={dropdownRef}>{children}</DropdownMenu> <StyledDropdownMenu ref={dropdownRef}>{children}</StyledDropdownMenu>
</StyledDropdownMenuContainer> </StyledDropdownMenuContainer>
); );
} }

View File

@ -1,6 +1,6 @@
import { Context } from 'react'; import { Context } from 'react';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator'; import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope'; import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
@ -22,7 +22,7 @@ export function FilterDropdownEntitySelect({
return ( return (
<> <>
<DropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<RecoilScope> <RecoilScope>
{filterDefinitionUsedInDropdown.entitySelectComponent} {filterDefinitionUsedInDropdown.entitySelectComponent}
</RecoilScope> </RecoilScope>

View File

@ -1,7 +1,7 @@
import { Context } from 'react'; import { Context } from 'react';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem'; import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope'; import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
@ -41,7 +41,7 @@ export function FilterDropdownFilterSelect({
const setHotkeyScope = useSetHotkeyScope(); const setHotkeyScope = useSetHotkeyScope();
return ( return (
<DropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
{availableFilters.map((availableFilter, index) => ( {availableFilters.map((availableFilter, index) => (
<DropdownMenuSelectableItem <DropdownMenuSelectableItem
key={`select-filter-${index}`} key={`select-filter-${index}`}
@ -63,6 +63,6 @@ export function FilterDropdownFilterSelect({
{availableFilter.label} {availableFilter.label}
</DropdownMenuSelectableItem> </DropdownMenuSelectableItem>
))} ))}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
); );
} }

View File

@ -1,7 +1,7 @@
import { Context } from 'react'; import { Context } from 'react';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useFilterCurrentlyEdited } from '../hooks/useFilterCurrentlyEdited'; import { useFilterCurrentlyEdited } from '../hooks/useFilterCurrentlyEdited';
@ -62,7 +62,7 @@ export function FilterDropdownOperandSelect({
} }
return ( return (
<DropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
{operandsForFilterType.map((filterOperand, index) => ( {operandsForFilterType.map((filterOperand, index) => (
<DropdownMenuItem <DropdownMenuItem
key={`select-filter-operand-${index}`} key={`select-filter-operand-${index}`}
@ -73,6 +73,6 @@ export function FilterDropdownOperandSelect({
{getOperandLabel(filterOperand)} {getOperandLabel(filterOperand)}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
); );
} }

View File

@ -1,6 +1,6 @@
import { Context, useCallback, useState } from 'react'; import { Context, useCallback, useState } from 'react';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator'; import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { filterDefinitionUsedInDropdownScopedState } from '@/ui/filter-n-sort/states/filterDefinitionUsedInDropdownScopedState'; import { filterDefinitionUsedInDropdownScopedState } from '@/ui/filter-n-sort/states/filterDefinitionUsedInDropdownScopedState';
import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState'; import { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState'; import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
@ -119,7 +119,7 @@ export function MultipleFiltersDropdownButton({
selectedOperandInDropdown && ( selectedOperandInDropdown && (
<> <>
<FilterDropdownOperandButton context={context} /> <FilterDropdownOperandButton context={context} />
<DropdownMenuSeparator /> <StyledDropdownMenuSeparator />
{filterDefinitionUsedInDropdown.type === 'text' && ( {filterDefinitionUsedInDropdown.type === 'text' && (
<FilterDropdownTextSearchInput context={context} /> <FilterDropdownTextSearchInput context={context} />
)} )}

View File

@ -10,6 +10,7 @@ import { selectedOperandInDropdownScopedState } from '@/ui/filter-n-sort/states/
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { StyledHeaderDropdownButton } from '../../dropdown/components/StyledHeaderDropdownButton';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState'; import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { filtersScopedState } from '../states/filtersScopedState'; import { filtersScopedState } from '../states/filtersScopedState';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope'; import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
@ -27,29 +28,6 @@ const StyledDropdownButtonContainer = styled.div`
z-index: 1; z-index: 1;
`; `;
type StyledDropdownButtonProps = {
isUnfolded: boolean;
};
const StyledDropdownButton = styled.div<StyledDropdownButtonProps>`
align-items: center;
background: ${({ theme }) => theme.background.primary};
border-radius: ${({ theme }) => theme.border.radius.sm};
cursor: pointer;
display: flex;
filter: ${(props) => (props.isUnfolded ? 'brightness(0.95)' : 'none')};
padding: ${({ theme }) => theme.spacing(1)};
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
&:hover {
filter: brightness(0.95);
}
user-select: none;
`;
export function SingleEntityFilterDropdownButton({ export function SingleEntityFilterDropdownButton({
context, context,
HotkeyScope, HotkeyScope,
@ -109,7 +87,7 @@ export function SingleEntityFilterDropdownButton({
return ( return (
<StyledDropdownButtonContainer> <StyledDropdownButtonContainer>
<StyledDropdownButton <StyledHeaderDropdownButton
isUnfolded={isUnfolded} isUnfolded={isUnfolded}
onClick={() => handleIsUnfoldedChange(!isUnfolded)} onClick={() => handleIsUnfoldedChange(!isUnfolded)}
> >
@ -119,7 +97,7 @@ export function SingleEntityFilterDropdownButton({
'Filter' 'Filter'
)} )}
<IconChevronDown size={theme.icon.size.md} /> <IconChevronDown size={theme.icon.size.md} />
</StyledDropdownButton> </StyledHeaderDropdownButton>
{isUnfolded && ( {isUnfolded && (
<DropdownMenuContainer onClose={() => handleIsUnfoldedChange(false)}> <DropdownMenuContainer onClose={() => handleIsUnfoldedChange(false)}>
<FilterDropdownEntitySearchInput context={context} /> <FilterDropdownEntitySearchInput context={context} />

View File

@ -3,9 +3,9 @@ import { useTheme } from '@emotion/react';
import { IconChevronDown } from '@tabler/icons-react'; import { IconChevronDown } from '@tabler/icons-react';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader'; import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem'; import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip'; import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
@ -81,7 +81,7 @@ export function SortDropdownButton<SortField>({
HotkeyScope={HotkeyScope} HotkeyScope={HotkeyScope}
> >
{isOptionUnfolded ? ( {isOptionUnfolded ? (
<DropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
{options.map((option, index) => ( {options.map((option, index) => (
<DropdownMenuSelectableItem <DropdownMenuSelectableItem
key={index} key={index}
@ -93,7 +93,7 @@ export function SortDropdownButton<SortField>({
{option === 'asc' ? 'Ascending' : 'Descending'} {option === 'asc' ? 'Ascending' : 'Descending'}
</DropdownMenuSelectableItem> </DropdownMenuSelectableItem>
))} ))}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
) : ( ) : (
<> <>
<DropdownMenuHeader <DropdownMenuHeader
@ -102,9 +102,9 @@ export function SortDropdownButton<SortField>({
> >
{selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'} {selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
</DropdownMenuHeader> </DropdownMenuHeader>
<DropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<DropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
{availableSorts.map((sort, index) => ( {availableSorts.map((sort, index) => (
<DropdownMenuSelectableItem <DropdownMenuSelectableItem
key={index} key={index}
@ -114,7 +114,7 @@ export function SortDropdownButton<SortField>({
<OverflowingTextWithTooltip text={sort.label} /> <OverflowingTextWithTooltip text={sort.label} />
</DropdownMenuSelectableItem> </DropdownMenuSelectableItem>
))} ))}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</> </>
)} )}
</DropdownButton> </DropdownButton>

View File

@ -1,12 +1,12 @@
import { useRef } from 'react'; import { useRef } from 'react';
import debounce from 'lodash.debounce'; import debounce from 'lodash.debounce';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuCheckableItem } from '@/ui/dropdown/components/DropdownMenuCheckableItem'; import { DropdownMenuCheckableItem } from '@/ui/dropdown/components/DropdownMenuCheckableItem';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput'; import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { Avatar } from '@/users/components/Avatar'; import { Avatar } from '@/users/components/Avatar';
import { isNonEmptyString } from '~/utils/isNonEmptyString'; import { isNonEmptyString } from '~/utils/isNonEmptyString';
@ -72,14 +72,14 @@ export function MultipleEntitySelect<
}); });
return ( return (
<DropdownMenu ref={containerRef}> <StyledDropdownMenu ref={containerRef}>
<DropdownMenuInput <DropdownMenuInput
value={searchFilter} value={searchFilter}
onChange={handleFilterChange} onChange={handleFilterChange}
autoFocus autoFocus
/> />
<DropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight> <StyledDropdownMenuItemsContainer hasMaxHeight>
{entitiesInDropdown?.map((entity) => ( {entitiesInDropdown?.map((entity) => (
<DropdownMenuCheckableItem <DropdownMenuCheckableItem
key={entity.id} key={entity.id}
@ -101,7 +101,7 @@ export function MultipleEntitySelect<
{entitiesInDropdown?.length === 0 && ( {entitiesInDropdown?.length === 0 && (
<DropdownMenuItem>No result</DropdownMenuItem> <DropdownMenuItem>No result</DropdownMenuItem>
)} )}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</DropdownMenu> </StyledDropdownMenu>
); );
} }

View File

@ -1,11 +1,11 @@
import { useRef } from 'react'; import { useRef } from 'react';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput'; import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { IconPlus } from '@/ui/icon'; import { IconPlus } from '@/ui/icon';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -55,7 +55,7 @@ export function SingleEntitySelect<
}); });
return ( return (
<DropdownMenu <StyledDropdownMenu
disableBlur={disableBackgroundBlur} disableBlur={disableBackgroundBlur}
ref={containerRef} ref={containerRef}
width={width} width={width}
@ -65,7 +65,7 @@ export function SingleEntitySelect<
onChange={handleSearchFilterChange} onChange={handleSearchFilterChange}
autoFocus autoFocus
/> />
<DropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<SingleEntitySelectBase <SingleEntitySelectBase
entities={entities} entities={entities}
onEntitySelected={onEntitySelected} onEntitySelected={onEntitySelected}
@ -73,15 +73,15 @@ export function SingleEntitySelect<
/> />
{showCreateButton && ( {showCreateButton && (
<> <>
<DropdownMenuItemsContainer hasMaxHeight> <StyledDropdownMenuItemsContainer hasMaxHeight>
<DropdownMenuItem onClick={onCreate}> <DropdownMenuItem onClick={onCreate}>
<IconPlus size={theme.icon.size.md} /> <IconPlus size={theme.icon.size.md} />
Add New Add New
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
<DropdownMenuSeparator /> <StyledDropdownMenuSeparator />
</> </>
)} )}
</DropdownMenu> </StyledDropdownMenu>
); );
} }

View File

@ -2,8 +2,8 @@ import { useRef } from 'react';
import { Key } from 'ts-key-enum'; import { Key } from 'ts-key-enum';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem'; import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip'; import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { Avatar } from '@/users/components/Avatar'; import { Avatar } from '@/users/components/Avatar';
@ -73,7 +73,7 @@ export function SingleEntitySelectBase<
); );
return ( return (
<DropdownMenuItemsContainer ref={containerRef} hasMaxHeight> <StyledDropdownMenuItemsContainer ref={containerRef} hasMaxHeight>
{entities.loading ? ( {entities.loading ? (
<DropdownMenuSkeletonItem /> <DropdownMenuSkeletonItem />
) : entitiesInDropdown.length === 0 ? ( ) : entitiesInDropdown.length === 0 ? (
@ -97,6 +97,6 @@ export function SingleEntitySelectBase<
</DropdownMenuSelectableItem> </DropdownMenuSelectableItem>
)) ))
)} )}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
); );
} }

View File

@ -1,9 +1,16 @@
/* eslint-disable twenty/styled-components-prefixed-with-styled */
import styled from '@emotion/styled'; import styled from '@emotion/styled';
export const TextInputDisplay = styled.div` const StyledTextInputDisplay = styled.div`
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
width: 100%; width: 100%;
`; `;
export type TextInputDisplayProps = {
children: React.ReactNode;
};
export function TextInputDisplay({ children }: TextInputDisplayProps) {
return <StyledTextInputDisplay>{children}</StyledTextInputDisplay>;
}

View File

@ -1,9 +1,9 @@
/* eslint-disable twenty/styled-components-prefixed-with-styled */ import { ReactElement } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
export const ShowPageContainer = styled.div` export const StyledShowPageContainer = styled.div`
display: flex; display: flex;
flex-direction: ${() => (useIsMobile() ? 'column' : 'row')}; flex-direction: ${() => (useIsMobile() ? 'column' : 'row')};
gap: ${({ theme }) => (useIsMobile() ? theme.spacing(3) : '0')}; gap: ${({ theme }) => (useIsMobile() ? theme.spacing(3) : '0')};
@ -11,3 +11,11 @@ export const ShowPageContainer = styled.div`
overflow-x: ${() => (useIsMobile() ? 'hidden' : 'auto')}; overflow-x: ${() => (useIsMobile() ? 'hidden' : 'auto')};
width: ${() => (useIsMobile() ? `calc(100% - 2px);` : '100%')}; width: ${() => (useIsMobile() ? `calc(100% - 2px);` : '100%')};
`; `;
export type ShowPageContainerProps = {
children: ReactElement[];
};
export function ShowPageContainer({ children }: ShowPageContainerProps) {
return <StyledShowPageContainer>{children} </StyledShowPageContainer>;
}

View File

@ -1,9 +1,9 @@
/* eslint-disable twenty/styled-components-prefixed-with-styled */ import { ReactElement } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
export const ShowPageLeftContainer = styled.div` const StyledShowPageLeftContainer = styled.div`
background: ${({ theme }) => theme.background.secondary}; background: ${({ theme }) => theme.background.secondary};
border-bottom-left-radius: 8px; border-bottom-left-radius: 8px;
border-right: 1px solid border-right: 1px solid
@ -24,3 +24,13 @@ export const ShowPageLeftContainer = styled.div`
z-index: 10; z-index: 10;
`; `;
export type ShowPageLeftContainerProps = {
children: ReactElement[];
};
export function ShowPageLeftContainer({
children,
}: ShowPageLeftContainerProps) {
return <StyledShowPageLeftContainer>{children} </StyledShowPageLeftContainer>;
}

View File

@ -1,9 +1,9 @@
/* eslint-disable twenty/styled-components-prefixed-with-styled */ import { ReactElement } from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
export const ShowPageRightContainer = styled.div` export const StyledShowPageRightContainer = styled.div`
display: flex; display: flex;
flex: 1 0 0; flex: 1 0 0;
flex-direction: column; flex-direction: column;
@ -15,3 +15,15 @@ export const ShowPageRightContainer = styled.div`
return isMobile ? `calc(100% - ${theme.spacing(6)})` : 'auto'; return isMobile ? `calc(100% - ${theme.spacing(6)})` : 'auto';
}}; }};
`; `;
export type ShowPageRightContainerProps = {
children: ReactElement;
};
export function ShowPageRightContainer({
children,
}: ShowPageRightContainerProps) {
return (
<StyledShowPageRightContainer>{children} </StyledShowPageRightContainer>
);
}

View File

@ -4,15 +4,15 @@ import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton'; import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { IconPlus } from '@/ui/icon'; import { IconPlus } from '@/ui/icon';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { hiddenTableColumnsState } from '../states/tableColumnsState'; import { hiddenTableColumnsState } from '../states/tableColumnsState';
const StyledColumnMenu = styled(DropdownMenu)` const StyledColumnMenu = styled(StyledDropdownMenu)`
font-weight: ${({ theme }) => theme.font.weight.regular}; font-weight: ${({ theme }) => theme.font.weight.regular};
`; `;
@ -38,7 +38,7 @@ export const EntityTableColumnMenu = ({
return ( return (
<StyledColumnMenu {...props} ref={ref}> <StyledColumnMenu {...props} ref={ref}>
<DropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
{hiddenColumns.map((column) => ( {hiddenColumns.map((column) => (
<DropdownMenuItem <DropdownMenuItem
key={column.id} key={column.id}
@ -56,7 +56,7 @@ export const EntityTableColumnMenu = ({
{column.columnLabel} {column.columnLabel}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</StyledColumnMenu> </StyledColumnMenu>
); );
}; };

View File

@ -0,0 +1,40 @@
import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { type TableView } from '../../states/tableViewsState';
import { TableOptionsDropdownButton } from './TableOptionsDropdownButton';
import { TableOptionsDropdownContent } from './TableOptionsDropdownContent';
type TableOptionsDropdownProps = {
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
onViewsChange?: (views: TableView[]) => void;
onImport?: () => void;
customHotkeyScope: HotkeyScope;
};
export function TableOptionsDropdown({
onColumnsChange,
onViewsChange,
onImport,
customHotkeyScope,
}: TableOptionsDropdownProps) {
return (
<DropdownButton
buttonComponents={<TableOptionsDropdownButton />}
dropdownHotkeyScope={customHotkeyScope}
dropdownKey="options"
dropdownComponents={
<TableOptionsDropdownContent
onColumnsChange={onColumnsChange}
onImport={onImport}
onViewsChange={onViewsChange}
/>
}
/>
);
}

View File

@ -1,273 +1,17 @@
import { import { StyledHeaderDropdownButton } from '@/ui/dropdown/components/StyledHeaderDropdownButton';
type FormEvent, import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { useTheme } from '@emotion/react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { IconButton } from '@/ui/button/components/IconButton'; export function TableOptionsDropdownButton() {
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader'; const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton({
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput'; key: 'options',
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; });
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
import {
IconChevronLeft,
IconFileImport,
IconMinus,
IconPlus,
IconTag,
} from '@/ui/icon';
import {
hiddenTableColumnsState,
tableColumnsState,
visibleTableColumnsState,
} from '@/ui/table/states/tableColumnsState';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import {
type TableView,
tableViewEditModeState,
tableViewsByIdState,
tableViewsState,
} from '../../states/tableViewsState';
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
import { TableOptionsDropdownSection } from './TableOptionsDropdownSection';
type TableOptionsDropdownButtonProps = {
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
onViewsChange?: (views: TableView[]) => void;
onImport?: () => void;
HotkeyScope: TableOptionsHotkeyScope;
};
enum Option {
Properties = 'Properties',
}
export const TableOptionsDropdownButton = ({
onColumnsChange,
onViewsChange,
onImport,
HotkeyScope,
}: TableOptionsDropdownButtonProps) => {
const theme = useTheme();
const [isUnfolded, setIsUnfolded] = useState(false);
const [selectedOption, setSelectedOption] = useState<Option | undefined>(
undefined,
);
const viewEditInputRef = useRef<HTMLInputElement>(null);
const [columns, setColumns] = useRecoilState(tableColumnsState);
const [viewEditMode, setViewEditMode] = useRecoilState(
tableViewEditModeState,
);
const [views, setViews] = useRecoilScopedState(
tableViewsState,
TableRecoilScopeContext,
);
const visibleColumns = useRecoilValue(visibleTableColumnsState);
const hiddenColumns = useRecoilValue(hiddenTableColumnsState);
const viewsById = useRecoilScopedValue(
tableViewsByIdState,
TableRecoilScopeContext,
);
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const handleColumnVisibilityChange = useCallback(
(columnId: string, nextIsVisible: boolean) => {
const nextColumns = columns.map((column) =>
column.id === columnId
? { ...column, isVisible: nextIsVisible }
: column,
);
(onColumnsChange ?? setColumns)(nextColumns);
},
[columns, onColumnsChange, setColumns],
);
const renderFieldActions = useCallback(
(column: ViewFieldDefinition<ViewFieldMetadata>) =>
// Do not allow hiding last visible column
!column.isVisible || visibleColumns.length > 1 ? (
<IconButton
icon={
column.isVisible ? (
<IconMinus size={theme.icon.size.sm} />
) : (
<IconPlus size={theme.icon.size.sm} />
)
}
onClick={() =>
handleColumnVisibilityChange(column.id, !column.isVisible)
}
/>
) : undefined,
[handleColumnVisibilityChange, theme.icon.size.sm, visibleColumns.length],
);
const resetViewEditMode = useCallback(() => {
setViewEditMode({ mode: undefined, viewId: undefined });
if (viewEditInputRef.current) {
viewEditInputRef.current.value = '';
}
}, [setViewEditMode]);
const handleViewNameSubmit = useCallback(
(event?: FormEvent) => {
event?.preventDefault();
if (viewEditMode.mode && viewEditInputRef.current?.value) {
const name = viewEditInputRef.current.value;
const nextViews =
viewEditMode.mode === 'create'
? [...views, { id: v4(), name }]
: views.map((view) =>
view.id === viewEditMode.viewId ? { ...view, name } : view,
);
(onViewsChange ?? setViews)(nextViews);
}
resetViewEditMode();
},
[
onViewsChange,
resetViewEditMode,
setViews,
viewEditMode.mode,
viewEditMode.viewId,
views,
],
);
const handleSelectOption = useCallback(
(option: Option) => {
handleViewNameSubmit();
setIsUnfolded(true);
setSelectedOption(option);
},
[handleViewNameSubmit],
);
const resetSelectedOption = useCallback(() => {
setSelectedOption(undefined);
}, []);
const handleUnfoldedChange = useCallback(
(nextIsUnfolded: boolean) => {
setIsUnfolded(nextIsUnfolded);
if (!nextIsUnfolded) {
handleViewNameSubmit();
resetSelectedOption();
}
},
[handleViewNameSubmit, resetSelectedOption],
);
useEffect(() => {
isUnfolded || viewEditMode.mode
? setHotkeyScopeAndMemorizePreviousScope(HotkeyScope)
: goBackToPreviousHotkeyScope();
}, [
HotkeyScope,
goBackToPreviousHotkeyScope,
isUnfolded,
setHotkeyScopeAndMemorizePreviousScope,
viewEditMode.mode,
]);
return ( return (
<DropdownButton <StyledHeaderDropdownButton
label="Options" isUnfolded={isDropdownButtonOpen}
isActive={false} onClick={toggleDropdownButton}
isUnfolded={isUnfolded || !!viewEditMode.mode}
onIsUnfoldedChange={handleUnfoldedChange}
HotkeyScope={HotkeyScope}
> >
{!selectedOption && ( Options
<> </StyledHeaderDropdownButton>
{!!viewEditMode.mode ? (
<DropdownMenuInput
ref={viewEditInputRef}
autoFocus
placeholder={
viewEditMode.mode === 'create' ? 'New view' : 'View name'
}
defaultValue={
viewEditMode.viewId
? viewsById[viewEditMode.viewId]?.name
: undefined
}
/>
) : (
<DropdownMenuHeader>View settings</DropdownMenuHeader>
)}
<DropdownMenuSeparator />
<DropdownMenuItemsContainer>
<DropdownMenuItem
onClick={() => handleSelectOption(Option.Properties)}
>
<IconTag size={theme.icon.size.md} />
Properties
</DropdownMenuItem>
{onImport && (
<DropdownMenuItem onClick={onImport}>
<IconFileImport size={theme.icon.size.md} />
Import
</DropdownMenuItem>
)}
</DropdownMenuItemsContainer>
</>
)}
{selectedOption === Option.Properties && (
<>
<DropdownMenuHeader
startIcon={<IconChevronLeft size={theme.icon.size.md} />}
onClick={resetSelectedOption}
>
Properties
</DropdownMenuHeader>
<DropdownMenuSeparator />
<TableOptionsDropdownSection
renderActions={renderFieldActions}
title="Visible"
columns={visibleColumns}
/>
{hiddenColumns.length > 0 && (
<>
<DropdownMenuSeparator />
<TableOptionsDropdownSection
renderActions={renderFieldActions}
title="Hidden"
columns={hiddenColumns}
/>
</>
)}
</>
)}
</DropdownButton>
); );
}; }

View File

@ -0,0 +1,250 @@
import { type FormEvent, useCallback, useRef, useState } from 'react';
import { useTheme } from '@emotion/react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { v4 } from 'uuid';
import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenuHeader } from '@/ui/dropdown/components/DropdownMenuHeader';
import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
} from '@/ui/editable-field/types/ViewField';
import {
IconChevronLeft,
IconFileImport,
IconMinus,
IconPlus,
IconTag,
} from '@/ui/icon';
import {
hiddenTableColumnsState,
tableColumnsState,
visibleTableColumnsState,
} from '@/ui/table/states/tableColumnsState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedValue';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import {
type TableView,
tableViewEditModeState,
tableViewsByIdState,
tableViewsState,
} from '../../states/tableViewsState';
import { TableOptionsHotkeyScope } from '../../types/TableOptionsHotkeyScope';
import { TableOptionsDropdownSection } from './TableOptionsDropdownSection';
type TableOptionsDropdownButtonProps = {
onColumnsChange?: (columns: ViewFieldDefinition<ViewFieldMetadata>[]) => void;
onViewsChange?: (views: TableView[]) => void;
onImport?: () => void;
};
enum Option {
Properties = 'Properties',
}
export function TableOptionsDropdownContent({
onColumnsChange,
onViewsChange,
onImport,
}: TableOptionsDropdownButtonProps) {
const theme = useTheme();
const { closeDropdownButton } = useDropdownButton({ key: 'options' });
const [selectedOption, setSelectedOption] = useState<Option | undefined>(
undefined,
);
const viewEditInputRef = useRef<HTMLInputElement>(null);
const [columns, setColumns] = useRecoilState(tableColumnsState);
const [viewEditMode, setViewEditMode] = useRecoilState(
tableViewEditModeState,
);
const [views, setViews] = useRecoilScopedState(
tableViewsState,
TableRecoilScopeContext,
);
const visibleColumns = useRecoilValue(visibleTableColumnsState);
const hiddenColumns = useRecoilValue(hiddenTableColumnsState);
const viewsById = useRecoilScopedValue(
tableViewsByIdState,
TableRecoilScopeContext,
);
const handleColumnVisibilityChange = useCallback(
(columnId: string, nextIsVisible: boolean) => {
const nextColumns = columns.map((column) =>
column.id === columnId
? { ...column, isVisible: nextIsVisible }
: column,
);
(onColumnsChange ?? setColumns)(nextColumns);
},
[columns, onColumnsChange, setColumns],
);
const renderFieldActions = useCallback(
(column: ViewFieldDefinition<ViewFieldMetadata>) =>
// Do not allow hiding last visible column
!column.isVisible || visibleColumns.length > 1 ? (
<IconButton
icon={
column.isVisible ? (
<IconMinus size={theme.icon.size.sm} />
) : (
<IconPlus size={theme.icon.size.sm} />
)
}
onClick={() =>
handleColumnVisibilityChange(column.id, !column.isVisible)
}
/>
) : undefined,
[handleColumnVisibilityChange, theme.icon.size.sm, visibleColumns.length],
);
const resetViewEditMode = useCallback(() => {
setViewEditMode({ mode: undefined, viewId: undefined });
if (viewEditInputRef.current) {
viewEditInputRef.current.value = '';
}
}, [setViewEditMode]);
const handleViewNameSubmit = useCallback(
(event?: FormEvent) => {
event?.preventDefault();
if (viewEditMode.mode && viewEditInputRef.current?.value) {
const name = viewEditInputRef.current.value;
const nextViews =
viewEditMode.mode === 'create'
? [...views, { id: v4(), name }]
: views.map((view) =>
view.id === viewEditMode.viewId ? { ...view, name } : view,
);
(onViewsChange ?? setViews)(nextViews);
}
resetViewEditMode();
},
[
onViewsChange,
resetViewEditMode,
setViews,
viewEditMode.mode,
viewEditMode.viewId,
views,
],
);
const handleSelectOption = useCallback(
(option: Option) => {
handleViewNameSubmit();
setSelectedOption(option);
},
[handleViewNameSubmit],
);
const resetSelectedOption = useCallback(() => {
setSelectedOption(undefined);
}, []);
useScopedHotkeys(
Key.Escape,
() => {
closeDropdownButton();
},
TableOptionsHotkeyScope.Dropdown,
);
useScopedHotkeys(
Key.Enter,
() => {
handleViewNameSubmit();
resetSelectedOption();
closeDropdownButton();
},
TableOptionsHotkeyScope.Dropdown,
);
return (
<StyledDropdownMenu>
{!selectedOption && (
<>
{!!viewEditMode.mode ? (
<DropdownMenuInput
ref={viewEditInputRef}
autoFocus
placeholder={
viewEditMode.mode === 'create' ? 'New view' : 'View name'
}
defaultValue={
viewEditMode.viewId
? viewsById[viewEditMode.viewId]?.name
: undefined
}
/>
) : (
<DropdownMenuHeader>View settings</DropdownMenuHeader>
)}
<StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer>
<DropdownMenuItem
onClick={() => handleSelectOption(Option.Properties)}
>
<IconTag size={theme.icon.size.md} />
Properties
</DropdownMenuItem>
{onImport && (
<DropdownMenuItem onClick={onImport}>
<IconFileImport size={theme.icon.size.md} />
Import
</DropdownMenuItem>
)}
</StyledDropdownMenuItemsContainer>
</>
)}
{selectedOption === Option.Properties && (
<>
<DropdownMenuHeader
startIcon={<IconChevronLeft size={theme.icon.size.md} />}
onClick={resetSelectedOption}
>
Properties
</DropdownMenuHeader>
<StyledDropdownMenuSeparator />
<TableOptionsDropdownSection
renderActions={renderFieldActions}
title="Visible"
columns={visibleColumns}
/>
{hiddenColumns.length > 0 && (
<>
<StyledDropdownMenuSeparator />
<TableOptionsDropdownSection
renderActions={renderFieldActions}
title="Hidden"
columns={hiddenColumns}
/>
</>
)}
</>
)}
</StyledDropdownMenu>
);
}

View File

@ -5,8 +5,8 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuItemProps, DropdownMenuItemProps,
} from '@/ui/dropdown/components/DropdownMenuItem'; } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { DropdownMenuSubheader } from '@/ui/dropdown/components/DropdownMenuSubheader'; import { StyledDropdownMenuSubheader } from '@/ui/dropdown/components/StyledDropdownMenuSubheader';
import { import {
ViewFieldDefinition, ViewFieldDefinition,
ViewFieldMetadata, ViewFieldMetadata,
@ -20,17 +20,17 @@ type TableOptionsDropdownSectionProps = {
columns: ViewFieldDefinition<ViewFieldMetadata>[]; columns: ViewFieldDefinition<ViewFieldMetadata>[];
}; };
export const TableOptionsDropdownSection = ({ export function TableOptionsDropdownSection({
renderActions, renderActions,
title, title,
columns, columns,
}: TableOptionsDropdownSectionProps) => { }: TableOptionsDropdownSectionProps) {
const theme = useTheme(); const theme = useTheme();
return ( return (
<> <>
<DropdownMenuSubheader>{title}</DropdownMenuSubheader> <StyledDropdownMenuSubheader>{title}</StyledDropdownMenuSubheader>
<DropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
{columns.map((column) => ( {columns.map((column) => (
<DropdownMenuItem key={column.id} actions={renderActions(column)}> <DropdownMenuItem key={column.id} actions={renderActions(column)}>
{column.columnIcon && {column.columnIcon &&
@ -40,7 +40,7 @@ export const TableOptionsDropdownSection = ({
{column.columnLabel} {column.columnLabel}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</> </>
); );
}; }

View File

@ -7,7 +7,7 @@ import { Key } from 'ts-key-enum';
import { Button, ButtonSize } from '@/ui/button/components/Button'; import { Button, ButtonSize } from '@/ui/button/components/Button';
import { ButtonGroup } from '@/ui/button/components/ButtonGroup'; import { ButtonGroup } from '@/ui/button/components/ButtonGroup';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { DropdownMenuContainer } from '@/ui/filter-n-sort/components/DropdownMenuContainer'; import { DropdownMenuContainer } from '@/ui/filter-n-sort/components/DropdownMenuContainer';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState'; import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { savedFiltersScopedState } from '@/ui/filter-n-sort/states/savedFiltersScopedState'; import { savedFiltersScopedState } from '@/ui/filter-n-sort/states/savedFiltersScopedState';
@ -110,12 +110,12 @@ export const TableUpdateViewButtonGroup = ({
{isDropdownOpen && ( {isDropdownOpen && (
<StyledDropdownMenuContainer onClose={handleDropdownClose}> <StyledDropdownMenuContainer onClose={handleDropdownClose}>
<DropdownMenuItemsContainer> <StyledDropdownMenuItemsContainer>
<DropdownMenuItem onClick={handleCreateViewButtonClick}> <DropdownMenuItem onClick={handleCreateViewButtonClick}>
<IconPlus size={theme.icon.size.md} /> <IconPlus size={theme.icon.size.md} />
Create view Create view
</DropdownMenuItem> </DropdownMenuItem>
</DropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
</StyledDropdownMenuContainer> </StyledDropdownMenuContainer>
)} )}
</StyledContainer> </StyledContainer>

View File

@ -5,8 +5,9 @@ import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { IconButton } from '@/ui/button/components/IconButton'; import { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { DropdownMenuSeparator } from '@/ui/dropdown/components/DropdownMenuSeparator'; import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton'; import DropdownButton from '@/ui/filter-n-sort/components/DropdownButton';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState'; import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
import { savedFiltersScopedState } from '@/ui/filter-n-sort/states/savedFiltersScopedState'; import { savedFiltersScopedState } from '@/ui/filter-n-sort/states/savedFiltersScopedState';
@ -34,7 +35,9 @@ import { useRecoilScopedValue } from '@/ui/utilities/recoil-scope/hooks/useRecoi
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext'; import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
import { TableViewsHotkeyScope } from '../../types/TableViewsHotkeyScope'; import { TableViewsHotkeyScope } from '../../types/TableViewsHotkeyScope';
const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)` const StyledBoldDropdownMenuItemsContainer = styled(
StyledDropdownMenuItemsContainer,
)`
font-weight: ${({ theme }) => theme.font.weight.regular}; font-weight: ${({ theme }) => theme.font.weight.regular};
`; `;
@ -66,6 +69,10 @@ export const TableViewsDropdownButton = ({
const tableScopeId = useContextScopeId(TableRecoilScopeContext); const tableScopeId = useContextScopeId(TableRecoilScopeContext);
const { openDropdownButton: openOptionsDropdownButton } = useDropdownButton({
key: 'options',
});
const currentView = useRecoilScopedValue( const currentView = useRecoilScopedValue(
currentTableViewState, currentTableViewState,
TableRecoilScopeContext, TableRecoilScopeContext,
@ -105,8 +112,9 @@ export const TableViewsDropdownButton = ({
const handleAddViewButtonClick = useCallback(() => { const handleAddViewButtonClick = useCallback(() => {
setViewEditMode({ mode: 'create', viewId: undefined }); setViewEditMode({ mode: 'create', viewId: undefined });
openOptionsDropdownButton();
setIsUnfolded(false); setIsUnfolded(false);
}, [setViewEditMode]); }, [setViewEditMode, openOptionsDropdownButton]);
const handleEditViewButtonClick = useCallback( const handleEditViewButtonClick = useCallback(
(event: MouseEvent<HTMLButtonElement>, viewId: string) => { (event: MouseEvent<HTMLButtonElement>, viewId: string) => {
@ -184,13 +192,13 @@ export const TableViewsDropdownButton = ({
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</StyledDropdownMenuItemsContainer> </StyledDropdownMenuItemsContainer>
<DropdownMenuSeparator /> <StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer> <StyledBoldDropdownMenuItemsContainer>
<DropdownMenuItem onClick={handleAddViewButtonClick}> <DropdownMenuItem onClick={handleAddViewButtonClick}>
<IconPlus size={theme.icon.size.md} /> <IconPlus size={theme.icon.size.md} />
Add view Add view
</DropdownMenuItem> </DropdownMenuItem>
</StyledDropdownMenuItemsContainer> </StyledBoldDropdownMenuItemsContainer>
</DropdownButton> </DropdownButton>
); );
}; };

View File

@ -1,5 +1,6 @@
import { useCallback } from 'react'; import { useCallback } from 'react';
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
import type { import type {
ViewFieldDefinition, ViewFieldDefinition,
ViewFieldMetadata, ViewFieldMetadata,
@ -10,10 +11,11 @@ import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownBu
import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState'; import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState';
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope'; import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface'; import { SelectedSortType, SortType } from '@/ui/filter-n-sort/types/interface';
import { TableOptionsDropdownButton } from '@/ui/table/options/components/TableOptionsDropdownButton';
import { TopBar } from '@/ui/top-bar/TopBar'; import { TopBar } from '@/ui/top-bar/TopBar';
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState'; import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { TableOptionsDropdown } from '../../options/components/TableOptionsDropdown';
import { TableUpdateViewButtonGroup } from '../../options/components/TableUpdateViewButtonGroup'; import { TableUpdateViewButtonGroup } from '../../options/components/TableUpdateViewButtonGroup';
import { TableViewsDropdownButton } from '../../options/components/TableViewsDropdownButton'; import { TableViewsDropdownButton } from '../../options/components/TableViewsDropdownButton';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext'; import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
@ -60,54 +62,56 @@ export function TableHeader<SortField>({
); );
return ( return (
<TopBar <RecoilScope SpecificContext={DropdownRecoilScopeContext}>
leftComponent={ <TopBar
<TableViewsDropdownButton leftComponent={
defaultViewName={viewName} <TableViewsDropdownButton
onViewsChange={onViewsChange} defaultViewName={viewName}
HotkeyScope={TableViewsHotkeyScope.ListDropdown}
/>
}
displayBottomBorder={false}
rightComponent={
<>
<FilterDropdownButton
context={TableRecoilScopeContext}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
isPrimaryButton
/>
<SortDropdownButton<SortField>
context={TableRecoilScopeContext}
isSortSelected={sorts.length > 0}
availableSorts={availableSorts || []}
onSortSelect={sortSelect}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
isPrimaryButton
/>
<TableOptionsDropdownButton
onImport={onImport}
onColumnsChange={onColumnsChange}
onViewsChange={onViewsChange} onViewsChange={onViewsChange}
HotkeyScope={TableOptionsHotkeyScope.Dropdown} HotkeyScope={TableViewsHotkeyScope.ListDropdown}
/> />
</> }
} displayBottomBorder={false}
bottomComponent={ rightComponent={
<SortAndFilterBar <>
context={TableRecoilScopeContext} <FilterDropdownButton
sorts={sorts} context={TableRecoilScopeContext}
onRemoveSort={sortUnselect} HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
onCancelClick={() => setSorts([])} isPrimaryButton
hasFilterButton
rightComponent={
<TableUpdateViewButtonGroup
onViewSubmit={onViewSubmit}
HotkeyScope={TableViewsHotkeyScope.CreateDropdown}
/> />
} <SortDropdownButton<SortField>
/> context={TableRecoilScopeContext}
} isSortSelected={sorts.length > 0}
/> availableSorts={availableSorts || []}
onSortSelect={sortSelect}
HotkeyScope={FiltersHotkeyScope.FilterDropdownButton}
isPrimaryButton
/>
<TableOptionsDropdown
onImport={onImport}
onColumnsChange={onColumnsChange}
onViewsChange={onViewsChange}
customHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }}
/>
</>
}
bottomComponent={
<SortAndFilterBar
context={TableRecoilScopeContext}
sorts={sorts}
onRemoveSort={sortUnselect}
onCancelClick={() => setSorts([])}
hasFilterButton
rightComponent={
<TableUpdateViewButtonGroup
onViewSubmit={onViewSubmit}
HotkeyScope={TableViewsHotkeyScope.CreateDropdown}
/>
}
/>
}
/>
</RecoilScope>
); );
} }

View File

@ -1,5 +1,4 @@
/* eslint-disable twenty/styled-components-prefixed-with-styled */ import { PlacesType, PositionStrategy, Tooltip } from 'react-tooltip';
import { Tooltip } from 'react-tooltip';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { rgba } from '../theme/constants/colors'; import { rgba } from '../theme/constants/colors';
@ -11,7 +10,7 @@ export enum TooltipPosition {
Bottom = 'bottom', Bottom = 'bottom',
} }
export const AppTooltip = styled(Tooltip)` const StyledAppTooltip = styled(Tooltip)`
backdrop-filter: ${({ theme }) => theme.blur.strong}; backdrop-filter: ${({ theme }) => theme.blur.strong};
background-color: ${({ theme }) => rgba(theme.color.gray80, 0.8)}; background-color: ${({ theme }) => rgba(theme.color.gray80, 0.8)};
border-radius: ${({ theme }) => theme.border.radius.sm}; border-radius: ${({ theme }) => theme.border.radius.sm};
@ -31,3 +30,19 @@ export const AppTooltip = styled(Tooltip)`
z-index: ${({ theme }) => theme.lastLayerZIndex}; z-index: ${({ theme }) => theme.lastLayerZIndex};
`; `;
export type AppToolipProps = {
className?: string;
anchorSelect?: string;
content?: string;
delayHide?: number;
offset?: number;
noArrow?: boolean;
isOpen?: boolean;
place?: PlacesType;
positionStrategy?: PositionStrategy;
};
export function AppTooltip(props: AppToolipProps) {
return <StyledAppTooltip {...props} />;
}

View File

@ -0,0 +1,21 @@
import { Context, useContext } from 'react';
import { RecoilState, useRecoilState } from 'recoil';
import { RecoilScopeContext } from '../states/RecoilScopeContext';
export function useRecoilScopedFamilyState<StateType>(
recoilState: (param: string) => RecoilState<StateType>,
stateKey: string,
SpecificContext?: Context<string | null>,
) {
const recoilScopeId = useContext(SpecificContext ?? RecoilScopeContext);
if (!recoilScopeId)
throw new Error(
`Using a scoped atom without a RecoilScope : ${
recoilState(stateKey).key
}, verify that you are using a RecoilScope with a specific context if you intended to do so.`,
);
return useRecoilState<StateType>(recoilState(recoilScopeId + stateKey));
}

View File

@ -8,6 +8,7 @@ import { EntityBoard } from '@/ui/board/components/EntityBoard';
import { EntityBoardActionBar } from '@/ui/board/components/EntityBoardActionBar'; import { EntityBoardActionBar } from '@/ui/board/components/EntityBoardActionBar';
import { EntityBoardContextMenu } from '@/ui/board/components/EntityBoardContextMenu'; import { EntityBoardContextMenu } from '@/ui/board/components/EntityBoardContextMenu';
import { BoardOptionsContext } from '@/ui/board/contexts/BoardOptionsContext'; import { BoardOptionsContext } from '@/ui/board/contexts/BoardOptionsContext';
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers'; import { reduceSortsToOrderBy } from '@/ui/filter-n-sort/helpers';
import { SelectedSortType } from '@/ui/filter-n-sort/types/interface'; import { SelectedSortType } from '@/ui/filter-n-sort/types/interface';
import { IconTargetArrow } from '@/ui/icon'; import { IconTargetArrow } from '@/ui/icon';
@ -72,7 +73,7 @@ export function Opportunities() {
title="Opportunities" title="Opportunities"
icon={<IconTargetArrow size={theme.icon.size.md} />} icon={<IconTargetArrow size={theme.icon.size.md} />}
> >
<RecoilScope> <RecoilScope SpecificContext={DropdownRecoilScopeContext}>
<PipelineAddButton /> <PipelineAddButton />
</RecoilScope> </RecoilScope>
</PageHeader> </PageHeader>