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

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

View File

@ -3,9 +3,9 @@ import styled from '@emotion/styled';
import { IconPencil } from '@tabler/icons-react';
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 { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { icon } from '@/ui/theme/constants/icon';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
@ -50,14 +50,14 @@ export function BoardColumnMenu({
return (
<StyledMenuContainer ref={boardColumnMenuRef}>
<DropdownMenu>
<StyledDropdownMenu>
{openMenu === 'actions' && (
<DropdownMenuItemsContainer>
<StyledDropdownMenuItemsContainer>
<DropdownMenuSelectableItem onClick={() => setOpenMenu('title')}>
<IconPencil size={icon.size.md} stroke={icon.stroke.sm} />
Rename
</DropdownMenuSelectableItem>
</DropdownMenuItemsContainer>
</StyledDropdownMenuItemsContainer>
)}
{openMenu === 'title' && (
<BoardColumnEditTitleMenu
@ -67,7 +67,7 @@ export function BoardColumnMenu({
title={title}
/>
)}
</DropdownMenu>
</StyledDropdownMenu>
</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 { contextMenuPositionState } from '@/ui/context-menu/states/contextMenuPositionState';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
import { DropdownMenuItemsContainer } from '@/ui/dropdown/components/DropdownMenuItemsContainer';
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { contextMenuEntriesState } from '../states/contextMenuEntriesState';
@ -60,11 +60,11 @@ export function ContextMenu({ selectedIds }: OwnProps) {
}
return (
<StyledContainerContextMenu ref={wrapperRef} position={position}>
<DropdownMenu>
<DropdownMenuItemsContainer>
<StyledDropdownMenu>
<StyledDropdownMenuItemsContainer>
{contextMenuEntries}
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
</StyledContainerContextMenu>
);
}

View File

@ -1,10 +1,16 @@
import { useEffect, useRef } from 'react';
import { Keys } from 'react-hotkeys-hook';
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 { 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 { dropdownButtonCustomHotkeyScopeScopedFamilyState } from '../states/dropdownButtonCustomHotkeyScopeScopedFamilyState';
import { DropdownRecoilScopeContext } from '../states/recoil-scope-contexts/DropdownRecoilScopeContext';
import { HotkeyEffect } from './HotkeyEffect';
@ -16,38 +22,74 @@ const StyledContainer = styled.div`
type OwnProps = {
buttonComponents: JSX.Element | JSX.Element[];
dropdownComponents: JSX.Element | JSX.Element[];
dropdownKey: string;
hotkey?: {
key: Keys;
scope: string;
};
dropdownScopeToSet?: HotkeyScope;
dropdownHotkeyScope?: HotkeyScope;
dropdownPlacement?: Placement;
};
export function DropdownButton({
buttonComponents,
dropdownComponents,
dropdownKey,
hotkey,
dropdownScopeToSet,
dropdownHotkeyScope,
dropdownPlacement = 'bottom-end',
}: OwnProps) {
const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton();
const containerRef = useRef<HTMLDivElement>(null);
const { isDropdownButtonOpen, toggleDropdownButton, closeDropdownButton } =
useDropdownButton({
key: dropdownKey,
});
const { refs, floatingStyles } = useFloating({
placement: 'bottom-end',
placement: dropdownPlacement,
middleware: [flip(), offset()],
});
function handleButtonClick() {
toggleDropdownButton(dropdownScopeToSet);
function handleHotkeyTriggered() {
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 (
<StyledContainer>
<StyledContainer ref={containerRef}>
{hotkey && (
<HotkeyEffect hotkey={hotkey} onHotkeyTriggered={handleButtonClick} />
<HotkeyEffect
hotkey={hotkey}
onHotkeyTriggered={handleHotkeyTriggered}
/>
)}
<div ref={refs.setReference} onClick={handleButtonClick}>
{buttonComponents}
</div>
<div ref={refs.setReference}>{buttonComponents}</div>
{isDropdownButtonOpen && (
<div ref={refs.setFloating} style={floatingStyles}>
{dropdownComponents}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
/* eslint-disable twenty/styled-components-prefixed-with-styled */
import styled from '@emotion/styled';
export const DropdownMenuSubheader = styled.div`
export const StyledDropdownMenuSubheader = styled.div`
background-color: ${({ theme }) => theme.background.transparent.lighter};
color: ${({ theme }) => theme.font.color.light};
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 { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { DropdownMenu } from '../DropdownMenu';
import { DropdownMenuCheckableItem } from '../DropdownMenuCheckableItem';
import { DropdownMenuHeader } from '../DropdownMenuHeader';
import { DropdownMenuInput } from '../DropdownMenuInput';
import { DropdownMenuItem } from '../DropdownMenuItem';
import { DropdownMenuItemsContainer } from '../DropdownMenuItemsContainer';
import { DropdownMenuSelectableItem } from '../DropdownMenuSelectableItem';
import { DropdownMenuSeparator } from '../DropdownMenuSeparator';
import { DropdownMenuSubheader } from '../DropdownMenuSubheader';
import { StyledDropdownMenu } from '../StyledDropdownMenu';
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',
component: DropdownMenu,
component: StyledDropdownMenu,
decorators: [ComponentDecorator],
argTypes: {
as: { table: { disable: true } },
@ -29,7 +29,7 @@ const meta: Meta<typeof DropdownMenu> = {
};
export default meta;
type Story = StoryObj<typeof DropdownMenu>;
type Story = StoryObj<typeof StyledDropdownMenu>;
const FakeContentBelow = () => (
<div style={{ position: 'absolute' }}>
@ -156,9 +156,9 @@ const FakeCheckableMenuItemList = ({ hasAvatar }: { hasAvatar?: boolean }) => {
export const Empty: Story = {
render: (args) => (
<DropdownMenu {...args}>
<StyledDropdownMenu {...args}>
<StyledFakeMenuContent />
</DropdownMenu>
</StyledDropdownMenu>
),
};
@ -179,60 +179,60 @@ export const WithContentBelow: Story = {
export const SimpleMenuItem: Story = {
...WithContentBelow,
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight>
<StyledDropdownMenu {...args}>
<StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
),
};
export const WithHeaders: Story = {
...WithContentBelow,
render: (args) => (
<DropdownMenu {...args}>
<StyledDropdownMenu {...args}>
<DropdownMenuHeader>Header</DropdownMenuHeader>
<DropdownMenuSeparator />
<DropdownMenuSubheader>Subheader 1</DropdownMenuSubheader>
<DropdownMenuItemsContainer>
<StyledDropdownMenuSeparator />
<StyledDropdownMenuSubheader>Subheader 1</StyledDropdownMenuSubheader>
<StyledDropdownMenuItemsContainer>
{mockSelectArray.slice(0, 3).map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
<DropdownMenuSeparator />
<DropdownMenuSubheader>Subheader 2</DropdownMenuSubheader>
<DropdownMenuItemsContainer>
</StyledDropdownMenuItemsContainer>
<StyledDropdownMenuSeparator />
<StyledDropdownMenuSubheader>Subheader 2</StyledDropdownMenuSubheader>
<StyledDropdownMenuItemsContainer>
{mockSelectArray.slice(3).map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
),
};
export const WithIcons: Story = {
...WithContentBelow,
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight>
<StyledDropdownMenu {...args}>
<StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }) => (
<DropdownMenuItem>
<IconUser size={16} />
{name}
</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
),
};
export const WithActions: Story = {
...WithContentBelow,
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight>
<StyledDropdownMenu {...args}>
<StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }, index) => (
<DropdownMenuItem
className={index === 0 ? 'hover' : undefined}
@ -244,8 +244,8 @@ export const WithActions: Story = {
{name}
</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
),
parameters: {
pseudo: { hover: ['.hover'] },
@ -255,71 +255,71 @@ export const WithActions: Story = {
export const LoadingMenu: Story = {
...WithContentBelow,
render: () => (
<DropdownMenu>
<StyledDropdownMenu>
<DropdownMenuInput value={'query'} autoFocus />
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
<StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer hasMaxHeight>
<DropdownMenuSkeletonItem />
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
),
};
export const Search: Story = {
...WithContentBelow,
render: (args) => (
<DropdownMenu {...args}>
<StyledDropdownMenu {...args}>
<DropdownMenuInput />
<DropdownMenuSeparator />
<DropdownMenuItemsContainer hasMaxHeight>
<StyledDropdownMenuSeparator />
<StyledDropdownMenuItemsContainer hasMaxHeight>
{mockSelectArray.map(({ name }) => (
<DropdownMenuItem>{name}</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
),
};
export const SelectableMenuItem: Story = {
...WithContentBelow,
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight>
<StyledDropdownMenu {...args}>
<StyledDropdownMenuItemsContainer hasMaxHeight>
<FakeSelectableMenuItemList />
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
),
};
export const SelectableMenuItemWithAvatar: Story = {
...WithContentBelow,
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight>
<StyledDropdownMenu {...args}>
<StyledDropdownMenuItemsContainer hasMaxHeight>
<FakeSelectableMenuItemList hasAvatar />
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
),
};
export const CheckableMenuItem: Story = {
...WithContentBelow,
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight>
<StyledDropdownMenu {...args}>
<StyledDropdownMenuItemsContainer hasMaxHeight>
<FakeCheckableMenuItemList />
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
),
};
export const CheckableMenuItemWithAvatar: Story = {
...WithContentBelow,
render: (args) => (
<DropdownMenu {...args}>
<DropdownMenuItemsContainer hasMaxHeight>
<StyledDropdownMenu {...args}>
<StyledDropdownMenuItemsContainer hasMaxHeight>
<FakeCheckableMenuItemList hasAvatar />
</DropdownMenuItemsContainer>
</DropdownMenu>
</StyledDropdownMenuItemsContainer>
</StyledDropdownMenu>
),
};

View File

@ -1,17 +1,27 @@
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { useRecoilScopedFamilyState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedFamilyState';
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 {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const [isDropdownButtonOpen, setIsDropdownButtonOpen] = useRecoilScopedState(
isDropdownButtonOpenScopedState,
const [isDropdownButtonOpen, setIsDropdownButtonOpen] =
useRecoilScopedFamilyState(
isDropdownButtonOpenScopedFamilyState,
key,
DropdownRecoilScopeContext,
);
const [dropdownButtonCustomHotkeyScope] = useRecoilScopedFamilyState(
dropdownButtonCustomHotkeyScopeScopedFamilyState,
key,
DropdownRecoilScopeContext,
);
function closeDropdownButton() {
@ -19,22 +29,22 @@ export function useDropdownButton() {
setIsDropdownButtonOpen(false);
}
function openDropdownButton(hotkeyScopeToSet?: HotkeyScope) {
function openDropdownButton() {
setIsDropdownButtonOpen(true);
if (hotkeyScopeToSet) {
if (dropdownButtonCustomHotkeyScope) {
setHotkeyScopeAndMemorizePreviousScope(
hotkeyScopeToSet.scope,
hotkeyScopeToSet.customScopes,
dropdownButtonCustomHotkeyScope.scope,
dropdownButtonCustomHotkeyScope.customScopes,
);
}
}
function toggleDropdownButton(hotkeyScopeToSet?: HotkeyScope) {
function toggleDropdownButton() {
if (isDropdownButtonOpen) {
closeDropdownButton();
} 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';
export const isDropdownButtonOpenScopedState = atomFamily<boolean, string>({
export const isDropdownButtonOpenScopedFamilyState = atomFamily<
boolean,
string
>({
key: 'isDropdownButtonOpenScopedState',
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 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';
const StyledDropdownMenuContainer = styled.ul<{
@ -38,7 +38,7 @@ export function DropdownMenuContainer({
return (
<StyledDropdownMenuContainer data-select-disable {...props} anchor={anchor}>
<DropdownMenu ref={dropdownRef}>{children}</DropdownMenu>
<StyledDropdownMenu ref={dropdownRef}>{children}</StyledDropdownMenu>
</StyledDropdownMenuContainer>
);
}

View File

@ -1,6 +1,6 @@
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 { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
@ -22,7 +22,7 @@ export function FilterDropdownEntitySelect({
return (
<>
<DropdownMenuSeparator />
<StyledDropdownMenuSeparator />
<RecoilScope>
{filterDefinitionUsedInDropdown.entitySelectComponent}
</RecoilScope>

View File

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

View File

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

View File

@ -1,6 +1,6 @@
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 { filterDropdownSearchInputScopedState } from '@/ui/filter-n-sort/states/filterDropdownSearchInputScopedState';
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
@ -119,7 +119,7 @@ export function MultipleFiltersDropdownButton({
selectedOperandInDropdown && (
<>
<FilterDropdownOperandButton context={context} />
<DropdownMenuSeparator />
<StyledDropdownMenuSeparator />
{filterDefinitionUsedInDropdown.type === 'text' && (
<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 { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { StyledHeaderDropdownButton } from '../../dropdown/components/StyledHeaderDropdownButton';
import { availableFiltersScopedState } from '../states/availableFiltersScopedState';
import { filtersScopedState } from '../states/filtersScopedState';
import { FiltersHotkeyScope } from '../types/FiltersHotkeyScope';
@ -27,29 +28,6 @@ const StyledDropdownButtonContainer = styled.div`
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({
context,
HotkeyScope,
@ -109,7 +87,7 @@ export function SingleEntityFilterDropdownButton({
return (
<StyledDropdownButtonContainer>
<StyledDropdownButton
<StyledHeaderDropdownButton
isUnfolded={isUnfolded}
onClick={() => handleIsUnfoldedChange(!isUnfolded)}
>
@ -119,7 +97,7 @@ export function SingleEntityFilterDropdownButton({
'Filter'
)}
<IconChevronDown size={theme.icon.size.md} />
</StyledDropdownButton>
</StyledHeaderDropdownButton>
{isUnfolded && (
<DropdownMenuContainer onClose={() => handleIsUnfoldedChange(false)}>
<FilterDropdownEntitySearchInput context={context} />

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,16 @@
/* eslint-disable twenty/styled-components-prefixed-with-styled */
import styled from '@emotion/styled';
export const TextInputDisplay = styled.div`
const StyledTextInputDisplay = styled.div`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
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 { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
export const ShowPageContainer = styled.div`
export const StyledShowPageContainer = styled.div`
display: flex;
flex-direction: ${() => (useIsMobile() ? 'column' : 'row')};
gap: ${({ theme }) => (useIsMobile() ? theme.spacing(3) : '0')};
@ -11,3 +11,11 @@ export const ShowPageContainer = styled.div`
overflow-x: ${() => (useIsMobile() ? 'hidden' : 'auto')};
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 { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
export const ShowPageLeftContainer = styled.div`
const StyledShowPageLeftContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
border-bottom-left-radius: 8px;
border-right: 1px solid
@ -24,3 +24,13 @@ export const ShowPageLeftContainer = styled.div`
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 { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
export const ShowPageRightContainer = styled.div`
export const StyledShowPageRightContainer = styled.div`
display: flex;
flex: 1 0 0;
flex-direction: column;
@ -15,3 +15,15 @@ export const ShowPageRightContainer = styled.div`
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 { IconButton } from '@/ui/button/components/IconButton';
import { DropdownMenu } from '@/ui/dropdown/components/DropdownMenu';
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 { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { hiddenTableColumnsState } from '../states/tableColumnsState';
const StyledColumnMenu = styled(DropdownMenu)`
const StyledColumnMenu = styled(StyledDropdownMenu)`
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
@ -38,7 +38,7 @@ export const EntityTableColumnMenu = ({
return (
<StyledColumnMenu {...props} ref={ref}>
<DropdownMenuItemsContainer>
<StyledDropdownMenuItemsContainer>
{hiddenColumns.map((column) => (
<DropdownMenuItem
key={column.id}
@ -56,7 +56,7 @@ export const EntityTableColumnMenu = ({
{column.columnLabel}
</DropdownMenuItem>
))}
</DropdownMenuItemsContainer>
</StyledDropdownMenuItemsContainer>
</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 {
type FormEvent,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { useTheme } from '@emotion/react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { v4 } from 'uuid';
import { StyledHeaderDropdownButton } from '@/ui/dropdown/components/StyledHeaderDropdownButton';
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
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 { 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,
]);
export function TableOptionsDropdownButton() {
const { isDropdownButtonOpen, toggleDropdownButton } = useDropdownButton({
key: 'options',
});
return (
<DropdownButton
label="Options"
isActive={false}
isUnfolded={isUnfolded || !!viewEditMode.mode}
onIsUnfoldedChange={handleUnfoldedChange}
HotkeyScope={HotkeyScope}
<StyledHeaderDropdownButton
isUnfolded={isDropdownButtonOpen}
onClick={toggleDropdownButton}
>
{!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>
)}
<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>
Options
</StyledHeaderDropdownButton>
);
};
}

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
import { useCallback } from 'react';
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
import type {
ViewFieldDefinition,
ViewFieldMetadata,
@ -10,10 +11,11 @@ import { SortDropdownButton } from '@/ui/filter-n-sort/components/SortDropdownBu
import { sortsScopedState } from '@/ui/filter-n-sort/states/sortsScopedState';
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
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 { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
import { TableOptionsDropdown } from '../../options/components/TableOptionsDropdown';
import { TableUpdateViewButtonGroup } from '../../options/components/TableUpdateViewButtonGroup';
import { TableViewsDropdownButton } from '../../options/components/TableViewsDropdownButton';
import { TableRecoilScopeContext } from '../../states/recoil-scope-contexts/TableRecoilScopeContext';
@ -60,54 +62,56 @@ export function TableHeader<SortField>({
);
return (
<TopBar
leftComponent={
<TableViewsDropdownButton
defaultViewName={viewName}
onViewsChange={onViewsChange}
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}
<RecoilScope SpecificContext={DropdownRecoilScopeContext}>
<TopBar
leftComponent={
<TableViewsDropdownButton
defaultViewName={viewName}
onViewsChange={onViewsChange}
HotkeyScope={TableOptionsHotkeyScope.Dropdown}
HotkeyScope={TableViewsHotkeyScope.ListDropdown}
/>
</>
}
bottomComponent={
<SortAndFilterBar
context={TableRecoilScopeContext}
sorts={sorts}
onRemoveSort={sortUnselect}
onCancelClick={() => setSorts([])}
hasFilterButton
rightComponent={
<TableUpdateViewButtonGroup
onViewSubmit={onViewSubmit}
HotkeyScope={TableViewsHotkeyScope.CreateDropdown}
}
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
/>
<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 { Tooltip } from 'react-tooltip';
import { PlacesType, PositionStrategy, Tooltip } from 'react-tooltip';
import styled from '@emotion/styled';
import { rgba } from '../theme/constants/colors';
@ -11,7 +10,7 @@ export enum TooltipPosition {
Bottom = 'bottom',
}
export const AppTooltip = styled(Tooltip)`
const StyledAppTooltip = styled(Tooltip)`
backdrop-filter: ${({ theme }) => theme.blur.strong};
background-color: ${({ theme }) => rgba(theme.color.gray80, 0.8)};
border-radius: ${({ theme }) => theme.border.radius.sm};
@ -31,3 +30,19 @@ export const AppTooltip = styled(Tooltip)`
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));
}