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:
44
front/package-lock.json
generated
44
front/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
|
||||
|
||||
@ -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()}
|
||||
/>,
|
||||
),
|
||||
};
|
||||
@ -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: {
|
||||
|
||||
@ -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 };
|
||||
|
||||
5
front/src/modules/comments/types/CommentForDrawer.ts
Normal file
5
front/src/modules/comments/types/CommentForDrawer.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { CommentThreadForDrawer } from './CommentThreadForDrawer';
|
||||
|
||||
export type CommentForDrawer = NonNullable<
|
||||
CommentThreadForDrawer['comments']
|
||||
>[0];
|
||||
@ -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}
|
||||
/>
|
||||
),
|
||||
|
||||
@ -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',
|
||||
},
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -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;
|
||||
`;
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -3,6 +3,4 @@ import styled from '@emotion/styled';
|
||||
export const RightDrawerBody = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
padding: 8px;
|
||||
`;
|
||||
|
||||
@ -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)',
|
||||
|
||||
61
front/src/modules/users/components/UserAvatar.tsx
Normal file
61
front/src/modules/users/components/UserAvatar.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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" />,
|
||||
),
|
||||
};
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
});
|
||||
|
||||
155
front/src/modules/utils/datetime/__tests__/date-utils.test.ts
Normal file
155
front/src/modules/utils/datetime/__tests__/date-utils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
75
front/src/modules/utils/datetime/date-utils.ts
Normal file
75
front/src/modules/utils/datetime/date-utils.ts
Normal 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 '';
|
||||
}
|
||||
}
|
||||
3
front/src/modules/utils/logs/logError.ts
Normal file
3
front/src/modules/utils/logs/logError.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export function logError(message: any) {
|
||||
console.error(message);
|
||||
}
|
||||
@ -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));
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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',
|
||||
|
||||
Reference in New Issue
Block a user