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,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);