Add "show company / people" view and "Notes" concept (#528)

* Begin adding show view and refactoring threads to become notes

* Progress on design

* Progress redesign timeline

* Dropdown button, design improvement

* Open comment thread edit mode in drawer

* Autosave local storage and commentThreadcount

* Improve display and fix missing key issue

* Remove some hardcoded CSS properties

* Create button

* Split company show into ui/business + fix eslint

* Fix font weight

* Begin auto-save on edit mode

* Save server-side query result to Apollo cache

* Fix save behavior

* Refetch timeline after creating note

* Rename createCommentThreadWithComment

* Improve styling

* Revert "Improve styling"

This reverts commit 9fbbf2db006e529330edc64f3eb8ff9ecdde6bb0.

* Improve CSS styling

* Bring back border radius inadvertently removed

* padding adjustment

* Improve blocknote design

* Improve edit mode display

* Remove Comments.tsx

* Remove irrelevant comment stories

* Removed un-necessary panel component

* stop using fragment, move trash icon

* Add a basic story for CompanyShow

* Add a basic People show view

* Fix storybook tests

* Add very basic Person story

* Refactor PR1

* Refactor part 2

* Refactor part 3

* Refactor part 4

* Fix tests

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Félix Malfait
2023-07-10 07:25:34 +02:00
committed by GitHub
parent ca180acf9f
commit 94a913a41f
191 changed files with 5390 additions and 721 deletions
@@ -0,0 +1,107 @@
import { Tooltip } from 'react-tooltip';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
import { Avatar } from '@/users/components/Avatar';
import {
beautifyExactDate,
beautifyPastDateRelativeToNow,
} from '@/utils/datetime/date-utils';
import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
type OwnProps = {
comment: Pick<CommentForDrawer, 'id' | 'author' | 'createdAt'>;
actionBar?: React.ReactNode;
};
const StyledContainer = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
padding: ${({ theme }) => theme.spacing(1)};
width: calc(100% - ${({ theme }) => theme.spacing(1)});
`;
const StyledLeftContainer = styled.div`
align-items: end;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledName = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const StyledDate = styled.div`
color: ${({ theme }) => theme.font.color.light};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin-left: ${({ theme }) => theme.spacing(1)};
`;
const StyledTooltip = styled(Tooltip)`
background-color: ${({ theme }) => theme.background.primary};
box-shadow: 0px 2px 4px 3px
${({ theme }) => theme.background.transparent.light};
box-shadow: 2px 4px 16px 6px
${({ theme }) => theme.background.transparent.light};
color: ${({ theme }) => theme.font.color.primary};
opacity: 1;
padding: 8px;
`;
export function CommentHeader({ comment, actionBar }: OwnProps) {
const theme = useTheme();
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(comment.createdAt);
const exactCreatedAt = beautifyExactDate(comment.createdAt);
const showDate = beautifiedCreatedAt !== '';
const author = comment.author;
const authorName = author.displayName;
const avatarUrl = author.avatarUrl;
const commentId = comment.id;
const capitalizedFirstUsernameLetter = isNonEmptyString(authorName)
? authorName.toLocaleUpperCase()[0]
: '';
return (
<StyledContainer>
<StyledLeftContainer>
<Avatar
avatarUrl={avatarUrl}
size={theme.icon.size.md}
placeholder={capitalizedFirstUsernameLetter}
/>
<StyledName>{authorName}</StyledName>
{showDate && (
<>
<StyledDate id={`id-${commentId}`}>
{beautifiedCreatedAt}
</StyledDate>
<StyledTooltip
anchorSelect={`#id-${commentId}`}
content={exactCreatedAt}
clickable
noArrow
/>
</>
)}
</StyledLeftContainer>
<div>{actionBar}</div>
</StyledContainer>
);
}
@@ -0,0 +1,40 @@
import styled from '@emotion/styled';
import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
import { CommentHeader } from './CommentHeader';
type OwnProps = {
comment: CommentForDrawer;
actionBar?: React.ReactNode;
};
const StyledContainer = styled.div`
align-items: flex-start;
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(1)};
justify-content: flex-start;
width: 100%;
`;
const StyledCommentBody = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.md};
line-height: ${({ theme }) => theme.text.lineHeight.md};
overflow-wrap: anywhere;
padding-left: 24px;
text-align: left;
`;
export function CommentThreadItem({ comment, actionBar }: OwnProps) {
return (
<StyledContainer>
<CommentHeader comment={comment} actionBar={actionBar} />
<StyledCommentBody>{comment.body}</StyledCommentBody>
</StyledContainer>
);
}
@@ -0,0 +1,133 @@
import type { Meta, StoryObj } from '@storybook/react';
import { DateTime } from 'luxon';
import { v4 } from 'uuid';
import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
import { mockedUsersData } from '~/testing/mock-data/users';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { CommentThreadActionBar } from '../../right-drawer/CommentThreadActionBar';
import { CommentHeader } from '../CommentHeader';
const meta: Meta<typeof CommentHeader> = {
title: 'Modules/Comments/CommentHeader',
component: CommentHeader,
};
export default meta;
type Story = StoryObj<typeof CommentHeader>;
const mockUser = mockedUsersData[0];
const mockComment: Pick<CommentForDrawer, 'id' | 'author' | 'createdAt'> = {
id: v4(),
author: {
id: v4(),
displayName: mockUser.displayName ?? '',
firstName: mockUser.firstName ?? '',
lastName: mockUser.lastName ?? '',
avatarUrl: mockUser.avatarUrl,
},
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
};
const mockCommentWithLongName: Pick<
CommentForDrawer,
'id' | 'author' | 'createdAt'
> = {
id: v4(),
author: {
id: v4(),
displayName: mockUser.displayName + ' with a very long suffix' ?? '',
firstName: mockUser.firstName ?? '',
lastName: mockUser.lastName ?? '',
avatarUrl: mockUser.avatarUrl,
},
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
};
export const Default: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
comment={{
...mockComment,
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
}}
/>,
),
};
export const FewDaysAgo: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
comment={{
...mockComment,
createdAt: DateTime.now().minus({ days: 2 }).toISO() ?? '',
}}
/>,
),
};
export const FewMonthsAgo: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
comment={{
...mockComment,
createdAt: DateTime.now().minus({ months: 2 }).toISO() ?? '',
}}
/>,
),
};
export const FewYearsAgo: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
comment={{
...mockComment,
createdAt: DateTime.now().minus({ years: 2 }).toISO() ?? '',
}}
/>,
),
};
export const WithoutAvatar: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
comment={{
...mockComment,
author: {
...mockComment.author,
avatarUrl: '',
},
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
}}
/>,
),
};
export const WithLongUserName: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
comment={{
...mockCommentWithLongName,
author: {
...mockCommentWithLongName.author,
avatarUrl: '',
},
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
}}
/>,
),
};
export const WithActionBar: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
comment={{
...mockComment,
createdAt: DateTime.now().minus({ days: 2 }).toISO() ?? '',
}}
actionBar={<CommentThreadActionBar commentThreadId="test-id" />}
/>,
),
};