From ce4ba10f7b52a794447f1adf9c0fd37bb1f323a8 Mon Sep 17 00:00:00 2001 From: Lucas Bordeau Date: Thu, 8 Jun 2023 10:36:37 +0200 Subject: [PATCH] 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 --- front/codegen.js | 3 + front/src/App.tsx | 43 +---------- front/src/__stories__/App.stories.tsx | 5 +- front/src/generated/graphql.tsx | 76 ++++++++++++++++--- front/src/index.tsx | 12 ++- .../modules/auth/states/currentUserState.ts | 8 ++ .../auth/states/isAuthenticatedState.ts | 11 +++ .../auth/states/isAuthenticatingState.ts | 6 ++ .../components/comments/CommentHeader.tsx | 39 ++++++---- .../components/comments/CommentThread.tsx | 74 ++++++++++++++++-- .../components/comments/CommentThreadItem.tsx | 10 +-- .../comments/RightDrawerComments.tsx | 2 +- .../__stories__/CommentChip.stories.tsx | 7 +- .../__stories__/CommentHeader.stories.tsx | 51 +++++++++---- .../__stories__/CommentTextInput.stories.tsx | 32 -------- front/src/modules/comments/services/create.ts | 31 ++++++++ .../components/CompanyEditableNameCell.tsx | 12 +-- .../modules/people/components/PersonChip.tsx | 2 + .../editable-cell/EditableCellDisplayMode.tsx | 2 +- .../components/inputs/AutosizeTextInput.tsx} | 10 +-- .../__stories__/AutosizeTextInput.stories.tsx | 22 ++++++ front/src/modules/ui/layout/AppLayout.tsx | 12 +-- front/src/modules/ui/layout/navbar/Navbar.tsx | 12 +-- .../ui/layout/navbar/WorkspaceContainer.tsx | 21 +++-- .../ui/layout/states/themeEnabledState.ts | 6 ++ front/src/modules/ui/layout/styles/themes.ts | 1 + .../interfaces/workspace.interface.ts | 1 + .../auth/{Callback.tsx => AuthCallback.tsx} | 4 +- front/src/providers/AppThemeProvider.tsx | 14 ++++ front/src/providers/AuthProvider.tsx | 29 +++++++ .../comment-thread-relations.resolver.ts | 4 + 31 files changed, 395 insertions(+), 167 deletions(-) create mode 100644 front/src/modules/auth/states/currentUserState.ts create mode 100644 front/src/modules/auth/states/isAuthenticatedState.ts create mode 100644 front/src/modules/auth/states/isAuthenticatingState.ts delete mode 100644 front/src/modules/comments/components/comments/__stories__/CommentTextInput.stories.tsx create mode 100644 front/src/modules/comments/services/create.ts rename front/src/modules/{comments/components/comments/CommentTextInput.tsx => ui/components/inputs/AutosizeTextInput.tsx} (91%) create mode 100644 front/src/modules/ui/components/inputs/__stories__/AutosizeTextInput.stories.tsx create mode 100644 front/src/modules/ui/layout/states/themeEnabledState.ts rename front/src/pages/auth/{Callback.tsx => AuthCallback.tsx} (93%) create mode 100644 front/src/providers/AppThemeProvider.tsx create mode 100644 front/src/providers/AuthProvider.tsx diff --git a/front/codegen.js b/front/codegen.js index 4f4b6676b..3d4151371 100644 --- a/front/codegen.js +++ b/front/codegen.js @@ -14,6 +14,9 @@ module.exports = { withHooks: true, withHOC: false, withComponent: false, + scalars: { + DateTime: 'string', + } }, }, }, diff --git a/front/src/App.tsx b/front/src/App.tsx index 6367b0bd2..79d7ba152 100644 --- a/front/src/App.tsx +++ b/front/src/App.tsx @@ -1,43 +1,18 @@ -import React, { useEffect, useState } from 'react'; import { Navigate, Route, Routes } from 'react-router-dom'; -import { ThemeProvider } from '@emotion/react'; - -import { browserPrefersDarkMode } from '@/utils/utils'; import { RequireAuth } from './modules/auth/components/RequireAuth'; -import { getUserIdFromToken } from './modules/auth/services/AuthService'; import { AppLayout } from './modules/ui/layout/AppLayout'; -import { darkTheme, lightTheme } from './modules/ui/layout/styles/themes'; -import { mapToUser, User } from './modules/users/interfaces/user.interface'; -import { useGetCurrentUserQuery } from './modules/users/services'; -import AuthCallback from './pages/auth/Callback'; +import { AuthCallback } from './pages/auth/AuthCallback'; import { Login } from './pages/auth/Login'; import { Companies } from './pages/companies/Companies'; import { Opportunities } from './pages/opportunities/Opportunities'; import { People } from './pages/people/People'; -type AppProps = { - themeEnabled?: boolean; -}; - -export function App({ themeEnabled = true }: AppProps) { - const [user, setUser] = useState(undefined); - - const userIdFromToken = getUserIdFromToken(); - const { data } = useGetCurrentUserQuery(userIdFromToken); - - useEffect(() => { - if (data?.users[0]) { - setUser(mapToUser(data?.users[0])); - } - }, [data]); - - const defaultTheme = browserPrefersDarkMode() ? darkTheme : lightTheme; - - const app = ( +export function App() { + return ( <> { - + ); - - return ( - <> - {themeEnabled ? ( - {app} - ) : ( - app - )} - - ); } diff --git a/front/src/__stories__/App.stories.tsx b/front/src/__stories__/App.stories.tsx index 54acb3b70..36cddc0c2 100644 --- a/front/src/__stories__/App.stories.tsx +++ b/front/src/__stories__/App.stories.tsx @@ -4,6 +4,7 @@ import type { Meta, StoryObj } from '@storybook/react'; import { RecoilRoot } from 'recoil'; import { App } from '~/App'; +import { AuthProvider } from '~/providers/AuthProvider'; import { FullHeightStorybookLayout } from '~/testing/FullHeightStorybookLayout'; import { graphqlMocks } from '~/testing/graphqlMocks'; import { mockedUserJWT } from '~/testing/mock-data/jwt'; @@ -22,7 +23,9 @@ const render = () => ( - + + + diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 5e1cc0804..755d7d39b 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -13,7 +13,7 @@ export type Scalars = { Boolean: boolean; Int: number; Float: number; - DateTime: any; + DateTime: string; JSON: any; }; @@ -1081,6 +1081,17 @@ export type WorkspaceMember = { workspace: Workspace; }; +export type CreateCommentMutationVariables = Exact<{ + commentId: Scalars['String']; + commentText: Scalars['String']; + authorId: Scalars['String']; + commentThreadId: Scalars['String']; + createdAt: Scalars['DateTime']; +}>; + + +export type CreateCommentMutation = { __typename?: 'Mutation', createOneComment: { __typename?: 'Comment', id: string, createdAt: string, body: string, commentThreadId: string, author: { __typename?: 'User', id: string, displayName: string, avatarUrl?: string | null } } }; + export type GetCompanyCommentsCountQueryVariables = Exact<{ where?: InputMaybe; }>; @@ -1100,7 +1111,7 @@ export type GetCommentThreadsByTargetsQueryVariables = Exact<{ }>; -export type GetCommentThreadsByTargetsQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: any, updatedAt: any, author: { __typename?: 'User', id: string, displayName: string, avatarUrl?: string | null } }> | null }> }; +export type GetCommentThreadsByTargetsQuery = { __typename?: 'Query', findManyCommentThreads: Array<{ __typename?: 'CommentThread', id: string, comments?: Array<{ __typename?: 'Comment', id: string, body: string, createdAt: string, updatedAt: string, author: { __typename?: 'User', id: string, displayName: string, avatarUrl?: string | null } }> | null }> }; export type GetCompaniesQueryVariables = Exact<{ orderBy?: InputMaybe | CompanyOrderByWithRelationInput>; @@ -1108,7 +1119,7 @@ export type GetCompaniesQueryVariables = Exact<{ }>; -export type GetCompaniesQuery = { __typename?: 'Query', companies: Array<{ __typename?: 'Company', id: string, domainName: string, name: string, createdAt: any, address: string, employees?: number | null, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string } | null }> }; +export type GetCompaniesQuery = { __typename?: 'Query', companies: Array<{ __typename?: 'Company', id: string, domainName: string, name: string, createdAt: string, address: string, employees?: number | null, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string } | null }> }; export type UpdateCompanyMutationVariables = Exact<{ id?: InputMaybe; @@ -1121,7 +1132,7 @@ export type UpdateCompanyMutationVariables = Exact<{ }>; -export type UpdateCompanyMutation = { __typename?: 'Mutation', updateOneCompany?: { __typename?: 'Company', address: string, createdAt: any, domainName: string, employees?: number | null, id: string, name: string, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string } | null } | null }; +export type UpdateCompanyMutation = { __typename?: 'Mutation', updateOneCompany?: { __typename?: 'Company', address: string, createdAt: string, domainName: string, employees?: number | null, id: string, name: string, accountOwner?: { __typename?: 'User', id: string, email: string, displayName: string } | null } | null }; export type InsertCompanyMutationVariables = Exact<{ id: Scalars['String']; @@ -1133,7 +1144,7 @@ export type InsertCompanyMutationVariables = Exact<{ }>; -export type InsertCompanyMutation = { __typename?: 'Mutation', createOneCompany: { __typename?: 'Company', address: string, createdAt: any, domainName: string, employees?: number | null, id: string, name: string } }; +export type InsertCompanyMutation = { __typename?: 'Mutation', createOneCompany: { __typename?: 'Company', address: string, createdAt: string, domainName: string, employees?: number | null, id: string, name: string } }; export type DeleteCompaniesMutationVariables = Exact<{ ids?: InputMaybe | Scalars['String']>; @@ -1149,7 +1160,7 @@ export type GetPeopleQueryVariables = Exact<{ }>; -export type GetPeopleQuery = { __typename?: 'Query', people: Array<{ __typename?: 'Person', id: string, phone: string, email: string, city: string, firstname: string, lastname: string, createdAt: any, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null }> }; +export type GetPeopleQuery = { __typename?: 'Query', people: Array<{ __typename?: 'Person', id: string, phone: string, email: string, city: string, firstname: string, lastname: string, createdAt: string, company?: { __typename?: 'Company', id: string, name: string, domainName: string } | null }> }; export type UpdatePeopleMutationVariables = Exact<{ id?: InputMaybe; @@ -1163,7 +1174,7 @@ export type UpdatePeopleMutationVariables = Exact<{ }>; -export type UpdatePeopleMutation = { __typename?: 'Mutation', updateOnePerson?: { __typename?: 'Person', city: string, email: string, firstname: string, id: string, lastname: string, phone: string, createdAt: any, company?: { __typename?: 'Company', domainName: string, name: string, id: string } | null } | null }; +export type UpdatePeopleMutation = { __typename?: 'Mutation', updateOnePerson?: { __typename?: 'Person', city: string, email: string, firstname: string, id: string, lastname: string, phone: string, createdAt: string, company?: { __typename?: 'Company', domainName: string, name: string, id: string } | null } | null }; export type InsertPersonMutationVariables = Exact<{ id: Scalars['String']; @@ -1176,7 +1187,7 @@ export type InsertPersonMutationVariables = Exact<{ }>; -export type InsertPersonMutation = { __typename?: 'Mutation', createOnePerson: { __typename?: 'Person', city: string, email: string, firstname: string, id: string, lastname: string, phone: string, createdAt: any, company?: { __typename?: 'Company', domainName: string, name: string, id: string } | null } }; +export type InsertPersonMutation = { __typename?: 'Mutation', createOnePerson: { __typename?: 'Person', city: string, email: string, firstname: string, id: string, lastname: string, phone: string, createdAt: string, company?: { __typename?: 'Company', domainName: string, name: string, id: string } | null } }; export type DeletePeopleMutationVariables = Exact<{ ids?: InputMaybe | Scalars['String']>; @@ -1191,7 +1202,7 @@ export type SearchPeopleQueryQueryVariables = Exact<{ }>; -export type SearchPeopleQueryQuery = { __typename?: 'Query', searchResults: Array<{ __typename?: 'Person', id: string, phone: string, email: string, city: string, firstname: string, lastname: string, createdAt: any }> }; +export type SearchPeopleQueryQuery = { __typename?: 'Query', searchResults: Array<{ __typename?: 'Person', id: string, phone: string, email: string, city: string, firstname: string, lastname: string, createdAt: string }> }; export type SearchUserQueryQueryVariables = Exact<{ where?: InputMaybe; @@ -1227,6 +1238,53 @@ export type GetUsersQueryVariables = Exact<{ [key: string]: never; }>; export type GetUsersQuery = { __typename?: 'Query', findManyUser: Array<{ __typename?: 'User', id: string }> }; +export const CreateCommentDocument = 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 + } +} + `; +export type CreateCommentMutationFn = Apollo.MutationFunction; + +/** + * __useCreateCommentMutation__ + * + * To run a mutation, you first call `useCreateCommentMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateCommentMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [createCommentMutation, { data, loading, error }] = useCreateCommentMutation({ + * variables: { + * commentId: // value for 'commentId' + * commentText: // value for 'commentText' + * authorId: // value for 'authorId' + * commentThreadId: // value for 'commentThreadId' + * createdAt: // value for 'createdAt' + * }, + * }); + */ +export function useCreateCommentMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateCommentDocument, options); + } +export type CreateCommentMutationHookResult = ReturnType; +export type CreateCommentMutationResult = Apollo.MutationResult; +export type CreateCommentMutationOptions = Apollo.BaseMutationOptions; export const GetCompanyCommentsCountDocument = gql` query GetCompanyCommentsCount($where: CompanyWhereInput) { companies: findManyCompany(where: $where) { diff --git a/front/src/index.tsx b/front/src/index.tsx index 7a2d75f1a..974e72ea9 100644 --- a/front/src/index.tsx +++ b/front/src/index.tsx @@ -7,6 +7,8 @@ import { RecoilRoot } from 'recoil'; import '@emotion/react'; import { ThemeType } from './modules/ui/layout/styles/themes'; +import { AppThemeProvider } from './providers/AppThemeProvider'; +import { AuthProvider } from './providers/AuthProvider'; import { apiClient } from './apollo'; import { App } from './App'; @@ -19,9 +21,13 @@ root.render( - - - + + + + + + + , diff --git a/front/src/modules/auth/states/currentUserState.ts b/front/src/modules/auth/states/currentUserState.ts new file mode 100644 index 000000000..e3f405e79 --- /dev/null +++ b/front/src/modules/auth/states/currentUserState.ts @@ -0,0 +1,8 @@ +import { atom } from 'recoil'; + +import { User } from '@/users/interfaces/user.interface'; + +export const currentUserState = atom({ + key: 'auth/current-user', + default: null, +}); diff --git a/front/src/modules/auth/states/isAuthenticatedState.ts b/front/src/modules/auth/states/isAuthenticatedState.ts new file mode 100644 index 000000000..96c8dbe9c --- /dev/null +++ b/front/src/modules/auth/states/isAuthenticatedState.ts @@ -0,0 +1,11 @@ +import { selector } from 'recoil'; + +import { currentUserState } from './currentUserState'; + +export const isAuthenticatedState = selector({ + key: 'auth/is-authenticated', + get: ({ get }) => { + const user = get(currentUserState); + return !!user; + }, +}); diff --git a/front/src/modules/auth/states/isAuthenticatingState.ts b/front/src/modules/auth/states/isAuthenticatingState.ts new file mode 100644 index 000000000..3935f804f --- /dev/null +++ b/front/src/modules/auth/states/isAuthenticatingState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const isAuthenticatingState = atom({ + key: 'auth/is-authenticating', + default: true, +}); diff --git a/front/src/modules/comments/components/comments/CommentHeader.tsx b/front/src/modules/comments/components/comments/CommentHeader.tsx index b8612075a..7a761f0ce 100644 --- a/front/src/modules/comments/components/comments/CommentHeader.tsx +++ b/front/src/modules/comments/components/comments/CommentHeader.tsx @@ -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; }; 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 ( @@ -61,14 +76,12 @@ export function CommentHeader({ avatarUrl, username, createdAt }: OwnProps) { size={16} placeholderLetter={capitalizedFirstUsernameLetter} /> - {username} + {authorName} {showDate && ( <> - - {beautifiedCreatedAt} - + {beautifiedCreatedAt} 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 ( - {commentThread.comments?.map((comment) => ( - - ))} - + + {commentThread.comments?.map((comment) => ( + + ))} + + ); } diff --git a/front/src/modules/comments/components/comments/CommentThreadItem.tsx b/front/src/modules/comments/components/comments/CommentThreadItem.tsx index 6718f9bd7..a77b57972 100644 --- a/front/src/modules/comments/components/comments/CommentThreadItem.tsx +++ b/front/src/modules/comments/components/comments/CommentThreadItem.tsx @@ -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 ( - + {comment.body} ); diff --git a/front/src/modules/comments/components/comments/RightDrawerComments.tsx b/front/src/modules/comments/components/comments/RightDrawerComments.tsx index 6bb17746e..0596e9f1d 100644 --- a/front/src/modules/comments/components/comments/RightDrawerComments.tsx +++ b/front/src/modules/comments/components/comments/RightDrawerComments.tsx @@ -29,7 +29,7 @@ export function RightDrawerComments() { {commentThreads.map((commentThread) => ( - + ))} diff --git a/front/src/modules/comments/components/comments/__stories__/CommentChip.stories.tsx b/front/src/modules/comments/components/comments/__stories__/CommentChip.stories.tsx index 85015dbb5..2d73ee47f 100644 --- a/front/src/modules/comments/components/comments/__stories__/CommentChip.stories.tsx +++ b/front/src/modules/comments/components/comments/__stories__/CommentChip.stories.tsx @@ -17,9 +17,14 @@ type Story = StoryObj; 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}; diff --git a/front/src/modules/comments/components/comments/__stories__/CommentHeader.stories.tsx b/front/src/modules/comments/components/comments/__stories__/CommentHeader.stories.tsx index bc6c9add7..43baf59f6 100644 --- a/front/src/modules/comments/components/comments/__stories__/CommentHeader.stories.tsx +++ b/front/src/modules/comments/components/comments/__stories__/CommentHeader.stories.tsx @@ -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; const mockUser = mockedUsersData[0]; +const mockComment: Pick = { + id: v4(), + author: { + id: v4(), + displayName: mockUser.displayName ?? '', + avatarUrl: mockUser.avatarUrl, + }, + createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '', +}; + export const Default: Story = { render: getRenderWrapperForComponent( , ), }; @@ -29,9 +42,10 @@ export const Default: Story = { export const FewDaysAgo: Story = { render: getRenderWrapperForComponent( , ), }; @@ -39,9 +53,10 @@ export const FewDaysAgo: Story = { export const FewMonthsAgo: Story = { render: getRenderWrapperForComponent( , ), }; @@ -49,9 +64,10 @@ export const FewMonthsAgo: Story = { export const FewYearsAgo: Story = { render: getRenderWrapperForComponent( , ), }; @@ -59,9 +75,14 @@ export const FewYearsAgo: Story = { export const WithoutAvatar: Story = { render: getRenderWrapperForComponent( , ), }; diff --git a/front/src/modules/comments/components/comments/__stories__/CommentTextInput.stories.tsx b/front/src/modules/comments/components/comments/__stories__/CommentTextInput.stories.tsx deleted file mode 100644 index 1b7239bb6..000000000 --- a/front/src/modules/comments/components/comments/__stories__/CommentTextInput.stories.tsx +++ /dev/null @@ -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 = { - title: 'Components/Comments/CommentTextInput', - component: CommentTextInput, - argTypes: { - onSend: { - action: 'onSend', - }, - }, -}; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - render: getRenderWrapperForComponent(), - parameters: { - msw: graphqlMocks, - actions: { argTypesRegex: '^on.*' }, - }, - args: { - onSend: (text: string) => { - console.log(text); - }, - }, -}; diff --git a/front/src/modules/comments/services/create.ts b/front/src/modules/comments/services/create.ts new file mode 100644 index 000000000..fac426d2a --- /dev/null +++ b/front/src/modules/comments/services/create.ts @@ -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 + } + } +`; diff --git a/front/src/modules/companies/components/CompanyEditableNameCell.tsx b/front/src/modules/companies/components/CompanyEditableNameCell.tsx index bfd80f8d5..e3723ac66 100644 --- a/front/src/modules/companies/components/CompanyEditableNameCell.tsx +++ b/front/src/modules/companies/components/CompanyEditableNameCell.tsx @@ -30,8 +30,6 @@ export function CompanyEditableNameChipCell({ company }: OwnProps) { const commentCount = useCompanyCommentsCountQuery(company.id); - const displayCommentCount = !commentCount.loading; - return ( - ), + , ]} /> ); diff --git a/front/src/modules/people/components/PersonChip.tsx b/front/src/modules/people/components/PersonChip.tsx index b5a997ca6..a6a8daee1 100644 --- a/front/src/modules/people/components/PersonChip.tsx +++ b/front/src/modules/people/components/PersonChip.tsx @@ -30,6 +30,8 @@ const StyledContainer = styled.span` border-radius: 100%; object-fit: cover; } + + height: 12px; `; export function PersonChip({ name, picture }: PersonChipPropsType) { diff --git a/front/src/modules/ui/components/editable-cell/EditableCellDisplayMode.tsx b/front/src/modules/ui/components/editable-cell/EditableCellDisplayMode.tsx index 3f2efe1b3..1f24e4937 100644 --- a/front/src/modules/ui/components/editable-cell/EditableCellDisplayMode.tsx +++ b/front/src/modules/ui/components/editable-cell/EditableCellDisplayMode.tsx @@ -9,7 +9,7 @@ export const EditableCellNormalModeOuterContainer = styled.div` overflow: hidden; padding-left: ${(props) => props.theme.spacing(2)}; - padding-right: ${(props) => props.theme.spacing(2)}; + padding-right: ${(props) => props.theme.spacing(1)}; `; export const EditableCellNormalModeInnerContainer = styled.div` diff --git a/front/src/modules/comments/components/comments/CommentTextInput.tsx b/front/src/modules/ui/components/inputs/AutosizeTextInput.tsx similarity index 91% rename from front/src/modules/comments/components/comments/CommentTextInput.tsx rename to front/src/modules/ui/components/inputs/AutosizeTextInput.tsx index af6798007..7515f0d16 100644 --- a/front/src/modules/comments/components/comments/CommentTextInput.tsx +++ b/front/src/modules/ui/components/inputs/AutosizeTextInput.tsx @@ -5,7 +5,7 @@ import { HiArrowSmRight } from 'react-icons/hi'; import TextareaAutosize from 'react-textarea-autosize'; import styled from '@emotion/styled'; -import { IconButton } from '@/ui/components/buttons/IconButton'; +import { IconButton } from '../buttons/IconButton'; type OwnProps = { onSend?: (text: string) => void; @@ -50,7 +50,7 @@ const StyledBottomRightIconButton = styled.div` right: 26px; `; -export function CommentTextInput({ placeholder, onSend }: OwnProps) { +export function AutosizeTextInput({ placeholder, onSend }: OwnProps) { const [text, setText] = useState(''); const isSendButtonDisabled = !text; @@ -72,12 +72,12 @@ export function CommentTextInput({ placeholder, onSend }: OwnProps) { enableOnContentEditable: true, enableOnFormTags: true, }, - [onSend], + [onSend, text, setText], ); useHotkeys( 'esc', - (event: KeyboardEvent, handler: HotkeysEvent) => { + (event: KeyboardEvent) => { event.preventDefault(); setText(''); @@ -86,7 +86,7 @@ export function CommentTextInput({ placeholder, onSend }: OwnProps) { enableOnContentEditable: true, enableOnFormTags: true, }, - [onSend], + [onSend, setText], ); function handleInputChange(event: React.FormEvent) { diff --git a/front/src/modules/ui/components/inputs/__stories__/AutosizeTextInput.stories.tsx b/front/src/modules/ui/components/inputs/__stories__/AutosizeTextInput.stories.tsx new file mode 100644 index 000000000..c7aee098c --- /dev/null +++ b/front/src/modules/ui/components/inputs/__stories__/AutosizeTextInput.stories.tsx @@ -0,0 +1,22 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { getRenderWrapperForComponent } from '~/testing/renderWrappers'; + +import { AutosizeTextInput } from '../AutosizeTextInput'; + +const meta: Meta = { + title: 'Components/Common/AutosizeTextInput', + component: AutosizeTextInput, + argTypes: { + onSend: { + action: 'onSend', + }, + }, +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: getRenderWrapperForComponent(), +}; diff --git a/front/src/modules/ui/layout/AppLayout.tsx b/front/src/modules/ui/layout/AppLayout.tsx index 6976aeddd..fabded947 100644 --- a/front/src/modules/ui/layout/AppLayout.tsx +++ b/front/src/modules/ui/layout/AppLayout.tsx @@ -1,6 +1,7 @@ import styled from '@emotion/styled'; +import { useRecoilState } from 'recoil'; -import { User } from '@/users/interfaces/user.interface'; +import { currentUserState } from '@/auth/states/currentUserState'; import { Navbar } from './navbar/Navbar'; @@ -23,16 +24,17 @@ const MainContainer = styled.div` type OwnProps = { children: JSX.Element; - user?: User; }; -export function AppLayout({ children, user }: OwnProps) { - const userIsAuthenticated = !!user; +export function AppLayout({ children }: OwnProps) { + const currentUser = useRecoilState(currentUserState); + const userIsAuthenticated = !!currentUser; + return ( {userIsAuthenticated ? ( <> - + {children} ) : ( diff --git a/front/src/modules/ui/layout/navbar/Navbar.tsx b/front/src/modules/ui/layout/navbar/Navbar.tsx index 9a381aaf9..a227d1f5c 100644 --- a/front/src/modules/ui/layout/navbar/Navbar.tsx +++ b/front/src/modules/ui/layout/navbar/Navbar.tsx @@ -2,9 +2,6 @@ import { TbBuilding, TbUser } from 'react-icons/tb'; import { useMatch, useResolvedPath } from 'react-router-dom'; import styled from '@emotion/styled'; -import { User } from '@/users/interfaces/user.interface'; -import { Workspace } from '@/workspaces/interfaces/workspace.interface'; - import NavItem from './NavItem'; import NavTitle from './NavTitle'; import WorkspaceContainer from './WorkspaceContainer'; @@ -23,16 +20,11 @@ const NavItemsContainer = styled.div` margin-top: 40px; `; -type OwnProps = { - user?: User; - workspace?: Workspace; -}; - -export function Navbar({ workspace }: OwnProps) { +export function Navbar() { return ( <> - {workspace && } + props.theme.text80}; `; -function WorkspaceContainer({ workspace }: OwnProps) { +function WorkspaceContainer() { + const currentUser = useRecoilValue(currentUserState); + + const currentWorkspace = currentUser?.workspaceMember?.workspace; + + if (!currentWorkspace) { + return null; + } + return ( - - {workspace?.displayName} + + {currentWorkspace?.displayName} ); } diff --git a/front/src/modules/ui/layout/states/themeEnabledState.ts b/front/src/modules/ui/layout/states/themeEnabledState.ts new file mode 100644 index 000000000..5a819aa13 --- /dev/null +++ b/front/src/modules/ui/layout/states/themeEnabledState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const themeEnabledState = atom({ + key: 'ui/theme-enabled', + default: true, +}); diff --git a/front/src/modules/ui/layout/styles/themes.ts b/front/src/modules/ui/layout/styles/themes.ts index 10ae48900..95f365cb3 100644 --- a/front/src/modules/ui/layout/styles/themes.ts +++ b/front/src/modules/ui/layout/styles/themes.ts @@ -15,6 +15,7 @@ const commonTheme = { fontWeightBold: 500, fontFamily: 'Inter, sans-serif', + lineHeight: '150%', spacing: (multiplicator: number) => `${multiplicator * 4}px`, diff --git a/front/src/modules/workspaces/interfaces/workspace.interface.ts b/front/src/modules/workspaces/interfaces/workspace.interface.ts index 7371ff9b7..16384811f 100644 --- a/front/src/modules/workspaces/interfaces/workspace.interface.ts +++ b/front/src/modules/workspaces/interfaces/workspace.interface.ts @@ -3,6 +3,7 @@ export interface Workspace { domainName?: string; displayName?: string; logo?: string | null; + __typename?: string; } export type GraphqlQueryWorkspace = { diff --git a/front/src/pages/auth/Callback.tsx b/front/src/pages/auth/AuthCallback.tsx similarity index 93% rename from front/src/pages/auth/Callback.tsx rename to front/src/pages/auth/AuthCallback.tsx index 37a3d9cb0..630e9a112 100644 --- a/front/src/pages/auth/Callback.tsx +++ b/front/src/pages/auth/AuthCallback.tsx @@ -3,7 +3,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom'; import { refreshAccessToken } from '@/auth/services/AuthService'; -function Callback() { +export function AuthCallback() { const [searchParams] = useSearchParams(); const [isLoading, setIsLoading] = useState(true); @@ -25,5 +25,3 @@ function Callback() { return <>; } - -export default Callback; diff --git a/front/src/providers/AppThemeProvider.tsx b/front/src/providers/AppThemeProvider.tsx new file mode 100644 index 000000000..6efaf1601 --- /dev/null +++ b/front/src/providers/AppThemeProvider.tsx @@ -0,0 +1,14 @@ +import { ThemeProvider } from '@emotion/react'; + +import { darkTheme, lightTheme } from '@/ui/layout/styles/themes'; +import { browserPrefersDarkMode } from '@/utils/utils'; + +type OwnProps = { + children: JSX.Element; +}; + +export function AppThemeProvider({ children }: OwnProps) { + const selectedTheme = browserPrefersDarkMode() ? darkTheme : lightTheme; + + return {children}; +} diff --git a/front/src/providers/AuthProvider.tsx b/front/src/providers/AuthProvider.tsx new file mode 100644 index 000000000..6e94f051c --- /dev/null +++ b/front/src/providers/AuthProvider.tsx @@ -0,0 +1,29 @@ +import { useEffect } from 'react'; +import { useRecoilState } from 'recoil'; + +import { getUserIdFromToken } from '@/auth/services/AuthService'; +import { currentUserState } from '@/auth/states/currentUserState'; +import { isAuthenticatingState } from '@/auth/states/isAuthenticatingState'; +import { mapToUser } from '@/users/interfaces/user.interface'; +import { useGetCurrentUserQuery } from '@/users/services'; + +type OwnProps = { + children: JSX.Element; +}; + +export function AuthProvider({ children }: OwnProps) { + const [, setCurrentUser] = useRecoilState(currentUserState); + const [, setIsAuthenticating] = useRecoilState(isAuthenticatingState); + + const userIdFromToken = getUserIdFromToken(); + const { data } = useGetCurrentUserQuery(userIdFromToken); + + useEffect(() => { + if (data?.users[0]) { + setCurrentUser(mapToUser(data?.users?.[0])); + setIsAuthenticating(false); + } + }, [data, setCurrentUser, setIsAuthenticating]); + + return <>{children}; +} diff --git a/server/src/api/resolvers/relations/comment-thread-relations.resolver.ts b/server/src/api/resolvers/relations/comment-thread-relations.resolver.ts index d66bfb09f..08001717d 100644 --- a/server/src/api/resolvers/relations/comment-thread-relations.resolver.ts +++ b/server/src/api/resolvers/relations/comment-thread-relations.resolver.ts @@ -17,6 +17,10 @@ export class CommentThreadRelationsResolver { where: { commentThreadId: commentThread.id, }, + orderBy: { + // TODO: find a way to pass it in the query + createdAt: 'desc', + }, }); } }