Right drawer to edit records (#5551)

This PR introduces a new side panel to edit records and the ability to
minimize the side panel.

The goal is leverage this sidepanel to be able to create records while
being in another show page.

I'm opening the PR for feedback since it involved refactoring and
therefore already touches a lot of files, even though it was quick to
implement.

<img width="1503" alt="Screenshot 2024-05-23 at 17 41 37"
src="https://github.com/twentyhq/twenty/assets/6399865/6f17e7a8-f4e9-4eb4-b392-c756db7198ac">
This commit is contained in:
Félix Malfait
2024-06-03 17:15:05 +02:00
committed by GitHub
parent 8e8078d596
commit 09bfb617b2
61 changed files with 957 additions and 452 deletions

View File

@ -7,6 +7,7 @@ import { Key } from 'ts-key-enum';
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompleted';
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/rightDrawerCloseEventsState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
@ -46,6 +47,8 @@ export const RightDrawer = () => {
isRightDrawerOpenState,
);
const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState);
const isRightDrawerExpanded = useRecoilValue(isRightDrawerExpandedState);
const [, setIsRightDrawerAnimationCompleted] = useRecoilState(
isRightDrawerAnimationCompletedState,
@ -69,8 +72,11 @@ export const RightDrawer = () => {
const isRightDrawerOpen = snapshot
.getLoadable(isRightDrawerOpenState)
.getValue();
const isRightDrawerMinimized = snapshot
.getLoadable(isRightDrawerMinimizedState)
.getValue();
if (isRightDrawerOpen) {
if (isRightDrawerOpen && !isRightDrawerMinimized) {
set(rightDrawerCloseEventState, event);
closeRightDrawer();
}
@ -115,6 +121,13 @@ export const RightDrawer = () => {
closed: {
x: '100%',
},
minimized: {
x: '0%',
width: 'auto',
height: 'auto',
bottom: '0',
top: 'auto',
},
};
const handleAnimationComplete = () => {
setIsRightDrawerAnimationCompleted(isRightDrawerOpen);
@ -122,8 +135,20 @@ export const RightDrawer = () => {
return (
<StyledContainer
initial="closed"
animate={isRightDrawerOpen ? 'normal' : 'closed'}
initial={
isRightDrawerOpen
? isRightDrawerMinimized
? 'minimized'
: 'normal'
: 'closed'
}
animate={
isRightDrawerOpen
? isRightDrawerMinimized
? 'minimized'
: 'normal'
: 'closed'
}
variants={variants}
transition={{
duration: theme.animation.duration.normal,

View File

@ -1,12 +1,14 @@
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { useRecoilState, useRecoilValue } from 'recoil';
import { RightDrawerCalendarEvent } from '@/activities/calendar/right-drawer/components/RightDrawerCalendarEvent';
import { RightDrawerEmailThread } from '@/activities/emails/right-drawer/components/RightDrawerEmailThread';
import { RightDrawerCreateActivity } from '@/activities/right-drawer/components/create/RightDrawerCreateActivity';
import { RightDrawerEditActivity } from '@/activities/right-drawer/components/edit/RightDrawerEditActivity';
import { RightDrawerRecord } from '@/object-record/record-right-drawer/components/RightDrawerRecord';
import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar';
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
import { RightDrawerActivityTopBar } from '../../../../activities/right-drawer/components/RightDrawerActivityTopBar';
import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerPages } from '../types/RightDrawerPages';
@ -30,19 +32,23 @@ const StyledRightDrawerBody = styled.div`
const RIGHT_DRAWER_PAGES_CONFIG = {
[RightDrawerPages.CreateActivity]: {
page: <RightDrawerCreateActivity />,
topBar: <RightDrawerActivityTopBar />,
topBar: <RightDrawerTopBar page={RightDrawerPages.CreateActivity} />,
},
[RightDrawerPages.EditActivity]: {
page: <RightDrawerEditActivity />,
topBar: <RightDrawerActivityTopBar />,
topBar: <RightDrawerTopBar page={RightDrawerPages.EditActivity} />,
},
[RightDrawerPages.ViewEmailThread]: {
page: <RightDrawerEmailThread />,
topBar: <RightDrawerActivityTopBar showActionBar={false} />,
topBar: <RightDrawerTopBar page={RightDrawerPages.ViewEmailThread} />,
},
[RightDrawerPages.ViewCalendarEvent]: {
page: <RightDrawerCalendarEvent />,
topBar: <RightDrawerActivityTopBar showActionBar={false} />,
topBar: <RightDrawerTopBar page={RightDrawerPages.ViewCalendarEvent} />,
},
[RightDrawerPages.ViewRecord]: {
page: <RightDrawerRecord />,
topBar: <RightDrawerTopBar page={RightDrawerPages.ViewRecord} />,
},
};
@ -53,10 +59,14 @@ export const RightDrawerRouter = () => {
? RIGHT_DRAWER_PAGES_CONFIG[rightDrawerPage]
: {};
const isRightDrawerMinimized = useRecoilValue(isRightDrawerMinimizedState);
return (
<StyledRightDrawerPage>
{topBar}
<StyledRightDrawerBody>{page}</StyledRightDrawerBody>
{!isRightDrawerMinimized && (
<StyledRightDrawerBody>{page}</StyledRightDrawerBody>
)}
</StyledRightDrawerPage>
);
};

View File

@ -0,0 +1,116 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilState, useRecoilValue } from 'recoil';
import { Chip, ChipAccent, ChipSize, useIcons } from 'twenty-ui';
import { ActivityActionBar } from '@/activities/right-drawer/components/ActivityActionBar';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
import { RightDrawerTopBarCloseButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton';
import { RightDrawerTopBarExpandButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton';
import { RightDrawerTopBarMinimizeButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarMinimizeButton';
import { StyledRightDrawerTopBar } from '@/ui/layout/right-drawer/components/StyledRightDrawerTopBar';
import { RIGHT_DRAWER_PAGE_ICONS } from '@/ui/layout/right-drawer/constants/RightDrawerPageIcons';
import { RIGHT_DRAWER_PAGE_TITLES } from '@/ui/layout/right-drawer/constants/RightDrawerPageTitles';
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
const StyledTopBarWrapper = styled.div`
display: flex;
`;
const StyledMinimizeTopBarTitleContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
height: 24px;
width: 168px;
`;
const StyledMinimizeTopBarTitle = styled.div`
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const StyledMinimizeTopBarIcon = styled.div`
align-items: center;
display: flex;
`;
export const RightDrawerTopBar = ({ page }: { page: RightDrawerPages }) => {
const isMobile = useIsMobile();
const [isRightDrawerMinimized, setIsRightDrawerMinimized] = useRecoilState(
isRightDrawerMinimizedState,
);
const theme = useTheme();
const handleOnclick = () => {
if (isRightDrawerMinimized) {
setIsRightDrawerMinimized(false);
}
};
const { getIcon } = useIcons();
const PageIcon = getIcon(RIGHT_DRAWER_PAGE_ICONS[page]);
const viewableRecordNameSingular = useRecoilValue(
viewableRecordNameSingularState,
);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular: viewableRecordNameSingular ?? 'company',
});
const ObjectIcon = getIcon(objectMetadataItem.icon);
const label =
page === RightDrawerPages.ViewRecord
? objectMetadataItem.labelSingular
: RIGHT_DRAWER_PAGE_TITLES[page];
const Icon = page === RightDrawerPages.ViewRecord ? ObjectIcon : PageIcon;
return (
<StyledRightDrawerTopBar
onClick={handleOnclick}
isRightDrawerMinimized={isRightDrawerMinimized}
>
{!isRightDrawerMinimized &&
(page === RightDrawerPages.EditActivity ||
page === RightDrawerPages.CreateActivity) && <ActivityActionBar />}
{!isRightDrawerMinimized &&
page !== RightDrawerPages.EditActivity &&
page !== RightDrawerPages.CreateActivity && (
<Chip
label={label}
leftComponent={<Icon size={theme.icon.size.md} />}
size={ChipSize.Large}
accent={ChipAccent.TextSecondary}
clickable={false}
/>
)}
{isRightDrawerMinimized && (
<StyledMinimizeTopBarTitleContainer>
<StyledMinimizeTopBarIcon>
<Icon size={theme.icon.size.md} />
</StyledMinimizeTopBarIcon>
<StyledMinimizeTopBarTitle>{label}</StyledMinimizeTopBarTitle>
</StyledMinimizeTopBarTitleContainer>
)}
<StyledTopBarWrapper>
{!isMobile && !isRightDrawerMinimized && (
<RightDrawerTopBarMinimizeButton />
)}
{!isMobile && !isRightDrawerMinimized && (
<RightDrawerTopBarExpandButton />
)}
<RightDrawerTopBarCloseButton />
</StyledTopBarWrapper>
</StyledRightDrawerTopBar>
);
};

View File

@ -1,4 +1,4 @@
import { IconChevronsRight } from 'twenty-ui';
import { IconX } from 'twenty-ui';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
@ -13,7 +13,7 @@ export const RightDrawerTopBarCloseButton = () => {
return (
<LightIconButton
Icon={IconChevronsRight}
Icon={IconX}
onClick={handleButtonClick}
size="medium"
accent="tertiary"

View File

@ -1,20 +1,21 @@
import { useRecoilState } from 'recoil';
import {
IconLayoutSidebarRightCollapse,
IconLayoutSidebarRightExpand,
} from 'twenty-ui';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { isRightDrawerExpandedState } from '../states/isRightDrawerExpandedState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
export const RightDrawerTopBarExpandButton = () => {
const [isRightDrawerExpanded, setIsRightDrawerExpanded] = useRecoilState(
isRightDrawerExpandedState,
);
const { isRightDrawerExpanded, downsizeRightDrawer, expandRightDrawer } =
useRightDrawer();
const handleButtonClick = () => {
setIsRightDrawerExpanded(!isRightDrawerExpanded);
if (isRightDrawerExpanded === true) {
downsizeRightDrawer();
return;
}
expandRightDrawer();
};
return (

View File

@ -0,0 +1,22 @@
import { IconMinus } from 'twenty-ui';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
export const RightDrawerTopBarMinimizeButton = () => {
const { isRightDrawerMinimized, minimizeRightDrawer, maximizeRightDrawer } =
useRightDrawer();
const handleButtonClick = () => {
isRightDrawerMinimized ? maximizeRightDrawer() : minimizeRightDrawer();
};
return (
<LightIconButton
Icon={IconMinus}
onClick={handleButtonClick}
size="medium"
accent="tertiary"
/>
);
};

View File

@ -1,6 +1,8 @@
import styled from '@emotion/styled';
export const StyledRightDrawerTopBar = styled.div`
export const StyledRightDrawerTopBar = styled.div<{
isRightDrawerMinimized: boolean;
}>`
align-items: center;
background: ${({ theme }) => theme.background.secondary};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
@ -9,9 +11,12 @@ export const StyledRightDrawerTopBar = styled.div`
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.md};
gap: ${({ theme }) => theme.spacing(1)};
height: 56px;
height: ${({ isRightDrawerMinimized }) =>
isRightDrawerMinimized ? '40px' : '56px'};
justify-content: space-between;
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
cursor: ${({ isRightDrawerMinimized }) =>
isRightDrawerMinimized ? 'pointer' : 'default'};
`;

View File

@ -0,0 +1,27 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { RightDrawerTopBar } from '../RightDrawerTopBar';
const meta: Meta<typeof RightDrawerTopBar> = {
title: 'Modules/Activities/RightDrawer/RightDrawerActivityTopBar',
component: RightDrawerTopBar,
decorators: [
(Story) => (
<div style={{ width: '500px' }}>
<Story />
</div>
),
ComponentWithRouterDecorator,
],
parameters: {
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<typeof RightDrawerTopBar>;
export const Default: Story = {};

View File

@ -0,0 +1,9 @@
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
export const RIGHT_DRAWER_PAGE_ICONS = {
[RightDrawerPages.CreateActivity]: 'IconNote',
[RightDrawerPages.EditActivity]: 'IconNote',
[RightDrawerPages.ViewEmailThread]: 'IconMail',
[RightDrawerPages.ViewCalendarEvent]: 'IconCalendarEvent',
[RightDrawerPages.ViewRecord]: 'Icon123',
};

View File

@ -0,0 +1,9 @@
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
export const RIGHT_DRAWER_PAGE_TITLES = {
[RightDrawerPages.CreateActivity]: 'Create Activity',
[RightDrawerPages.EditActivity]: 'Edit Activity',
[RightDrawerPages.ViewEmailThread]: 'Email Thread',
[RightDrawerPages.ViewCalendarEvent]: 'Calendar Event',
[RightDrawerPages.ViewRecord]: 'Record Editor',
};

View File

@ -1,5 +1,6 @@
import { useRecoilCallback, useRecoilState } from 'recoil';
import { isRightDrawerMinimizedState } from '@/ui/layout/right-drawer/states/isRightDrawerMinimizedState';
import { rightDrawerCloseEventState } from '@/ui/layout/right-drawer/states/rightDrawerCloseEventsState';
import { isRightDrawerExpandedState } from '../states/isRightDrawerExpandedState';
@ -9,6 +10,8 @@ import { RightDrawerPages } from '../types/RightDrawerPages';
export const useRightDrawer = () => {
const [isRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
const [isRightDrawerExpanded] = useRecoilState(isRightDrawerExpandedState);
const [isRightDrawerMinimized] = useRecoilState(isRightDrawerMinimizedState);
const [rightDrawerPage] = useRecoilState(rightDrawerPageState);
@ -18,6 +21,7 @@ export const useRightDrawer = () => {
set(rightDrawerPageState, rightDrawerPage);
set(isRightDrawerExpandedState, false);
set(isRightDrawerOpenState, true);
set(isRightDrawerMinimizedState, false);
},
[],
);
@ -27,6 +31,47 @@ export const useRightDrawer = () => {
() => {
set(isRightDrawerExpandedState, false);
set(isRightDrawerOpenState, false);
set(isRightDrawerMinimizedState, false);
},
[],
);
const minimizeRightDrawer = useRecoilCallback(
({ set }) =>
() => {
set(isRightDrawerExpandedState, false);
set(isRightDrawerOpenState, true);
set(isRightDrawerMinimizedState, true);
},
[],
);
const maximizeRightDrawer = useRecoilCallback(
({ set }) =>
() => {
set(isRightDrawerMinimizedState, false);
set(isRightDrawerExpandedState, false);
set(isRightDrawerOpenState, true);
},
[],
);
const expandRightDrawer = useRecoilCallback(
({ set }) =>
() => {
set(isRightDrawerExpandedState, true);
set(isRightDrawerOpenState, true);
set(isRightDrawerMinimizedState, false);
},
[],
);
const downsizeRightDrawer = useRecoilCallback(
({ set }) =>
() => {
set(isRightDrawerExpandedState, false);
set(isRightDrawerOpenState, true);
set(isRightDrawerMinimizedState, false);
},
[],
);
@ -50,8 +95,14 @@ export const useRightDrawer = () => {
return {
rightDrawerPage,
isRightDrawerOpen,
isRightDrawerExpanded,
isRightDrawerMinimized,
openRightDrawer,
closeRightDrawer,
minimizeRightDrawer,
maximizeRightDrawer,
expandRightDrawer,
downsizeRightDrawer,
isSameEventThanRightDrawerClose,
};
};

View File

@ -0,0 +1,6 @@
import { createState } from 'twenty-ui';
export const isRightDrawerMinimizedState = createState<boolean>({
key: 'ui/layout/is-right-drawer-minimized',
defaultValue: false,
});

View File

@ -3,4 +3,5 @@ export enum RightDrawerPages {
EditActivity = 'edit-activity',
ViewEmailThread = 'view-email-thread',
ViewCalendarEvent = 'view-calendar-event',
ViewRecord = 'view-record',
}