Lucas/t 366 on comment drawer when i have comments on the selected (#201)

* Fixed right drawer width and shared in theme

* Added date packages and tooltip

* Added date utils and tests

* Added comment thread components

* Fixed comment chip

* Fix from rebase

* Fix from rebase

* Fix margin right

* Fixed CSS and graphql
This commit is contained in:
Lucas Bordeau
2023-06-07 12:48:44 +02:00
committed by GitHub
parent b1bf050936
commit 5e2673a2a4
30 changed files with 688 additions and 77 deletions

View File

@ -17,9 +17,11 @@
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"apollo-link-rest": "^0.9.0",
"date-fns": "^2.30.0",
"graphql": "^16.6.0",
"jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.26",
"luxon": "^3.3.0",
"react": "^18.2.0",
"react-datepicker": "^4.11.0",
"react-dom": "^18.2.0",
@ -27,6 +29,7 @@
"react-icons": "^4.8.0",
"react-router-dom": "^6.4.4",
"react-textarea-autosize": "^8.4.1",
"react-tooltip": "^5.13.1",
"recoil": "^0.7.7",
"uuid": "^9.0.0",
"web-vitals": "^2.1.4"
@ -54,6 +57,7 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/luxon": "^3.3.0",
"@types/react-datepicker": "^4.11.2",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.45.0",
@ -3701,6 +3705,19 @@
"integrity": "sha512-cEee/Z+I12mZcFJshKcCqC8tuX5hG3s+d+9nZ3LabqKF1vKdF41B92pJVCBggjAGORAeOzyyDDKrZwIkLffeOQ==",
"dev": true
},
"node_modules/@floating-ui/core": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.2.6.tgz",
"integrity": "sha512-EvYTiXet5XqweYGClEmpu3BoxmsQ4hkj3QaYA6qEnigCWffTP3vNRwBReTdrwDwo7OoJ3wM8Uoe9Uk4n+d4hfg=="
},
"node_modules/@floating-ui/dom": {
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.2.9.tgz",
"integrity": "sha512-sosQxsqgxMNkV3C+3UqTS6LxP7isRLwX8WMepp843Rb3/b0Wz8+MdUkxJksByip3C2WwLugLHN1b4ibn//zKwQ==",
"dependencies": {
"@floating-ui/core": "^1.2.6"
}
},
"node_modules/@graphql-codegen/cli": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@graphql-codegen/cli/-/cli-3.3.1.tgz",
@ -11044,6 +11061,12 @@
"integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==",
"dev": true
},
"node_modules/@types/luxon": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.3.0.tgz",
"integrity": "sha512-uKRI5QORDnrGFYgcdAVnHvEIvEZ8noTpP/Bg+HeUzZghwinDlIS87DEenV5r1YoOF9G4x600YsUXLWZ19rmTmg==",
"dev": true
},
"node_modules/@types/mdx": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.5.tgz",
@ -28277,6 +28300,14 @@
"yallist": "^3.0.2"
}
},
"node_modules/luxon": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.3.0.tgz",
"integrity": "sha512-An0UCfG/rSiqtAIiBPO0Y9/zAnHUZxAMiCpTd5h2smgsj7GGmcenvrvww2cqNA8/4A5ZrD1gJpHN2mIHZQF+Mg==",
"engines": {
"node": ">=12"
}
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
@ -33806,6 +33837,19 @@
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/react-tooltip": {
"version": "5.13.1",
"resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.13.1.tgz",
"integrity": "sha512-9NstDFdjyy6cIH9zjeT70zXTHlW/TIGCOWQmhkAyqLFeQioLg1FXvb9ec7AxSpn0zyFUkFSLdFYxZRuewti3Aw==",
"dependencies": {
"@floating-ui/dom": "^1.0.0",
"classnames": "^2.3.0"
},
"peerDependencies": {
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",

View File

@ -12,9 +12,11 @@
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"apollo-link-rest": "^0.9.0",
"date-fns": "^2.30.0",
"graphql": "^16.6.0",
"jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.26",
"luxon": "^3.3.0",
"react": "^18.2.0",
"react-datepicker": "^4.11.0",
"react-dom": "^18.2.0",
@ -22,6 +24,7 @@
"react-icons": "^4.8.0",
"react-router-dom": "^6.4.4",
"react-textarea-autosize": "^8.4.1",
"react-tooltip": "^5.13.1",
"recoil": "^0.7.7",
"uuid": "^9.0.0",
"web-vitals": "^2.1.4"
@ -98,6 +101,7 @@
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/luxon": "^3.3.0",
"@types/react-datepicker": "^4.11.2",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^5.45.0",

View File

@ -1081,19 +1081,19 @@ export type WorkspaceMember = {
workspace: Workspace;
};
export type GetCompanyCountsQueryVariables = Exact<{
export type GetCompanyCommentsCountQueryVariables = Exact<{
where?: InputMaybe<CompanyWhereInput>;
}>;
export type GetCompanyCountsQuery = { __typename?: 'Query', companies: Array<{ __typename?: 'Company', commentsCount: number }> };
export type GetCompanyCommentsCountQuery = { __typename?: 'Query', companies: Array<{ __typename?: 'Company', commentsCount: number }> };
export type GetPeopleCountsQueryVariables = Exact<{
export type GetPeopleCommentsCountQueryVariables = Exact<{
where?: InputMaybe<PersonWhereInput>;
}>;
export type GetPeopleCountsQuery = { __typename?: 'Query', people: Array<{ __typename?: 'Person', commentsCount: number }> };
export type GetPeopleCommentsCountQuery = { __typename?: 'Query', people: Array<{ __typename?: 'Person', commentsCount: number }> };
export type GetCommentThreadsByTargetsQueryVariables = Exact<{
commentThreadTargetIds: Array<Scalars['String']> | Scalars['String'];
@ -1227,8 +1227,8 @@ export type GetUsersQueryVariables = Exact<{ [key: string]: never; }>;
export type GetUsersQuery = { __typename?: 'Query', findManyUser: Array<{ __typename?: 'User', id: string }> };
export const GetCompanyCountsDocument = gql`
query GetCompanyCounts($where: CompanyWhereInput) {
export const GetCompanyCommentsCountDocument = gql`
query GetCompanyCommentsCount($where: CompanyWhereInput) {
companies: findManyCompany(where: $where) {
commentsCount: _commentCount
}
@ -1236,34 +1236,34 @@ export const GetCompanyCountsDocument = gql`
`;
/**
* __useGetCompanyCountsQuery__
* __useGetCompanyCommentsCountQuery__
*
* To run a query within a React component, call `useGetCompanyCountsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetCompanyCountsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* To run a query within a React component, call `useGetCompanyCommentsCountQuery` and pass it any options that fit your needs.
* When your component renders, `useGetCompanyCommentsCountQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetCompanyCountsQuery({
* const { data, loading, error } = useGetCompanyCommentsCountQuery({
* variables: {
* where: // value for 'where'
* },
* });
*/
export function useGetCompanyCountsQuery(baseOptions?: Apollo.QueryHookOptions<GetCompanyCountsQuery, GetCompanyCountsQueryVariables>) {
export function useGetCompanyCommentsCountQuery(baseOptions?: Apollo.QueryHookOptions<GetCompanyCommentsCountQuery, GetCompanyCommentsCountQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetCompanyCountsQuery, GetCompanyCountsQueryVariables>(GetCompanyCountsDocument, options);
return Apollo.useQuery<GetCompanyCommentsCountQuery, GetCompanyCommentsCountQueryVariables>(GetCompanyCommentsCountDocument, options);
}
export function useGetCompanyCountsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetCompanyCountsQuery, GetCompanyCountsQueryVariables>) {
export function useGetCompanyCommentsCountLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetCompanyCommentsCountQuery, GetCompanyCommentsCountQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetCompanyCountsQuery, GetCompanyCountsQueryVariables>(GetCompanyCountsDocument, options);
return Apollo.useLazyQuery<GetCompanyCommentsCountQuery, GetCompanyCommentsCountQueryVariables>(GetCompanyCommentsCountDocument, options);
}
export type GetCompanyCountsQueryHookResult = ReturnType<typeof useGetCompanyCountsQuery>;
export type GetCompanyCountsLazyQueryHookResult = ReturnType<typeof useGetCompanyCountsLazyQuery>;
export type GetCompanyCountsQueryResult = Apollo.QueryResult<GetCompanyCountsQuery, GetCompanyCountsQueryVariables>;
export const GetPeopleCountsDocument = gql`
query GetPeopleCounts($where: PersonWhereInput) {
export type GetCompanyCommentsCountQueryHookResult = ReturnType<typeof useGetCompanyCommentsCountQuery>;
export type GetCompanyCommentsCountLazyQueryHookResult = ReturnType<typeof useGetCompanyCommentsCountLazyQuery>;
export type GetCompanyCommentsCountQueryResult = Apollo.QueryResult<GetCompanyCommentsCountQuery, GetCompanyCommentsCountQueryVariables>;
export const GetPeopleCommentsCountDocument = gql`
query GetPeopleCommentsCount($where: PersonWhereInput) {
people: findManyPerson(where: $where) {
commentsCount: _commentCount
}
@ -1271,32 +1271,32 @@ export const GetPeopleCountsDocument = gql`
`;
/**
* __useGetPeopleCountsQuery__
* __useGetPeopleCommentsCountQuery__
*
* To run a query within a React component, call `useGetPeopleCountsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetPeopleCountsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* To run a query within a React component, call `useGetPeopleCommentsCountQuery` and pass it any options that fit your needs.
* When your component renders, `useGetPeopleCommentsCountQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetPeopleCountsQuery({
* const { data, loading, error } = useGetPeopleCommentsCountQuery({
* variables: {
* where: // value for 'where'
* },
* });
*/
export function useGetPeopleCountsQuery(baseOptions?: Apollo.QueryHookOptions<GetPeopleCountsQuery, GetPeopleCountsQueryVariables>) {
export function useGetPeopleCommentsCountQuery(baseOptions?: Apollo.QueryHookOptions<GetPeopleCommentsCountQuery, GetPeopleCommentsCountQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetPeopleCountsQuery, GetPeopleCountsQueryVariables>(GetPeopleCountsDocument, options);
return Apollo.useQuery<GetPeopleCommentsCountQuery, GetPeopleCommentsCountQueryVariables>(GetPeopleCommentsCountDocument, options);
}
export function useGetPeopleCountsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetPeopleCountsQuery, GetPeopleCountsQueryVariables>) {
export function useGetPeopleCommentsCountLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetPeopleCommentsCountQuery, GetPeopleCommentsCountQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetPeopleCountsQuery, GetPeopleCountsQueryVariables>(GetPeopleCountsDocument, options);
return Apollo.useLazyQuery<GetPeopleCommentsCountQuery, GetPeopleCommentsCountQueryVariables>(GetPeopleCommentsCountDocument, options);
}
export type GetPeopleCountsQueryHookResult = ReturnType<typeof useGetPeopleCountsQuery>;
export type GetPeopleCountsLazyQueryHookResult = ReturnType<typeof useGetPeopleCountsLazyQuery>;
export type GetPeopleCountsQueryResult = Apollo.QueryResult<GetPeopleCountsQuery, GetPeopleCountsQueryVariables>;
export type GetPeopleCommentsCountQueryHookResult = ReturnType<typeof useGetPeopleCommentsCountQuery>;
export type GetPeopleCommentsCountLazyQueryHookResult = ReturnType<typeof useGetPeopleCommentsCountLazyQuery>;
export type GetPeopleCommentsCountQueryResult = Apollo.QueryResult<GetPeopleCommentsCountQuery, GetPeopleCommentsCountQueryVariables>;
export const GetCommentThreadsByTargetsDocument = gql`
query GetCommentThreadsByTargets($commentThreadTargetIds: [String!]!) {
findManyCommentThreads(

View File

@ -4,7 +4,7 @@ import { CommentChip, CommentChipProps } from './CommentChip';
const StyledCellWrapper = styled.div`
position: relative;
right: 38px;
right: 34px;
top: -13px;
width: 0;
height: 0;

View File

@ -9,18 +9,18 @@ export type CommentChipProps = {
const StyledChip = styled.div`
height: 26px;
min-width: 34px;
width: fit-content;
padding-left: 2px;
padding-right: 2px;
padding-left: 4px;
padding-right: 4px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
gap: 2px;
gap: 4px;
background: ${(props) => props.theme.secondaryBackgroundTransparent};
background: ${(props) => props.theme.primaryBackgroundTransparent};
backdrop-filter: blur(6px);
border-radius: ${(props) => props.theme.borderRadius};
@ -53,7 +53,7 @@ export function CommentChip({ count, onClick }: CommentChipProps) {
return (
<StyledChip data-testid="comment-chip" onClick={onClick}>
<StyledCount>{formattedCount}</StyledCount>
<IconComment size={12} />
<IconComment size={16} />
</StyledChip>
);
}

View File

@ -0,0 +1,80 @@
import { Tooltip } from 'react-tooltip';
import styled from '@emotion/styled';
import { UserAvatar } from '@/users/components/UserAvatar';
import {
beautifyExactDate,
beautifyPastDateRelativeToNow,
} from '@/utils/datetime/date-utils';
type OwnProps = {
avatarUrl: string | null | undefined;
username: string;
createdAt: Date;
};
const StyledContainer = styled.div`
display: flex;
align-items: center;
flex-direction: row;
justify-content: flex-start;
padding: ${(props) => props.theme.spacing(1)};
gap: ${(props) => props.theme.spacing(1)};
`;
const StyledName = styled.div`
font-size: 13px;
font-weight: 400;
color: ${(props) => props.theme.text80};
`;
const StyledDate = styled.div`
font-size: 12px;
font-weight: 400;
color: ${(props) => props.theme.text30};
padding-top: 1.5px;
margin-left: ${(props) => props.theme.spacing(1)};
`;
const StyledTooltip = styled(Tooltip)`
padding: 8px;
`;
export function CommentHeader({ avatarUrl, username, createdAt }: OwnProps) {
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(createdAt);
const exactCreatedAt = beautifyExactDate(createdAt);
const showDate = beautifiedCreatedAt !== '';
const capitalizedFirstUsernameLetter =
username !== '' ? username.toLocaleUpperCase()[0] : '';
return (
<StyledContainer>
<UserAvatar
avatarUrl={avatarUrl}
size={16}
placeholderLetter={capitalizedFirstUsernameLetter}
/>
<StyledName>{username}</StyledName>
{showDate && (
<>
<StyledDate className="comment-created-at">
{beautifiedCreatedAt}
</StyledDate>
<StyledTooltip
anchorSelect=".comment-created-at"
content={exactCreatedAt}
clickable
noArrow
/>
</>
)}
</StyledContainer>
);
}

View File

@ -1,27 +1,37 @@
import styled from '@emotion/styled';
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
import { CommentTextInput } from './CommentTextInput';
import { CommentThreadItem } from './CommentThreadItem';
type OwnProps = {
commentThread: CommentThreadForDrawer;
};
const StyledContainer = styled.div`
display: flex;
align-items: flex-start;
flex-direction: column;
justify-content: flex-start;
gap: ${(props) => props.theme.spacing(4)};
padding: ${(props) => props.theme.spacing(2)};
`;
export function CommentThread({ commentThread }: OwnProps) {
function handleSendComment(text: string) {
console.log(text);
}
return (
<div>
<StyledContainer>
{commentThread.comments?.map((comment) => (
<div key={comment.id}>
<div>
{comment.author?.displayName} - {comment.createdAt}
</div>
<div>{comment.body}</div>
</div>
<CommentThreadItem key={comment.id} comment={comment} />
))}
<CommentTextInput onSend={handleSendComment} />
</div>
</StyledContainer>
);
}

View File

@ -0,0 +1,40 @@
import styled from '@emotion/styled';
import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
import { CommentHeader } from './CommentHeader';
type OwnProps = {
comment: CommentForDrawer;
};
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
gap: ${(props) => props.theme.spacing(1)};
`;
const StyledCommentBody = styled.div`
font-size: ${(props) => props.theme.fontSizeMedium};
line-height: 19.5px;
text-align: left;
padding-left: 24px;
color: ${(props) => props.theme.text60};
`;
export function CommentThreadItem({ comment }: OwnProps) {
return (
<StyledContainer>
<CommentHeader
avatarUrl={comment.author.avatarUrl}
username={comment.author.displayName}
createdAt={comment.createdAt}
/>
<StyledCommentBody>{comment.body}</StyledCommentBody>
</StyledContainer>
);
}

View File

@ -7,7 +7,7 @@ import { CellCommentChip } from '../CellCommentChip';
import { CommentChip } from '../CommentChip';
const meta: Meta<typeof CellCommentChip> = {
title: 'Components/CellCommentChip',
title: 'Components/Comments/CellCommentChip',
component: CellCommentChip,
};

View File

@ -0,0 +1,67 @@
import type { Meta, StoryObj } from '@storybook/react';
import { DateTime } from 'luxon';
import { mockedUsersData } from '~/testing/mock-data/users';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { CommentHeader } from '../CommentHeader';
const meta: Meta<typeof CommentHeader> = {
title: 'Components/Comments/CommentHeader',
component: CommentHeader,
};
export default meta;
type Story = StoryObj<typeof CommentHeader>;
const mockUser = mockedUsersData[0];
export const Default: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
avatarUrl={mockUser.avatarUrl ?? ''}
username={mockUser.displayName ?? ''}
createdAt={DateTime.now().minus({ hours: 2 }).toJSDate()}
/>,
),
};
export const FewDaysAgo: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
avatarUrl={mockUser.avatarUrl ?? ''}
username={mockUser.displayName ?? ''}
createdAt={DateTime.now().minus({ days: 2 }).toJSDate()}
/>,
),
};
export const FewMonthsAgo: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
avatarUrl={mockUser.avatarUrl ?? ''}
username={mockUser.displayName ?? ''}
createdAt={DateTime.now().minus({ months: 2 }).toJSDate()}
/>,
),
};
export const FewYearsAgo: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
avatarUrl={mockUser.avatarUrl ?? ''}
username={mockUser.displayName ?? ''}
createdAt={DateTime.now().minus({ years: 2 }).toJSDate()}
/>,
),
};
export const WithoutAvatar: Story = {
render: getRenderWrapperForComponent(
<CommentHeader
avatarUrl={''}
username={mockUser.displayName ?? ''}
createdAt={DateTime.now().minus({ hours: 2 }).toJSDate()}
/>,
),
};

