Build message thread empty right drawer (#3585)

* Trigger message thread top bar

* Rename message thread to thread

* Move all components in a directory

---------

Co-authored-by: Thomas Trompette <thomast@twenty.com>
This commit is contained in:
Thomas Trompette
2024-01-23 10:56:31 +01:00
committed by GitHub
parent 762a56782c
commit 004c23768c
14 changed files with 226 additions and 83 deletions

View File

@ -1,5 +1,8 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { useSetRecoilState } from 'recoil';
import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { ActivityActionBar } from '../../right-drawer/components/ActivityActionBar'; import { ActivityActionBar } from '../../right-drawer/components/ActivityActionBar';
@ -7,15 +10,33 @@ import { Comment } from '../Comment';
import { mockComment, mockCommentWithLongValues } from './mock-comment'; import { mockComment, mockCommentWithLongValues } from './mock-comment';
const CommentSetterEffect = () => {
const setViewableActivity = useSetRecoilState(viewableActivityIdState);
useEffect(() => {
setViewableActivity('test-id');
}, [setViewableActivity]);
return null;
};
const meta: Meta<typeof Comment> = { const meta: Meta<typeof Comment> = {
title: 'Modules/Activity/Comment/Comment', title: 'Modules/Activity/Comment/Comment',
component: Comment, component: Comment,
decorators: [ComponentDecorator], decorators: [
(Story) => (
<>
<CommentSetterEffect />
<Story />
</>
),
ComponentDecorator,
],
argTypes: { argTypes: {
actionBar: { actionBar: {
type: 'boolean', type: 'boolean',
mapping: { mapping: {
true: <ActivityActionBar activityId="test-id" />, true: <ActivityActionBar />,
false: undefined, false: undefined,
}, },
}, },

View File

@ -1,7 +1,10 @@
import { useEffect } from 'react';
import { Meta, StoryObj } from '@storybook/react'; import { Meta, StoryObj } from '@storybook/react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { useSetRecoilState } from 'recoil';
import { ActivityActionBar } from '@/activities/right-drawer/components/ActivityActionBar'; import { ActivityActionBar } from '@/activities/right-drawer/components/ActivityActionBar';
import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { avatarUrl } from '~/testing/mock-data/users'; import { avatarUrl } from '~/testing/mock-data/users';
@ -9,15 +12,33 @@ import { CommentHeader } from '../CommentHeader';
import { mockComment, mockCommentWithLongValues } from './mock-comment'; import { mockComment, mockCommentWithLongValues } from './mock-comment';
const CommentHeaderSetterEffect = () => {
const setViewableActivity = useSetRecoilState(viewableActivityIdState);
useEffect(() => {
setViewableActivity('test-id');
}, [setViewableActivity]);
return null;
};
const meta: Meta<typeof CommentHeader> = { const meta: Meta<typeof CommentHeader> = {
title: 'Modules/Activity/Comment/CommentHeader', title: 'Modules/Activity/Comment/CommentHeader',
component: CommentHeader, component: CommentHeader,
decorators: [ComponentDecorator], decorators: [
(Story) => (
<>
<CommentHeaderSetterEffect />
<Story />
</>
),
ComponentDecorator,
],
argTypes: { argTypes: {
actionBar: { actionBar: {
type: 'boolean', type: 'boolean',
mapping: { mapping: {
true: <ActivityActionBar activityId="test-id" />, true: <ActivityActionBar />,
false: undefined, false: undefined,
}, },
}, },

View File

@ -1,5 +1,6 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useOpenThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenThreadRightDrawer';
import { CardContent } from '@/ui/layout/card/components/CardContent'; import { CardContent } from '@/ui/layout/card/components/CardContent';
import { Avatar } from '@/users/components/Avatar'; import { Avatar } from '@/users/components/Avatar';
import { TimelineThread } from '~/generated/graphql'; import { TimelineThread } from '~/generated/graphql';
@ -11,6 +12,7 @@ const StyledCardContent = styled(CardContent)`
gap: ${({ theme }) => theme.spacing(2)}; gap: ${({ theme }) => theme.spacing(2)};
height: ${({ theme }) => theme.spacing(12)}; height: ${({ theme }) => theme.spacing(12)};
padding: ${({ theme }) => theme.spacing(0, 4)}; padding: ${({ theme }) => theme.spacing(0, 4)};
cursor: pointer;
`; `;
const StyledHeading = styled.div<{ unread: boolean }>` const StyledHeading = styled.div<{ unread: boolean }>`
@ -81,24 +83,31 @@ type ThreadPreviewProps = {
thread: TimelineThread; thread: TimelineThread;
}; };
export const ThreadPreview = ({ divider, thread }: ThreadPreviewProps) => ( export const ThreadPreview = ({ divider, thread }: ThreadPreviewProps) => {
<StyledCardContent divider={divider}> const openMessageThreadRightDrawer = useOpenThreadRightDrawer();
<StyledHeading unread={!thread.read}>
<StyledAvatar
avatarUrl={thread.senderPictureUrl}
placeholder={thread.senderName}
type="rounded"
/>
<StyledSenderName>{thread.senderName}</StyledSenderName>
<StyledThreadCount>{thread.numberOfMessagesInThread}</StyledThreadCount>
</StyledHeading>
<StyledSubjectAndBody> return (
<StyledSubject unread={!thread.read}>{thread.subject}</StyledSubject> <StyledCardContent
<StyledBody>{thread.body}</StyledBody> onClick={() => openMessageThreadRightDrawer()}
</StyledSubjectAndBody> divider={divider}
<StyledReceivedAt> >
{formatToHumanReadableDate(thread.receivedAt)} <StyledHeading unread={!thread.read}>
</StyledReceivedAt> <StyledAvatar
</StyledCardContent> avatarUrl={thread.senderPictureUrl}
); placeholder={thread.senderName}
type="rounded"
/>
<StyledSenderName>{thread.senderName}</StyledSenderName>
<StyledThreadCount>{thread.numberOfMessagesInThread}</StyledThreadCount>
</StyledHeading>
<StyledSubjectAndBody>
<StyledSubject unread={!thread.read}>{thread.subject}</StyledSubject>
<StyledBody>{thread.body}</StyledBody>
</StyledSubjectAndBody>
<StyledReceivedAt>
{formatToHumanReadableDate(thread.receivedAt)}
</StyledReceivedAt>
</StyledCardContent>
);
};

View File

@ -0,0 +1,20 @@
import React from 'react';
import styled from '@emotion/styled';
const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
overflow-y: auto;
position: relative;
`;
export const RightDrawerThread = () => {
return (
<StyledContainer>
<></>
</StyledContainer>
);
};

View File

@ -0,0 +1,24 @@
import styled from '@emotion/styled';
import { StyledRightDrawerTopBar } from '@/ui/layout/right-drawer/components/StyledRightDrawerTopBar';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { RightDrawerTopBarCloseButton } from '../../../../ui/layout/right-drawer/components/RightDrawerTopBarCloseButton';
import { RightDrawerTopBarExpandButton } from '../../../../ui/layout/right-drawer/components/RightDrawerTopBarExpandButton';
const StyledTopBarWrapper = styled.div`
display: flex;
`;
export const RightDrawerThreadTopBar = () => {
const isMobile = useIsMobile();
return (
<StyledRightDrawerTopBar>
<StyledTopBarWrapper>
<RightDrawerTopBarCloseButton />
{!isMobile && <RightDrawerTopBarExpandButton />}
</StyledTopBarWrapper>
</StyledRightDrawerTopBar>
);
};

View File

@ -0,0 +1,26 @@
import { Meta, StoryObj } from '@storybook/react';
import { RightDrawerThreadTopBar } from '@/activities/emails/right-drawer/components/RightDrawerThreadTopBar';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta<typeof RightDrawerThreadTopBar> = {
title: 'Modules/Activities/Emails/RightDrawer/RightDrawerThreadTopBar',
component: RightDrawerThreadTopBar,
decorators: [
(Story) => (
<div style={{ width: '500px' }}>
<Story />
</div>
),
ComponentDecorator,
],
parameters: {
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<typeof RightDrawerThreadTopBar>;
export const Default: Story = {};

View File

@ -0,0 +1,14 @@
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
export const useOpenThreadRightDrawer = () => {
const { openRightDrawer } = useRightDrawer();
const setHotkeyScope = useSetHotkeyScope();
return () => {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
openRightDrawer(RightDrawerPages.ViewThread);
};
};

View File

@ -1,16 +1,14 @@
import { useRecoilState } from 'recoil'; import { useRecoilState, useRecoilValue } from 'recoil';
import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { IconTrash } from '@/ui/display/icon'; import { IconTrash } from '@/ui/display/icon';
import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState'; import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
type ActivityActionBarProps = { export const ActivityActionBar = () => {
activityId: string; const viewableActivityId = useRecoilValue(viewableActivityIdState);
};
export const ActivityActionBar = ({ activityId }: ActivityActionBarProps) => {
const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState); const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
const { deleteOneRecord: deleteOneActivity } = useDeleteOneRecord({ const { deleteOneRecord: deleteOneActivity } = useDeleteOneRecord({
objectNameSingular: CoreObjectNameSingular.Activity, objectNameSingular: CoreObjectNameSingular.Activity,
@ -18,7 +16,9 @@ export const ActivityActionBar = ({ activityId }: ActivityActionBarProps) => {
}); });
const deleteActivity = () => { const deleteActivity = () => {
deleteOneActivity?.(activityId); if (viewableActivityId) {
deleteOneActivity?.(viewableActivityId);
}
setIsRightDrawerOpen(false); setIsRightDrawerOpen(false);
}; };

View File

@ -0,0 +1,26 @@
import styled from '@emotion/styled';
import { ActivityActionBar } from '@/activities/right-drawer/components/ActivityActionBar';
import { StyledRightDrawerTopBar } from '@/ui/layout/right-drawer/components/StyledRightDrawerTopBar';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { RightDrawerTopBarCloseButton } from '../../../ui/layout/right-drawer/components/RightDrawerTopBarCloseButton';
import { RightDrawerTopBarExpandButton } from '../../../ui/layout/right-drawer/components/RightDrawerTopBarExpandButton';
const StyledTopBarWrapper = styled.div`
display: flex;
`;
export const RightDrawerActivityTopBar = () => {
const isMobile = useIsMobile();
return (
<StyledRightDrawerTopBar>
<StyledTopBarWrapper>
<RightDrawerTopBarCloseButton />
{!isMobile && <RightDrawerTopBarExpandButton />}
</StyledTopBarWrapper>
<ActivityActionBar />
</StyledRightDrawerTopBar>
);
};

View File

@ -3,11 +3,11 @@ import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { RightDrawerTopBar } from '../RightDrawerTopBar'; import { RightDrawerActivityTopBar } from '../RightDrawerActivityTopBar';
const meta: Meta<typeof RightDrawerTopBar> = { const meta: Meta<typeof RightDrawerActivityTopBar> = {
title: 'UI/Layout/RightDrawer/RightDrawerTopBar', title: 'Modules/Activities/RightDrawer/RightDrawerActivityTopBar',
component: RightDrawerTopBar, component: RightDrawerActivityTopBar,
decorators: [ decorators: [
(Story) => ( (Story) => (
<div style={{ width: '500px' }}> <div style={{ width: '500px' }}>
@ -22,6 +22,6 @@ const meta: Meta<typeof RightDrawerTopBar> = {
}; };
export default meta; export default meta;
type Story = StoryObj<typeof RightDrawerTopBar>; type Story = StoryObj<typeof RightDrawerActivityTopBar>;
export const Default: Story = {}; export const Default: Story = {};

View File

@ -1,14 +1,15 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilState } from 'recoil'; import { useRecoilState } from 'recoil';
import { RightDrawerThread } from '@/activities/emails/right-drawer/components/RightDrawerThread';
import { RightDrawerThreadTopBar } from '@/activities/emails/right-drawer/components/RightDrawerThreadTopBar';
import { RightDrawerCreateActivity } from '@/activities/right-drawer/components/create/RightDrawerCreateActivity'; import { RightDrawerCreateActivity } from '@/activities/right-drawer/components/create/RightDrawerCreateActivity';
import { RightDrawerEditActivity } from '@/activities/right-drawer/components/edit/RightDrawerEditActivity'; import { RightDrawerEditActivity } from '@/activities/right-drawer/components/edit/RightDrawerEditActivity';
import { RightDrawerActivityTopBar } from '../../../../activities/right-drawer/components/RightDrawerActivityTopBar';
import { rightDrawerPageState } from '../states/rightDrawerPageState'; import { rightDrawerPageState } from '../states/rightDrawerPageState';
import { RightDrawerPages } from '../types/RightDrawerPages'; import { RightDrawerPages } from '../types/RightDrawerPages';
import { RightDrawerTopBar } from './RightDrawerTopBar';
const StyledRightDrawerPage = styled.div` const StyledRightDrawerPage = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -30,13 +31,20 @@ export const RightDrawerRouter = () => {
const [rightDrawerPage] = useRecoilState(rightDrawerPageState); const [rightDrawerPage] = useRecoilState(rightDrawerPageState);
let page = <></>; let page = <></>;
let topBar = <></>;
switch (rightDrawerPage) { switch (rightDrawerPage) {
case RightDrawerPages.CreateActivity: case RightDrawerPages.CreateActivity:
page = <RightDrawerCreateActivity />; page = <RightDrawerCreateActivity />;
topBar = <RightDrawerActivityTopBar />;
break; break;
case RightDrawerPages.EditActivity: case RightDrawerPages.EditActivity:
page = <RightDrawerEditActivity />; page = <RightDrawerEditActivity />;
topBar = <RightDrawerActivityTopBar />;
break;
case RightDrawerPages.ViewThread:
page = <RightDrawerThread />;
topBar = <RightDrawerThreadTopBar />;
break; break;
default: default:
break; break;
@ -44,7 +52,7 @@ export const RightDrawerRouter = () => {
return ( return (
<StyledRightDrawerPage> <StyledRightDrawerPage>
<RightDrawerTopBar /> {topBar}
<StyledRightDrawerBody>{page}</StyledRightDrawerBody> <StyledRightDrawerBody>{page}</StyledRightDrawerBody>
</StyledRightDrawerPage> </StyledRightDrawerPage>
); );

View File

@ -1,44 +0,0 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { ActivityActionBar } from '@/activities/right-drawer/components/ActivityActionBar';
import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
import { RightDrawerTopBarCloseButton } from './RightDrawerTopBarCloseButton';
import { RightDrawerTopBarExpandButton } from './RightDrawerTopBarExpandButton';
const StyledRightDrawerTopBar = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.secondary};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.md};
gap: ${({ theme }) => theme.spacing(1)};
height: 56px;
justify-content: space-between;
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
`;
const StyledTopBarWrapper = styled.div`
display: flex;
`;
export const RightDrawerTopBar = () => {
const isMobile = useIsMobile();
const viewableActivityId = useRecoilValue(viewableActivityIdState);
return (
<StyledRightDrawerTopBar>
<StyledTopBarWrapper>
<RightDrawerTopBarCloseButton />
{!isMobile && <RightDrawerTopBarExpandButton />}
</StyledTopBarWrapper>
<ActivityActionBar activityId={viewableActivityId ?? ''} />
</StyledRightDrawerTopBar>
);
};

View File

@ -0,0 +1,17 @@
import styled from '@emotion/styled';
export const StyledRightDrawerTopBar = styled.div`
align-items: center;
background: ${({ theme }) => theme.background.secondary};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
flex-direction: row;
font-size: ${({ theme }) => theme.font.size.md};
gap: ${({ theme }) => theme.spacing(1)};
height: 56px;
justify-content: space-between;
padding-left: ${({ theme }) => theme.spacing(2)};
padding-right: ${({ theme }) => theme.spacing(2)};
`;

View File

@ -1,4 +1,5 @@
export enum RightDrawerPages { export enum RightDrawerPages {
CreateActivity = 'create-activity', CreateActivity = 'create-activity',
EditActivity = 'edit-activity', EditActivity = 'edit-activity',
ViewThread = 'view-thread',
} }