Lucas/t 369 on comment drawer i can reply to a comment thread and it (#206)
* Added prisma to suggested extension in container * Added comments and authors on drawer with proper resolving * Fix lint * Fix console log * Fixed generated front graphql from rebase * 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 * wip * wip 2 * - Added string typing for DateTime scalar - Refactored user in a recoil state and workspace using it - Added comment creation * Prepared EditableCell refactor * Fixed line height and tooltip * Fix lint
This commit is contained in:
@ -1,16 +1,16 @@
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
|
||||
import { UserAvatar } from '@/users/components/UserAvatar';
|
||||
import {
|
||||
beautifyExactDate,
|
||||
beautifyPastDateRelativeToNow,
|
||||
} from '@/utils/datetime/date-utils';
|
||||
import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
|
||||
|
||||
type OwnProps = {
|
||||
avatarUrl: string | null | undefined;
|
||||
username: string;
|
||||
createdAt: Date;
|
||||
comment: Pick<CommentForDrawer, 'id' | 'author' | 'createdAt'>;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -44,15 +44,30 @@ const StyledDate = styled.div`
|
||||
|
||||
const StyledTooltip = styled(Tooltip)`
|
||||
padding: 8px;
|
||||
|
||||
opacity: 1;
|
||||
|
||||
background-color: ${(props) => props.theme.primaryBackground};
|
||||
|
||||
color: ${(props) => props.theme.text100};
|
||||
|
||||
box-shadow: 2px 4px 16px 6px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0px 2px 4px 3px rgba(0, 0, 0, 0.04);
|
||||
`;
|
||||
|
||||
export function CommentHeader({ avatarUrl, username, createdAt }: OwnProps) {
|
||||
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(createdAt);
|
||||
const exactCreatedAt = beautifyExactDate(createdAt);
|
||||
export function CommentHeader({ comment }: OwnProps) {
|
||||
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(comment.createdAt);
|
||||
const exactCreatedAt = beautifyExactDate(comment.createdAt);
|
||||
const showDate = beautifiedCreatedAt !== '';
|
||||
|
||||
const capitalizedFirstUsernameLetter =
|
||||
username !== '' ? username.toLocaleUpperCase()[0] : '';
|
||||
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>
|
||||
@ -61,14 +76,12 @@ export function CommentHeader({ avatarUrl, username, createdAt }: OwnProps) {
|
||||
size={16}
|
||||
placeholderLetter={capitalizedFirstUsernameLetter}
|
||||
/>
|
||||
<StyledName>{username}</StyledName>
|
||||
<StyledName>{authorName}</StyledName>
|
||||
{showDate && (
|
||||
<>
|
||||
<StyledDate className="comment-created-at">
|
||||
{beautifiedCreatedAt}
|
||||
</StyledDate>
|
||||
<StyledDate id={`id-${commentId}`}>{beautifiedCreatedAt}</StyledDate>
|
||||
<StyledTooltip
|
||||
anchorSelect=".comment-created-at"
|
||||
anchorSelect={`#id-${commentId}`}
|
||||
content={exactCreatedAt}
|
||||
clickable
|
||||
noArrow
|
||||
|
||||
@ -1,123 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { HotkeysEvent } from 'react-hotkeys-hook/dist/types';
|
||||
import { HiArrowSmRight } from 'react-icons/hi';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { IconButton } from '@/ui/components/buttons/IconButton';
|
||||
|
||||
type OwnProps = {
|
||||
onSend?: (text: string) => void;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
min-height: 32px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTextArea = styled(TextareaAutosize)`
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
font-weight: 400;
|
||||
line-height: 16px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
background: ${(props) => props.theme.tertiaryBackground};
|
||||
color: ${(props) => props.theme.text80};
|
||||
overflow: auto;
|
||||
resize: none;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: ${(props) => props.theme.text30};
|
||||
font-weight: 400;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledBottomRightIconButton = styled.div`
|
||||
width: 0px;
|
||||
position: relative;
|
||||
top: calc(100% - 26.5px);
|
||||
right: 26px;
|
||||
`;
|
||||
|
||||
export function CommentTextInput({ placeholder, onSend }: OwnProps) {
|
||||
const [text, setText] = useState('');
|
||||
|
||||
const isSendButtonDisabled = !text;
|
||||
|
||||
useHotkeys(
|
||||
['shift+enter', 'enter'],
|
||||
(event: KeyboardEvent, handler: HotkeysEvent) => {
|
||||
if (handler.shift) {
|
||||
return;
|
||||
} else {
|
||||
event.preventDefault();
|
||||
|
||||
onSend?.(text);
|
||||
|
||||
setText('');
|
||||
}
|
||||
},
|
||||
{
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[onSend],
|
||||
);
|
||||
|
||||
useHotkeys(
|
||||
'esc',
|
||||
(event: KeyboardEvent, handler: HotkeysEvent) => {
|
||||
event.preventDefault();
|
||||
|
||||
setText('');
|
||||
},
|
||||
{
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[onSend],
|
||||
);
|
||||
|
||||
function handleInputChange(event: React.FormEvent<HTMLTextAreaElement>) {
|
||||
const newText = event.currentTarget.value;
|
||||
|
||||
setText(newText);
|
||||
}
|
||||
|
||||
function handleOnClickSendButton() {
|
||||
onSend?.(text);
|
||||
|
||||
setText('');
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContainer>
|
||||
<StyledTextArea
|
||||
placeholder={placeholder || 'Write something...'}
|
||||
maxRows={5}
|
||||
onChange={handleInputChange}
|
||||
value={text}
|
||||
/>
|
||||
<StyledBottomRightIconButton>
|
||||
<IconButton
|
||||
onClick={handleOnClickSendButton}
|
||||
icon={<HiArrowSmRight size={15} />}
|
||||
disabled={isSendButtonDisabled}
|
||||
/>
|
||||
</StyledBottomRightIconButton>
|
||||
</StyledContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,8 +1,15 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
|
||||
import { AutosizeTextInput } from '@/ui/components/inputs/AutosizeTextInput';
|
||||
import { logError } from '@/utils/logs/logError';
|
||||
import { isDefined } from '@/utils/type-guards/isDefined';
|
||||
import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
|
||||
import { useCreateCommentMutation } from '~/generated/graphql';
|
||||
|
||||
import { CommentTextInput } from './CommentTextInput';
|
||||
import { CommentThreadItem } from './CommentThreadItem';
|
||||
|
||||
type OwnProps = {
|
||||
@ -21,17 +28,70 @@ const StyledContainer = styled.div`
|
||||
padding: ${(props) => props.theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledThreadItemListContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
|
||||
gap: ${(props) => props.theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export function CommentThread({ commentThread }: OwnProps) {
|
||||
function handleSendComment(text: string) {
|
||||
console.log(text);
|
||||
const [createCommentMutation] = useCreateCommentMutation();
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
|
||||
function handleSendComment(commentText: string) {
|
||||
if (!isDefined(currentUser)) {
|
||||
logError(
|
||||
'In handleSendComment, currentUser is not defined, this should not happen.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isNonEmptyString(commentText)) {
|
||||
logError(
|
||||
'In handleSendComment, trying to send empty text, this should not happen.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDefined(currentUser)) {
|
||||
createCommentMutation({
|
||||
variables: {
|
||||
commentId: v4(),
|
||||
authorId: currentUser.id,
|
||||
commentThreadId: commentThread.id,
|
||||
commentText,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
// TODO: find a way to have this configuration dynamic and typed
|
||||
refetchQueries: [
|
||||
'GetCommentThreadsByTargets',
|
||||
'GetPeopleCommentsCount',
|
||||
'GetCompanyCommentsCount',
|
||||
],
|
||||
onError: (error) => {
|
||||
logError(
|
||||
`In handleSendComment, createCommentMutation onError, error: ${error}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{commentThread.comments?.map((comment) => (
|
||||
<CommentThreadItem key={comment.id} comment={comment} />
|
||||
))}
|
||||
<CommentTextInput onSend={handleSendComment} />
|
||||
<StyledThreadItemListContainer>
|
||||
{commentThread.comments?.map((comment) => (
|
||||
<CommentThreadItem key={comment.id} comment={comment} />
|
||||
))}
|
||||
</StyledThreadItemListContainer>
|
||||
<AutosizeTextInput onSend={handleSendComment} />
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -18,22 +18,20 @@ const StyledContainer = styled.div`
|
||||
|
||||
const StyledCommentBody = styled.div`
|
||||
font-size: ${(props) => props.theme.fontSizeMedium};
|
||||
line-height: 19.5px;
|
||||
line-height: ${(props) => props.theme.lineHeight};
|
||||
|
||||
text-align: left;
|
||||
padding-left: 24px;
|
||||
|
||||
color: ${(props) => props.theme.text60};
|
||||
|
||||
overflow-wrap: anywhere;
|
||||
`;
|
||||
|
||||
export function CommentThreadItem({ comment }: OwnProps) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<CommentHeader
|
||||
avatarUrl={comment.author.avatarUrl}
|
||||
username={comment.author.displayName}
|
||||
createdAt={comment.createdAt}
|
||||
/>
|
||||
<CommentHeader comment={comment} />
|
||||
<StyledCommentBody>{comment.body}</StyledCommentBody>
|
||||
</StyledContainer>
|
||||
);
|
||||
|
||||
@ -29,7 +29,7 @@ export function RightDrawerComments() {
|
||||
<RightDrawerTopBar title="Comments" />
|
||||
<RightDrawerBody>
|
||||
{commentThreads.map((commentThread) => (
|
||||
<CommentThread commentThread={commentThread} />
|
||||
<CommentThread key={commentThread.id} commentThread={commentThread} />
|
||||
))}
|
||||
</RightDrawerBody>
|
||||
</RightDrawerPage>
|
||||
|
||||
@ -17,9 +17,14 @@ type Story = StoryObj<typeof CellCommentChip>;
|
||||
const TestCellContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
justify-content: flex-start;
|
||||
|
||||
min-width: 250px;
|
||||
max-width: 250px;
|
||||
|
||||
text-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
height: fit-content;
|
||||
|
||||
background: ${(props) => props.theme.primaryBackground};
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
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';
|
||||
|
||||
@ -16,12 +18,23 @@ type Story = StoryObj<typeof CommentHeader>;
|
||||
|
||||
const mockUser = mockedUsersData[0];
|
||||
|
||||
const mockComment: Pick<CommentForDrawer, 'id' | 'author' | 'createdAt'> = {
|
||||
id: v4(),
|
||||
author: {
|
||||
id: v4(),
|
||||
displayName: mockUser.displayName ?? '',
|
||||
avatarUrl: mockUser.avatarUrl,
|
||||
},
|
||||
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
|
||||
};
|
||||
|
||||
export const Default: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<CommentHeader
|
||||
avatarUrl={mockUser.avatarUrl ?? ''}
|
||||
username={mockUser.displayName ?? ''}
|
||||
createdAt={DateTime.now().minus({ hours: 2 }).toJSDate()}
|
||||
comment={{
|
||||
...mockComment,
|
||||
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
};
|
||||
@ -29,9 +42,10 @@ export const Default: Story = {
|
||||
export const FewDaysAgo: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<CommentHeader
|
||||
avatarUrl={mockUser.avatarUrl ?? ''}
|
||||
username={mockUser.displayName ?? ''}
|
||||
createdAt={DateTime.now().minus({ days: 2 }).toJSDate()}
|
||||
comment={{
|
||||
...mockComment,
|
||||
createdAt: DateTime.now().minus({ days: 2 }).toISO() ?? '',
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
};
|
||||
@ -39,9 +53,10 @@ export const FewDaysAgo: Story = {
|
||||
export const FewMonthsAgo: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<CommentHeader
|
||||
avatarUrl={mockUser.avatarUrl ?? ''}
|
||||
username={mockUser.displayName ?? ''}
|
||||
createdAt={DateTime.now().minus({ months: 2 }).toJSDate()}
|
||||
comment={{
|
||||
...mockComment,
|
||||
createdAt: DateTime.now().minus({ months: 2 }).toISO() ?? '',
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
};
|
||||
@ -49,9 +64,10 @@ export const FewMonthsAgo: Story = {
|
||||
export const FewYearsAgo: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<CommentHeader
|
||||
avatarUrl={mockUser.avatarUrl ?? ''}
|
||||
username={mockUser.displayName ?? ''}
|
||||
createdAt={DateTime.now().minus({ years: 2 }).toJSDate()}
|
||||
comment={{
|
||||
...mockComment,
|
||||
createdAt: DateTime.now().minus({ years: 2 }).toISO() ?? '',
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
};
|
||||
@ -59,9 +75,14 @@ export const FewYearsAgo: Story = {
|
||||
export const WithoutAvatar: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<CommentHeader
|
||||
avatarUrl={''}
|
||||
username={mockUser.displayName ?? ''}
|
||||
createdAt={DateTime.now().minus({ hours: 2 }).toJSDate()}
|
||||
comment={{
|
||||
...mockComment,
|
||||
author: {
|
||||
...mockComment.author,
|
||||
avatarUrl: '',
|
||||
},
|
||||
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
|
||||
}}
|
||||
/>,
|
||||
),
|
||||
};
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
|
||||
import { CommentTextInput } from '../CommentTextInput';
|
||||
|
||||
const meta: Meta<typeof CommentTextInput> = {
|
||||
title: 'Components/Comments/CommentTextInput',
|
||||
component: CommentTextInput,
|
||||
argTypes: {
|
||||
onSend: {
|
||||
action: 'onSend',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CommentTextInput>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: getRenderWrapperForComponent(<CommentTextInput />),
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
actions: { argTypesRegex: '^on.*' },
|
||||
},
|
||||
args: {
|
||||
onSend: (text: string) => {
|
||||
console.log(text);
|
||||
},
|
||||
},
|
||||
};
|
||||
31
front/src/modules/comments/services/create.ts
Normal file
31
front/src/modules/comments/services/create.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_COMMENT = gql`
|
||||
mutation CreateComment(
|
||||
$commentId: String!
|
||||
$commentText: String!
|
||||
$authorId: String!
|
||||
$commentThreadId: String!
|
||||
$createdAt: DateTime!
|
||||
) {
|
||||
createOneComment(
|
||||
data: {
|
||||
id: $commentId
|
||||
createdAt: $createdAt
|
||||
body: $commentText
|
||||
author: { connect: { id: $authorId } }
|
||||
commentThread: { connect: { id: $commentThreadId } }
|
||||
}
|
||||
) {
|
||||
id
|
||||
createdAt
|
||||
body
|
||||
author {
|
||||
id
|
||||
displayName
|
||||
avatarUrl
|
||||
}
|
||||
commentThreadId
|
||||
}
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user