View File

@ -6,7 +6,7 @@ import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { CommentTextInput } from '../CommentTextInput';
const meta: Meta<typeof CommentTextInput> = {
title: 'Components/CommentTextInput',
title: 'Components/Comments/CommentTextInput',
component: CommentTextInput,
argTypes: {
onSend: {

View File

@ -1,12 +1,12 @@
import { gql } from '@apollo/client';
import {
useGetCompanyCountsQuery,
useGetPeopleCountsQuery,
useGetCompanyCommentsCountQuery,
useGetPeopleCommentsCountQuery,
} from '../../../generated/graphql';
export const GET_COMPANY_COMMENT_COUNT = gql`
query GetCompanyCounts($where: CompanyWhereInput) {
query GetCompanyCommentsCount($where: CompanyWhereInput) {
companies: findManyCompany(where: $where) {
commentsCount: _commentCount
}
@ -14,14 +14,14 @@ export const GET_COMPANY_COMMENT_COUNT = gql`
`;
export const useCompanyCommentsCountQuery = (companyId: string) => {
const { data, ...rest } = useGetCompanyCountsQuery({
const { data, ...rest } = useGetCompanyCommentsCountQuery({
variables: { where: { id: { equals: companyId } } },
});
return { ...rest, data: data?.companies[0].commentsCount };
};
export const GET_PEOPLE_COMMENT_COUNT = gql`
query GetPeopleCounts($where: PersonWhereInput) {
query GetPeopleCommentsCount($where: PersonWhereInput) {
people: findManyPerson(where: $where) {
commentsCount: _commentCount
}
@ -29,7 +29,7 @@ export const GET_PEOPLE_COMMENT_COUNT = gql`
`;
export const usePeopleCommentsCountQuery = (personId: string) => {
const { data, ...rest } = useGetPeopleCountsQuery({
const { data, ...rest } = useGetPeopleCommentsCountQuery({
variables: { where: { id: { equals: personId } } },
});
return { ...rest, data: data?.people[0].commentsCount };

View File

@ -0,0 +1,5 @@
import { CommentThreadForDrawer } from './CommentThreadForDrawer';
export type CommentForDrawer = NonNullable<
CommentThreadForDrawer['comments']
>[0];

View File

@ -1,9 +1,9 @@
import { CellCommentChip } from '@/comments/components/comments/CellCommentChip';
import { useOpenCommentRightDrawer } from '@/comments/hooks/useOpenCommentRightDrawer';
import { useCompanyCommentsCountQuery } from '@/comments/services';
import EditableChip from '@/ui/components/editable-cell/types/EditableChip';
import { getLogoUrlFromDomainName } from '@/utils/utils';
import { CellCommentChip } from '../../comments/components/comments/CellCommentChip';
import { useCompanyCommentsCountQuery } from '../../comments/services';
import { Company } from '../interfaces/company.interface';
import { updateCompany } from '../services';
@ -16,7 +16,10 @@ type OwnProps = {
export function CompanyEditableNameChipCell({ company }: OwnProps) {
const openCommentRightDrawer = useOpenCommentRightDrawer();
function handleCommentClick() {
function handleCommentClick(event: React.MouseEvent<HTMLDivElement>) {
event.preventDefault();
event.stopPropagation();
openCommentRightDrawer([
{
type: 'Company',
@ -27,6 +30,8 @@ export function CompanyEditableNameChipCell({ company }: OwnProps) {
const commentCount = useCompanyCommentsCountQuery(company.id);
const displayCommentCount = !commentCount.loading;
return (
<EditableChip
value={company.name || ''}
@ -40,9 +45,9 @@ export function CompanyEditableNameChipCell({ company }: OwnProps) {
}}
ChipComponent={CompanyChip}
rightEndContents={[
commentCount.loading ? null : (
displayCommentCount && (
<CellCommentChip
count={commentCount.data || 0}
count={commentCount.data ?? 0}
onClick={handleCommentClick}
/>
),

View File

@ -21,6 +21,7 @@ describe('Company mappers', () => {
id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e87',
email: 'john@example.com',
displayName: 'John Doe',
avatarUrl: 'https://example.com/avatar.png',
__typename: 'User',
},
pipes: [
@ -47,6 +48,7 @@ describe('Company mappers', () => {
__typename: 'users',
id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e87',
email: 'john@example.com',
avatarUrl: 'https://example.com/avatar.png',
displayName: 'John Doe',
workspaceMember: undefined,
},
@ -67,6 +69,7 @@ describe('Company mappers', () => {
accountOwner: {
id: '522d4ec4-c46b-4360-a0a7-df8df170be81',
email: 'john@example.com',
avatarUrl: 'https://example.com/avatar.png',
displayName: 'John Doe',
__typename: 'users',
},

View File

@ -2,9 +2,9 @@ import { useState } from 'react';
import styled from '@emotion/styled';
import { CellCommentChip } from '@/comments/components/comments/CellCommentChip';
import { useOpenCommentRightDrawer } from '@/comments/hooks/useOpenCommentRightDrawer';
import { EditableDoubleText } from '@/ui/components/editable-cell/types/EditableDoubleText';
import { useOpenCommentRightDrawer } from '../../comments/hooks/useOpenCommentRightDrawer';
import { usePeopleCommentsCountQuery } from '../../comments/services';
import { PersonChip } from './PersonChip';
@ -31,6 +31,7 @@ export function EditablePeopleFullName({
}: OwnProps) {
const [firstnameValue, setFirstnameValue] = useState(firstname);
const [lastnameValue, setLastnameValue] = useState(lastname);
const openCommentRightDrawer = useOpenCommentRightDrawer();
function handleDoubleTextChange(
firstValue: string,
@ -42,14 +43,13 @@ export function EditablePeopleFullName({
onChange(firstValue, secondValue);
}
const openCommentRightDrawer = useOpenCommentRightDrawer();
function handleCommentClick(event: React.MouseEvent<HTMLDivElement>) {
event.preventDefault();
event.stopPropagation();
openCommentRightDrawer([
{
type: 'Company',
type: 'Person',
id: personId,
},
]);
@ -57,6 +57,8 @@ export function EditablePeopleFullName({
const commentCount = usePeopleCommentsCountQuery(personId);
const displayCommentCount = !commentCount.loading;
return (
<EditableDoubleText
firstValue={firstnameValue}
@ -69,9 +71,9 @@ export function EditablePeopleFullName({
<StyledDiv>
<PersonChip name={firstname + ' ' + lastname} />
</StyledDiv>
{commentCount.loading ? null : (
{displayCommentCount && (
<CellCommentChip
count={commentCount.data || 0}
count={commentCount.data ?? 0}
onClick={handleCommentClick}
/>
)}

View File

@ -30,10 +30,9 @@ const MainContainer = styled.div`
background: ${(props) => props.theme.noisyBackground};
padding-right: ${(props) => props.theme.spacing(3)};
padding-bottom: ${(props) => props.theme.spacing(3)};
gap: ${(props) => props.theme.spacing(2)};
`;
const RIGHT_DRAWER_WIDTH = '300px';
type LeftContainerProps = {
isRightDrawerOpen?: boolean;
};
@ -41,7 +40,11 @@ type LeftContainerProps = {
const LeftContainer = styled.div<LeftContainerProps>`
display: flex;
width: calc(
100% - ${(props) => (props.isRightDrawerOpen ? RIGHT_DRAWER_WIDTH : '0px')}
100% -
${(props) =>
props.isRightDrawerOpen
? `${props.theme.rightDrawerWidth} - ${props.theme.spacing(2)}`
: '0px'}
);
position: relative;
`;

View File

@ -12,8 +12,7 @@ import { RightDrawerRouter } from './RightDrawerRouter';
const StyledRightDrawer = styled.div`
display: flex;
flex-direction: row;
width: 300px;
margin-left: ${(props) => props.theme.spacing(2)};
width: ${(props) => props.theme.rightDrawerWidth};
`;
export function RightDrawer() {

View File

@ -3,6 +3,4 @@ import styled from '@emotion/styled';
export const RightDrawerBody = styled.div`
display: flex;
flex-direction: column;
padding: 8px;
`;

View File

@ -23,6 +23,8 @@ const commonTheme = {
},
borderRadius: '4px',
rightDrawerWidth: '300px',
};
const lightThemeSpecific = {
@ -38,6 +40,7 @@ const lightThemeSpecific = {
purpleBackground: '#e0e0ff',
yellowBackground: '#fff2e7',
primaryBackgroundTransparent: 'rgba(255, 255, 255, 0.8)',
secondaryBackgroundTransparent: 'rgba(252, 252, 252, 0.8)',
primaryBorder: 'rgba(0, 0, 0, 0.08)',
@ -77,6 +80,7 @@ const darkThemeSpecific: typeof lightThemeSpecific = {
purpleBackground: '#1111b7',
yellowBackground: '#cc660a',
primaryBackgroundTransparent: 'rgba(20, 20, 20, 0.8)',
secondaryBackgroundTransparent: 'rgba(23, 23, 23, 0.8)',
clickableElementBackgroundHover: 'rgba(0, 0, 0, 0.04)',

View File

@ -0,0 +1,61 @@
import styled from '@emotion/styled';
import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
type OwnProps = {
avatarUrl: string | null | undefined;
size: number;
placeholderLetter: string;
};
export const StyledUserAvatar = styled.div<Omit<OwnProps, 'placeholderLetter'>>`
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
border-radius: 50%;
background-image: url(${(props) =>
isNonEmptyString(props.avatarUrl) ? props.avatarUrl : 'none'});
background-color: ${(props) =>
!isNonEmptyString(props.avatarUrl)
? props.theme.tertiaryBackground
: 'none'};
background-repeat: no-repeat;
background-position: center center;
background-size: cover;
display: flex;
align-items: center;
justify-content: center;
`;
type StyledPlaceholderLetterProps = {
size: number;
};
export const StyledPlaceholderLetter = styled.div<StyledPlaceholderLetterProps>`
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
font-size: 12px;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
color: ${(props) => props.theme.text80};
`;
export function UserAvatar({ avatarUrl, size, placeholderLetter }: OwnProps) {
const noAvatarUrl = !isNonEmptyString(avatarUrl);
return (
<StyledUserAvatar avatarUrl={avatarUrl} size={size}>
{noAvatarUrl && (
<StyledPlaceholderLetter size={size}>
{placeholderLetter}
</StyledPlaceholderLetter>
)}
</StyledUserAvatar>
);
}

View File

@ -0,0 +1,40 @@
import type { Meta, StoryObj } from '@storybook/react';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { UserAvatar } from '../UserAvatar';
const meta: Meta<typeof UserAvatar> = {
title: 'Components/User/UserAvatar',
component: UserAvatar,
};
export default meta;
type Story = StoryObj<typeof UserAvatar>;
const avatarUrl =
'https://s3-alpha-sig.figma.com/img/bbb5/4905/f0a52cc2b9aaeb0a82a360d478dae8bf?Expires=1687132800&Signature=iVBr0BADa3LHoFVGbwqO-wxC51n1o~ZyFD-w7nyTyFP4yB-Y6zFawL-igewaFf6PrlumCyMJThDLAAc-s-Cu35SBL8BjzLQ6HymzCXbrblUADMB208PnMAvc1EEUDq8TyryFjRO~GggLBk5yR0EXzZ3zenqnDEGEoQZR~TRqS~uDF-GwQB3eX~VdnuiU2iittWJkajIDmZtpN3yWtl4H630A3opQvBnVHZjXAL5YPkdh87-a-H~6FusWvvfJxfNC2ZzbrARzXofo8dUFtH7zUXGCC~eUk~hIuLbLuz024lFQOjiWq2VKyB7dQQuGFpM-OZQEV8tSfkViP8uzDLTaCg__&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4';
export const Size40: Story = {
render: getRenderWrapperForComponent(
<UserAvatar avatarUrl={avatarUrl} size={40} placeholderLetter="L" />,
),
};
export const Size32: Story = {
render: getRenderWrapperForComponent(
<UserAvatar avatarUrl={avatarUrl} size={32} placeholderLetter="L" />,
),
};
export const Size16: Story = {
render: getRenderWrapperForComponent(
<UserAvatar avatarUrl={avatarUrl} size={16} placeholderLetter="L" />,
),
};
export const NoAvatarPicture: Story = {
render: getRenderWrapperForComponent(
<UserAvatar avatarUrl={''} size={16} placeholderLetter="L" />,
),
};

View File

@ -13,6 +13,7 @@ describe('User mappers', () => {
const graphQLUser = {
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
displayName: 'John Doe',
avatarUrl: 'https://example.com/avatar.png',
email: 'john.doe@gmail.com',
workspaceMember: {
id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e88',
@ -31,6 +32,7 @@ describe('User mappers', () => {
__typename: 'users',
id: graphQLUser.id,
displayName: graphQLUser.displayName,
avatarUrl: graphQLUser.avatarUrl,
email: graphQLUser.email,
workspaceMember: {
id: graphQLUser.workspaceMember.id,
@ -51,6 +53,7 @@ describe('User mappers', () => {
__typename: 'users',
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
displayName: 'John Doe',
avatarUrl: 'https://example.com/avatar.png',
email: 'john.doe@gmail.com',
workspaceMember: {
id: '7af20dea-0412-4c4c-8b13-d6f0e6e09e88',
@ -65,6 +68,7 @@ describe('User mappers', () => {
expect(graphQLUser).toStrictEqual({
id: user.id,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
email: user.email,
workspaceMemberId: user.workspaceMember.id,
__typename: 'users',

View File

@ -9,6 +9,7 @@ export interface User {
id: string;
email?: string;
displayName?: string;
avatarUrl?: string;
workspaceMember?: WorkspaceMember | null;
}
@ -17,6 +18,7 @@ export type GraphqlQueryUser = {
email?: string;
displayName?: string;
workspaceMember?: GraphqlQueryWorkspaceMember | null;
avatarUrl?: string;
__typename?: string;
};
@ -24,6 +26,7 @@ export type GraphqlMutationUser = {
id: string;
email?: string;
displayName?: string;
avatarUrl?: string;
workspaceMemberId?: string;
__typename?: string;
};
@ -33,6 +36,7 @@ export const mapToUser = (user: GraphqlQueryUser): User => ({
id: user.id,
email: user.email,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
workspaceMember: user.workspaceMember
? mapToWorkspaceMember(user.workspaceMember)
: user.workspaceMember,
@ -42,6 +46,7 @@ export const mapToGqlUser = (user: User): GraphqlMutationUser => ({
id: user.id,
email: user.email,
displayName: user.displayName,
avatarUrl: user.avatarUrl,
workspaceMemberId: user.workspaceMember?.id,
__typename: 'users',
});

View File

@ -0,0 +1,155 @@
import { formatDistanceToNow } from 'date-fns';
import { DateTime } from 'luxon';
import { logError } from '@/utils/logs/logError';
import {
beautifyExactDate,
beautifyPastDateAbsolute,
beautifyPastDateRelativeToNow,
DEFAULT_DATE_LOCALE,
parseDate,
} from '../date-utils';
jest.mock('@/utils/logs/logError');
describe('beautifyExactDate', () => {
it('should return the correct relative date', () => {
const mockDate = '2023-01-01T12:13:24';
const actualDate = new Date(mockDate);
const expected = DateTime.fromJSDate(actualDate)
.toUTC()
.setLocale(DEFAULT_DATE_LOCALE)
.toFormat('DD · TT');
const result = beautifyExactDate(mockDate);
expect(result).toEqual(expected);
});
});
describe('parseDate', () => {
it('should log an error and return empty string when passed an invalid date string', () => {
expect(() => {
parseDate('invalid-date-string');
}).toThrow(
Error('Invalid date passed to formatPastDate: "invalid-date-string"'),
);
});
it('should log an error and return empty string when passed NaN', () => {
expect(() => {
parseDate(NaN);
}).toThrow(Error('Invalid date passed to formatPastDate: "NaN"'));
});
it('should log an error and return empty string when passed invalid Date object', () => {
expect(() => {
parseDate(new Date(NaN));
}).toThrow(Error('Invalid date passed to formatPastDate: "Invalid Date"'));
});
});
describe('beautifyPastDateRelativeToNow', () => {
it('should return the correct relative date', () => {
const mockDate = '2023-01-01';
const actualDate = new Date(mockDate);
const expected = formatDistanceToNow(actualDate);
const result = beautifyPastDateRelativeToNow(mockDate);
expect(result).toEqual(expected);
});
it('should log an error and return empty string when passed an invalid date string', () => {
const result = beautifyPastDateRelativeToNow('invalid-date-string');
expect(logError).toHaveBeenCalledWith(
Error('Invalid date passed to formatPastDate: "invalid-date-string"'),
);
expect(result).toEqual('');
});
it('should log an error and return empty string when passed NaN', () => {
const result = beautifyPastDateRelativeToNow(NaN);
expect(logError).toHaveBeenCalledWith(
Error('Invalid date passed to formatPastDate: "NaN"'),
);
expect(result).toEqual('');
});
it('should log an error and return empty string when passed invalid Date object', () => {
const result = beautifyPastDateRelativeToNow(
new Date('invalid-date-asdasd'),
);
expect(logError).toHaveBeenCalledWith(
Error('Invalid date passed to formatPastDate: "Invalid Date"'),
);
expect(result).toEqual('');
});
});
describe('beautifyPastDateAbsolute', () => {
it('should log an error and return empty string when passed an invalid date string', () => {
const result = beautifyPastDateAbsolute('invalid-date-string');
expect(logError).toHaveBeenCalledWith(
Error('Invalid date passed to formatPastDate: "invalid-date-string"'),
);
expect(result).toEqual('');
});
it('should log an error and return empty string when passed NaN', () => {
const result = beautifyPastDateAbsolute(NaN);
expect(logError).toHaveBeenCalledWith(
Error('Invalid date passed to formatPastDate: "NaN"'),
);
expect(result).toEqual('');
});
it('should log an error and return empty string when passed invalid Date object', () => {
const result = beautifyPastDateAbsolute(new Date(NaN));
expect(logError).toHaveBeenCalledWith(
Error('Invalid date passed to formatPastDate: "Invalid Date"'),
);
expect(result).toEqual('');
});
it('should return the correct format when the date difference is less than 24 hours', () => {
const now = DateTime.local();
const pastDate = now.minus({ hours: 23 });
const expected = pastDate.toFormat('HH:mm');
const result = beautifyPastDateAbsolute(pastDate.toJSDate());
expect(result).toEqual(expected);
});
it('should return the correct format when the date difference is less than 7 days', () => {
const now = DateTime.local();
const pastDate = now.minus({ days: 6 });
const expected = pastDate.toFormat('cccc - HH:mm');
const result = beautifyPastDateAbsolute(pastDate.toJSDate());
expect(result).toEqual(expected);
});
it('should return the correct format when the date difference is less than 365 days', () => {
const now = DateTime.local();
const pastDate = now.minus({ days: 364 });
const expected = pastDate.toFormat('MMMM d - HH:mm');
const result = beautifyPastDateAbsolute(pastDate.toJSDate());
expect(result).toEqual(expected);
});
it('should return the correct format when the date difference is more than 365 days', () => {
const now = DateTime.local();
const pastDate = now.minus({ days: 366 });
const expected = pastDate.toFormat('dd/MM/yyyy - HH:mm');
const result = beautifyPastDateAbsolute(pastDate.toJSDate());
expect(result).toEqual(expected);
});
});

View File

@ -0,0 +1,75 @@
import { formatDistanceToNow } from 'date-fns';
import { DateTime } from 'luxon';
import { logError } from '../logs/logError';
export const DEFAULT_DATE_LOCALE = 'en-EN';
export function parseDate(dateToParse: Date | string | number) {
let formattedDate: DateTime | null = null;
if (!dateToParse) {
throw new Error(`Invalid date passed to formatPastDate: "${dateToParse}"`);
} else if (typeof dateToParse === 'string') {
formattedDate = DateTime.fromISO(dateToParse);
} else if (dateToParse instanceof Date) {
formattedDate = DateTime.fromJSDate(dateToParse);
} else if (typeof dateToParse === 'number') {
formattedDate = DateTime.fromMillis(dateToParse);
}
if (!formattedDate) {
throw new Error(`Invalid date passed to formatPastDate: "${dateToParse}"`);
}
if (!formattedDate.isValid) {
throw new Error(`Invalid date passed to formatPastDate: "${dateToParse}"`);
}
return formattedDate.setLocale(DEFAULT_DATE_LOCALE);
}
export function beautifyExactDate(dateToBeautify: Date | string | number) {
try {
const parsedDate = parseDate(dateToBeautify);
return parsedDate.toFormat('DD · TT');
} catch (error) {
logError(error);
return '';
}
}
export function beautifyPastDateRelativeToNow(
pastDate: Date | string | number,
) {
try {
const parsedDate = parseDate(pastDate);
return formatDistanceToNow(parsedDate.toJSDate());
} catch (error) {
logError(error);
return '';
}
}
export function beautifyPastDateAbsolute(pastDate: Date | string | number) {
try {
const parsedPastDate = parseDate(pastDate);
const hoursDiff = parsedPastDate.diffNow('hours').negate().hours;
if (hoursDiff <= 24) {
return parsedPastDate.toFormat('HH:mm');
} else if (hoursDiff <= 7 * 24) {
return parsedPastDate.toFormat('cccc - HH:mm');
} else if (hoursDiff <= 365 * 24) {
return parsedPastDate.toFormat('MMMM d - HH:mm');
} else {
return parsedPastDate.toFormat('dd/MM/yyyy - HH:mm');
}
} catch (error) {
logError(error);
return '';
}
}

View File

@ -0,0 +1,3 @@
export function logError(message: any) {
console.error(message);
}

View File

@ -5,8 +5,8 @@ import { GraphqlQueryPerson } from '@/people/interfaces/person.interface';
import { GraphqlQueryUser } from '@/users/interfaces/user.interface';
import {
GetCompanyCountsQuery,
GetPeopleCountsQuery,
GetCompanyCommentsCountQuery,
GetPeopleCommentsCountQuery,
} from '../generated/graphql';
import { mockedCompaniesData } from './mock-data/companies';
@ -98,15 +98,15 @@ export const graphqlMocks = [
}),
);
}),
graphql.query('GetPeopleCounts', (req, res, ctx) => {
const mockedData: GetPeopleCountsQuery = {
graphql.query('GetPeopleCommentsCount', (req, res, ctx) => {
const mockedData: GetPeopleCommentsCountQuery = {
people: [{ commentsCount: 12 }],
};
return res(ctx.data(mockedData));
}),
graphql.query('GetCompanyCounts', (req, res, ctx) => {
const mockedData: GetCompanyCountsQuery = {
graphql.query('GetCompanyCommentsCount', (req, res, ctx) => {
const mockedData: GetCompanyCommentsCountQuery = {
companies: [{ commentsCount: 20 }],
};
return res(ctx.data(mockedData));

View File

@ -6,6 +6,8 @@ export const mockedUsersData: Array<GraphqlQueryUser> = [
__typename: 'User',
email: 'charles@test.com',
displayName: 'Charles Test',
avatarUrl:
'https://s3-alpha-sig.figma.com/img/bbb5/4905/f0a52cc2b9aaeb0a82a360d478dae8bf?Expires=1687132800&Signature=iVBr0BADa3LHoFVGbwqO-wxC51n1o~ZyFD-w7nyTyFP4yB-Y6zFawL-igewaFf6PrlumCyMJThDLAAc-s-Cu35SBL8BjzLQ6HymzCXbrblUADMB208PnMAvc1EEUDq8TyryFjRO~GggLBk5yR0EXzZ3zenqnDEGEoQZR~TRqS~uDF-GwQB3eX~VdnuiU2iittWJkajIDmZtpN3yWtl4H630A3opQvBnVHZjXAL5YPkdh87-a-H~6FusWvvfJxfNC2ZzbrARzXofo8dUFtH7zUXGCC~eUk~hIuLbLuz024lFQOjiWq2VKyB7dQQuGFpM-OZQEV8tSfkViP8uzDLTaCg__&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4',
workspaceMember: {
__typename: 'WorkspaceMember',
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',

View File

@ -8,6 +8,8 @@ export const seedUsers = async (prisma: PrismaClient) => {
displayName: 'Charles Bochet',
email: 'charles@test.com',
locale: 'en',
avatarUrl:
'https://s3-alpha-sig.figma.com/img/bbb5/4905/f0a52cc2b9aaeb0a82a360d478dae8bf?Expires=1687132800&Signature=iVBr0BADa3LHoFVGbwqO-wxC51n1o~ZyFD-w7nyTyFP4yB-Y6zFawL-igewaFf6PrlumCyMJThDLAAc-s-Cu35SBL8BjzLQ6HymzCXbrblUADMB208PnMAvc1EEUDq8TyryFjRO~GggLBk5yR0EXzZ3zenqnDEGEoQZR~TRqS~uDF-GwQB3eX~VdnuiU2iittWJkajIDmZtpN3yWtl4H630A3opQvBnVHZjXAL5YPkdh87-a-H~6FusWvvfJxfNC2ZzbrARzXofo8dUFtH7zUXGCC~eUk~hIuLbLuz024lFQOjiWq2VKyB7dQQuGFpM-OZQEV8tSfkViP8uzDLTaCg__&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4',
workspaceMember: {
create: {
id: 'twenty-7ef9d213-1c25-4d02-bf35-6aeccf7ea419',