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:
Lucas Bordeau
2023-06-08 10:36:37 +02:00
committed by GitHub
parent 5e2673a2a4
commit ce4ba10f7b
31 changed files with 395 additions and 167 deletions

View File

@ -14,6 +14,9 @@ module.exports = {
withHooks: true, withHooks: true,
withHOC: false, withHOC: false,
withComponent: false, withComponent: false,
scalars: {
DateTime: 'string',
}
}, },
}, },
}, },

View File

@ -1,43 +1,18 @@
import React, { useEffect, useState } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom'; 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 { RequireAuth } from './modules/auth/components/RequireAuth';
import { getUserIdFromToken } from './modules/auth/services/AuthService';
import { AppLayout } from './modules/ui/layout/AppLayout'; import { AppLayout } from './modules/ui/layout/AppLayout';
import { darkTheme, lightTheme } from './modules/ui/layout/styles/themes'; import { AuthCallback } from './pages/auth/AuthCallback';
import { mapToUser, User } from './modules/users/interfaces/user.interface';
import { useGetCurrentUserQuery } from './modules/users/services';
import AuthCallback from './pages/auth/Callback';
import { Login } from './pages/auth/Login'; import { Login } from './pages/auth/Login';
import { Companies } from './pages/companies/Companies'; import { Companies } from './pages/companies/Companies';
import { Opportunities } from './pages/opportunities/Opportunities'; import { Opportunities } from './pages/opportunities/Opportunities';
import { People } from './pages/people/People'; import { People } from './pages/people/People';
type AppProps = { export function App() {
themeEnabled?: boolean; return (
};
export function App({ themeEnabled = true }: AppProps) {
const [user, setUser] = useState<User | undefined>(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 = (
<> <>
{ {
<AppLayout user={user}> <AppLayout>
<Routes> <Routes>
<Route <Route
path="/" path="/"
@ -78,14 +53,4 @@ export function App({ themeEnabled = true }: AppProps) {
} }
</> </>
); );
return (
<>
{themeEnabled ? (
<ThemeProvider theme={defaultTheme}>{app}</ThemeProvider>
) : (
app
)}
</>
);
} }

View File

@ -4,6 +4,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { RecoilRoot } from 'recoil'; import { RecoilRoot } from 'recoil';
import { App } from '~/App'; import { App } from '~/App';
import { AuthProvider } from '~/providers/AuthProvider';
import { FullHeightStorybookLayout } from '~/testing/FullHeightStorybookLayout'; import { FullHeightStorybookLayout } from '~/testing/FullHeightStorybookLayout';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedUserJWT } from '~/testing/mock-data/jwt'; import { mockedUserJWT } from '~/testing/mock-data/jwt';
@ -22,7 +23,9 @@ const render = () => (
<ApolloProvider client={mockedClient}> <ApolloProvider client={mockedClient}>
<MemoryRouter> <MemoryRouter>
<FullHeightStorybookLayout> <FullHeightStorybookLayout>
<App themeEnabled={false} /> <AuthProvider>
<App />
</AuthProvider>
</FullHeightStorybookLayout> </FullHeightStorybookLayout>
</MemoryRouter> </MemoryRouter>
</ApolloProvider> </ApolloProvider>

View File

@ -13,7 +13,7 @@ export type Scalars = {
Boolean: boolean; Boolean: boolean;
Int: number; Int: number;
Float: number; Float: number;
DateTime: any; DateTime: string;
JSON: any; JSON: any;
}; };
@ -1081,6 +1081,17 @@ export type WorkspaceMember = {
workspace: Workspace; 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<{ export type GetCompanyCommentsCountQueryVariables = Exact<{
where?: InputMaybe<CompanyWhereInput>; where?: InputMaybe<CompanyWhereInput>;
}>; }>;
@ -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<{ export type GetCompaniesQueryVariables = Exact<{
orderBy?: InputMaybe<Array<CompanyOrderByWithRelationInput> | CompanyOrderByWithRelationInput>; orderBy?: InputMaybe<Array<CompanyOrderByWithRelationInput> | 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<{ export type UpdateCompanyMutationVariables = Exact<{
id?: InputMaybe<Scalars['String']>; id?: InputMaybe<Scalars['String']>;
@ -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<{ export type InsertCompanyMutationVariables = Exact<{
id: Scalars['String']; 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<{ export type DeleteCompaniesMutationVariables = Exact<{
ids?: InputMaybe<Array<Scalars['String']> | Scalars['String']>; ids?: InputMaybe<Array<Scalars['String']> | 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<{ export type UpdatePeopleMutationVariables = Exact<{
id?: InputMaybe<Scalars['String']>; id?: InputMaybe<Scalars['String']>;
@ -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<{ export type InsertPersonMutationVariables = Exact<{
id: Scalars['String']; 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<{ export type DeletePeopleMutationVariables = Exact<{
ids?: InputMaybe<Array<Scalars['String']> | Scalars['String']>; ids?: InputMaybe<Array<Scalars['String']> | 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<{ export type SearchUserQueryQueryVariables = Exact<{
where?: InputMaybe<UserWhereInput>; where?: InputMaybe<UserWhereInput>;
@ -1227,6 +1238,53 @@ export type GetUsersQueryVariables = Exact<{ [key: string]: never; }>;
export type GetUsersQuery = { __typename?: 'Query', findManyUser: Array<{ __typename?: 'User', id: string }> }; 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<CreateCommentMutation, CreateCommentMutationVariables>;
/**
* __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<CreateCommentMutation, CreateCommentMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateCommentMutation, CreateCommentMutationVariables>(CreateCommentDocument, options);
}
export type CreateCommentMutationHookResult = ReturnType<typeof useCreateCommentMutation>;
export type CreateCommentMutationResult = Apollo.MutationResult<CreateCommentMutation>;
export type CreateCommentMutationOptions = Apollo.BaseMutationOptions<CreateCommentMutation, CreateCommentMutationVariables>;
export const GetCompanyCommentsCountDocument = gql` export const GetCompanyCommentsCountDocument = gql`
query GetCompanyCommentsCount($where: CompanyWhereInput) { query GetCompanyCommentsCount($where: CompanyWhereInput) {
companies: findManyCompany(where: $where) { companies: findManyCompany(where: $where) {

View File

@ -7,6 +7,8 @@ import { RecoilRoot } from 'recoil';
import '@emotion/react'; import '@emotion/react';
import { ThemeType } from './modules/ui/layout/styles/themes'; import { ThemeType } from './modules/ui/layout/styles/themes';
import { AppThemeProvider } from './providers/AppThemeProvider';
import { AuthProvider } from './providers/AuthProvider';
import { apiClient } from './apollo'; import { apiClient } from './apollo';
import { App } from './App'; import { App } from './App';
@ -19,9 +21,13 @@ root.render(
<RecoilRoot> <RecoilRoot>
<ApolloProvider client={apiClient}> <ApolloProvider client={apiClient}>
<BrowserRouter> <BrowserRouter>
<StrictMode> <AuthProvider>
<App /> <AppThemeProvider>
</StrictMode> <StrictMode>
<App />
</StrictMode>
</AppThemeProvider>
</AuthProvider>
</BrowserRouter> </BrowserRouter>
</ApolloProvider> </ApolloProvider>
</RecoilRoot>, </RecoilRoot>,

View File

@ -0,0 +1,8 @@
import { atom } from 'recoil';
import { User } from '@/users/interfaces/user.interface';
export const currentUserState = atom<User | null>({
key: 'auth/current-user',
default: null,
});

View File

@ -0,0 +1,11 @@
import { selector } from 'recoil';
import { currentUserState } from './currentUserState';
export const isAuthenticatedState = selector<boolean>({
key: 'auth/is-authenticated',
get: ({ get }) => {
const user = get(currentUserState);
return !!user;
},
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const isAuthenticatingState = atom<boolean>({
key: 'auth/is-authenticating',
default: true,
});

View File

@ -1,16 +1,16 @@
import { Tooltip } from 'react-tooltip'; import { Tooltip } from 'react-tooltip';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
import { UserAvatar } from '@/users/components/UserAvatar'; import { UserAvatar } from '@/users/components/UserAvatar';
import { import {
beautifyExactDate, beautifyExactDate,
beautifyPastDateRelativeToNow, beautifyPastDateRelativeToNow,
} from '@/utils/datetime/date-utils'; } from '@/utils/datetime/date-utils';
import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
type OwnProps = { type OwnProps = {
avatarUrl: string | null | undefined; comment: Pick<CommentForDrawer, 'id' | 'author' | 'createdAt'>;
username: string;
createdAt: Date;
}; };
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -44,15 +44,30 @@ const StyledDate = styled.div`
const StyledTooltip = styled(Tooltip)` const StyledTooltip = styled(Tooltip)`
padding: 8px; 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) { export function CommentHeader({ comment }: OwnProps) {
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(createdAt); const beautifiedCreatedAt = beautifyPastDateRelativeToNow(comment.createdAt);
const exactCreatedAt = beautifyExactDate(createdAt); const exactCreatedAt = beautifyExactDate(comment.createdAt);
const showDate = beautifiedCreatedAt !== ''; const showDate = beautifiedCreatedAt !== '';
const capitalizedFirstUsernameLetter = const author = comment.author;
username !== '' ? username.toLocaleUpperCase()[0] : ''; const authorName = author.displayName;
const avatarUrl = author.avatarUrl;
const commentId = comment.id;
const capitalizedFirstUsernameLetter = isNonEmptyString(authorName)
? authorName.toLocaleUpperCase()[0]
: '';
return ( return (
<StyledContainer> <StyledContainer>
@ -61,14 +76,12 @@ export function CommentHeader({ avatarUrl, username, createdAt }: OwnProps) {
size={16} size={16}
placeholderLetter={capitalizedFirstUsernameLetter} placeholderLetter={capitalizedFirstUsernameLetter}
/> />
<StyledName>{username}</StyledName> <StyledName>{authorName}</StyledName>
{showDate && ( {showDate && (
<> <>
<StyledDate className="comment-created-at"> <StyledDate id={`id-${commentId}`}>{beautifiedCreatedAt}</StyledDate>
{beautifiedCreatedAt}
</StyledDate>
<StyledTooltip <StyledTooltip
anchorSelect=".comment-created-at" anchorSelect={`#id-${commentId}`}
content={exactCreatedAt} content={exactCreatedAt}
clickable clickable
noArrow noArrow

View File

@ -1,8 +1,15 @@
import styled from '@emotion/styled'; 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 { 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'; import { CommentThreadItem } from './CommentThreadItem';
type OwnProps = { type OwnProps = {
@ -21,17 +28,70 @@ const StyledContainer = styled.div`
padding: ${(props) => props.theme.spacing(2)}; 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) { export function CommentThread({ commentThread }: OwnProps) {
function handleSendComment(text: string) { const [createCommentMutation] = useCreateCommentMutation();
console.log(text); 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 ( return (
<StyledContainer> <StyledContainer>
{commentThread.comments?.map((comment) => ( <StyledThreadItemListContainer>
<CommentThreadItem key={comment.id} comment={comment} /> {commentThread.comments?.map((comment) => (
))} <CommentThreadItem key={comment.id} comment={comment} />
<CommentTextInput onSend={handleSendComment} /> ))}
</StyledThreadItemListContainer>
<AutosizeTextInput onSend={handleSendComment} />
</StyledContainer> </StyledContainer>
); );
} }

View File

@ -18,22 +18,20 @@ const StyledContainer = styled.div`
const StyledCommentBody = styled.div` const StyledCommentBody = styled.div`
font-size: ${(props) => props.theme.fontSizeMedium}; font-size: ${(props) => props.theme.fontSizeMedium};
line-height: 19.5px; line-height: ${(props) => props.theme.lineHeight};
text-align: left; text-align: left;
padding-left: 24px; padding-left: 24px;
color: ${(props) => props.theme.text60}; color: ${(props) => props.theme.text60};
overflow-wrap: anywhere;
`; `;
export function CommentThreadItem({ comment }: OwnProps) { export function CommentThreadItem({ comment }: OwnProps) {
return ( return (
<StyledContainer> <StyledContainer>
<CommentHeader <CommentHeader comment={comment} />
avatarUrl={comment.author.avatarUrl}
username={comment.author.displayName}
createdAt={comment.createdAt}
/>
<StyledCommentBody>{comment.body}</StyledCommentBody> <StyledCommentBody>{comment.body}</StyledCommentBody>
</StyledContainer> </StyledContainer>
); );

View File

@ -29,7 +29,7 @@ export function RightDrawerComments() {
<RightDrawerTopBar title="Comments" /> <RightDrawerTopBar title="Comments" />
<RightDrawerBody> <RightDrawerBody>
{commentThreads.map((commentThread) => ( {commentThreads.map((commentThread) => (
<CommentThread commentThread={commentThread} /> <CommentThread key={commentThread.id} commentThread={commentThread} />
))} ))}
</RightDrawerBody> </RightDrawerBody>
</RightDrawerPage> </RightDrawerPage>

View File

@ -17,9 +17,14 @@ type Story = StoryObj<typeof CellCommentChip>;
const TestCellContainer = styled.div` const TestCellContainer = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-start;
min-width: 250px; min-width: 250px;
max-width: 250px;
text-wrap: nowrap;
overflow: hidden;
height: fit-content; height: fit-content;
background: ${(props) => props.theme.primaryBackground}; background: ${(props) => props.theme.primaryBackground};

View File

@ -1,6 +1,8 @@
import type { Meta, StoryObj } from '@storybook/react'; import type { Meta, StoryObj } from '@storybook/react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { v4 } from 'uuid';
import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
import { mockedUsersData } from '~/testing/mock-data/users'; import { mockedUsersData } from '~/testing/mock-data/users';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers'; import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
@ -16,12 +18,23 @@ type Story = StoryObj<typeof CommentHeader>;
const mockUser = mockedUsersData[0]; 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 = { export const Default: Story = {
render: getRenderWrapperForComponent( render: getRenderWrapperForComponent(
<CommentHeader <CommentHeader
avatarUrl={mockUser.avatarUrl ?? ''} comment={{
username={mockUser.displayName ?? ''} ...mockComment,
createdAt={DateTime.now().minus({ hours: 2 }).toJSDate()} createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
}}
/>, />,
), ),
}; };
@ -29,9 +42,10 @@ export const Default: Story = {
export const FewDaysAgo: Story = { export const FewDaysAgo: Story = {
render: getRenderWrapperForComponent( render: getRenderWrapperForComponent(
<CommentHeader <CommentHeader
avatarUrl={mockUser.avatarUrl ?? ''} comment={{
username={mockUser.displayName ?? ''} ...mockComment,
createdAt={DateTime.now().minus({ days: 2 }).toJSDate()} createdAt: DateTime.now().minus({ days: 2 }).toISO() ?? '',
}}
/>, />,
), ),
}; };
@ -39,9 +53,10 @@ export const FewDaysAgo: Story = {
export const FewMonthsAgo: Story = { export const FewMonthsAgo: Story = {
render: getRenderWrapperForComponent( render: getRenderWrapperForComponent(
<CommentHeader <CommentHeader
avatarUrl={mockUser.avatarUrl ?? ''} comment={{
username={mockUser.displayName ?? ''} ...mockComment,
createdAt={DateTime.now().minus({ months: 2 }).toJSDate()} createdAt: DateTime.now().minus({ months: 2 }).toISO() ?? '',
}}
/>, />,
), ),
}; };
@ -49,9 +64,10 @@ export const FewMonthsAgo: Story = {
export const FewYearsAgo: Story = { export const FewYearsAgo: Story = {
render: getRenderWrapperForComponent( render: getRenderWrapperForComponent(
<CommentHeader <CommentHeader
avatarUrl={mockUser.avatarUrl ?? ''} comment={{
username={mockUser.displayName ?? ''} ...mockComment,
createdAt={DateTime.now().minus({ years: 2 }).toJSDate()} createdAt: DateTime.now().minus({ years: 2 }).toISO() ?? '',
}}
/>, />,
), ),
}; };
@ -59,9 +75,14 @@ export const FewYearsAgo: Story = {
export const WithoutAvatar: Story = { export const WithoutAvatar: Story = {
render: getRenderWrapperForComponent( render: getRenderWrapperForComponent(
<CommentHeader <CommentHeader
avatarUrl={''} comment={{
username={mockUser.displayName ?? ''} ...mockComment,
createdAt={DateTime.now().minus({ hours: 2 }).toJSDate()} author: {
...mockComment.author,
avatarUrl: '',
},
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
}}
/>, />,
), ),
}; };

View File

@ -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);
},
},
};

View 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
}
}
`;

View File

@ -30,8 +30,6 @@ export function CompanyEditableNameChipCell({ company }: OwnProps) {
const commentCount = useCompanyCommentsCountQuery(company.id); const commentCount = useCompanyCommentsCountQuery(company.id);
const displayCommentCount = !commentCount.loading;
return ( return (
<EditableChip <EditableChip
value={company.name || ''} value={company.name || ''}
@ -45,12 +43,10 @@ export function CompanyEditableNameChipCell({ company }: OwnProps) {
}} }}
ChipComponent={CompanyChip} ChipComponent={CompanyChip}
rightEndContents={[ rightEndContents={[
displayCommentCount && ( <CellCommentChip
<CellCommentChip count={commentCount.data ?? 0}
count={commentCount.data ?? 0} onClick={handleCommentClick}
onClick={handleCommentClick} />,
/>
),
]} ]}
/> />
); );

View File

@ -30,6 +30,8 @@ const StyledContainer = styled.span`
border-radius: 100%; border-radius: 100%;
object-fit: cover; object-fit: cover;
} }
height: 12px;
`; `;
export function PersonChip({ name, picture }: PersonChipPropsType) { export function PersonChip({ name, picture }: PersonChipPropsType) {

View File

@ -9,7 +9,7 @@ export const EditableCellNormalModeOuterContainer = styled.div`
overflow: hidden; overflow: hidden;
padding-left: ${(props) => props.theme.spacing(2)}; 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` export const EditableCellNormalModeInnerContainer = styled.div`

View File

@ -5,7 +5,7 @@ import { HiArrowSmRight } from 'react-icons/hi';
import TextareaAutosize from 'react-textarea-autosize'; import TextareaAutosize from 'react-textarea-autosize';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { IconButton } from '@/ui/components/buttons/IconButton'; import { IconButton } from '../buttons/IconButton';
type OwnProps = { type OwnProps = {
onSend?: (text: string) => void; onSend?: (text: string) => void;
@ -50,7 +50,7 @@ const StyledBottomRightIconButton = styled.div`
right: 26px; right: 26px;
`; `;
export function CommentTextInput({ placeholder, onSend }: OwnProps) { export function AutosizeTextInput({ placeholder, onSend }: OwnProps) {
const [text, setText] = useState(''); const [text, setText] = useState('');
const isSendButtonDisabled = !text; const isSendButtonDisabled = !text;
@ -72,12 +72,12 @@ export function CommentTextInput({ placeholder, onSend }: OwnProps) {
enableOnContentEditable: true, enableOnContentEditable: true,
enableOnFormTags: true, enableOnFormTags: true,
}, },
[onSend], [onSend, text, setText],
); );
useHotkeys( useHotkeys(
'esc', 'esc',
(event: KeyboardEvent, handler: HotkeysEvent) => { (event: KeyboardEvent) => {
event.preventDefault(); event.preventDefault();
setText(''); setText('');
@ -86,7 +86,7 @@ export function CommentTextInput({ placeholder, onSend }: OwnProps) {
enableOnContentEditable: true, enableOnContentEditable: true,
enableOnFormTags: true, enableOnFormTags: true,
}, },
[onSend], [onSend, setText],
); );
function handleInputChange(event: React.FormEvent<HTMLTextAreaElement>) { function handleInputChange(event: React.FormEvent<HTMLTextAreaElement>) {

View File

@ -0,0 +1,22 @@
import type { Meta, StoryObj } from '@storybook/react';
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
import { AutosizeTextInput } from '../AutosizeTextInput';
const meta: Meta<typeof AutosizeTextInput> = {
title: 'Components/Common/AutosizeTextInput',
component: AutosizeTextInput,
argTypes: {
onSend: {
action: 'onSend',
},
},
};
export default meta;
type Story = StoryObj<typeof AutosizeTextInput>;
export const Default: Story = {
render: getRenderWrapperForComponent(<AutosizeTextInput />),
};

View File

@ -1,6 +1,7 @@
import styled from '@emotion/styled'; 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'; import { Navbar } from './navbar/Navbar';
@ -23,16 +24,17 @@ const MainContainer = styled.div`
type OwnProps = { type OwnProps = {
children: JSX.Element; children: JSX.Element;
user?: User;
}; };
export function AppLayout({ children, user }: OwnProps) { export function AppLayout({ children }: OwnProps) {
const userIsAuthenticated = !!user; const currentUser = useRecoilState(currentUserState);
const userIsAuthenticated = !!currentUser;
return ( return (
<StyledLayout> <StyledLayout>
{userIsAuthenticated ? ( {userIsAuthenticated ? (
<> <>
<Navbar user={user} workspace={user?.workspaceMember?.workspace} /> <Navbar />
<MainContainer>{children}</MainContainer> <MainContainer>{children}</MainContainer>
</> </>
) : ( ) : (

View File

@ -2,9 +2,6 @@ import { TbBuilding, TbUser } from 'react-icons/tb';
import { useMatch, useResolvedPath } from 'react-router-dom'; import { useMatch, useResolvedPath } from 'react-router-dom';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { User } from '@/users/interfaces/user.interface';
import { Workspace } from '@/workspaces/interfaces/workspace.interface';
import NavItem from './NavItem'; import NavItem from './NavItem';
import NavTitle from './NavTitle'; import NavTitle from './NavTitle';
import WorkspaceContainer from './WorkspaceContainer'; import WorkspaceContainer from './WorkspaceContainer';
@ -23,16 +20,11 @@ const NavItemsContainer = styled.div`
margin-top: 40px; margin-top: 40px;
`; `;
type OwnProps = { export function Navbar() {
user?: User;
workspace?: Workspace;
};
export function Navbar({ workspace }: OwnProps) {
return ( return (
<> <>
<NavbarContainer> <NavbarContainer>
{workspace && <WorkspaceContainer workspace={workspace} />} <WorkspaceContainer />
<NavItemsContainer> <NavItemsContainer>
<NavTitle label="Workspace" /> <NavTitle label="Workspace" />
<NavItem <NavItem

View File

@ -1,10 +1,7 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { Workspace } from '@/workspaces/interfaces/workspace.interface'; import { currentUserState } from '@/auth/states/currentUserState';
type OwnProps = {
workspace: Workspace;
};
const StyledContainer = styled.button` const StyledContainer = styled.button`
display: inline-flex; display: inline-flex;
@ -39,11 +36,19 @@ const StyledName = styled.div`
color: ${(props) => props.theme.text80}; color: ${(props) => props.theme.text80};
`; `;
function WorkspaceContainer({ workspace }: OwnProps) { function WorkspaceContainer() {
const currentUser = useRecoilValue(currentUserState);
const currentWorkspace = currentUser?.workspaceMember?.workspace;
if (!currentWorkspace) {
return null;
}
return ( return (
<StyledContainer> <StyledContainer>
<StyledLogo logo={workspace.logo}></StyledLogo> <StyledLogo logo={currentWorkspace?.logo}></StyledLogo>
<StyledName>{workspace?.displayName}</StyledName> <StyledName>{currentWorkspace?.displayName}</StyledName>
</StyledContainer> </StyledContainer>
); );
} }

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const themeEnabledState = atom<boolean>({
key: 'ui/theme-enabled',
default: true,
});

View File

@ -15,6 +15,7 @@ const commonTheme = {
fontWeightBold: 500, fontWeightBold: 500,
fontFamily: 'Inter, sans-serif', fontFamily: 'Inter, sans-serif',
lineHeight: '150%',
spacing: (multiplicator: number) => `${multiplicator * 4}px`, spacing: (multiplicator: number) => `${multiplicator * 4}px`,

View File

@ -3,6 +3,7 @@ export interface Workspace {
domainName?: string; domainName?: string;
displayName?: string; displayName?: string;
logo?: string | null; logo?: string | null;
__typename?: string;
} }
export type GraphqlQueryWorkspace = { export type GraphqlQueryWorkspace = {

View File

@ -3,7 +3,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
import { refreshAccessToken } from '@/auth/services/AuthService'; import { refreshAccessToken } from '@/auth/services/AuthService';
function Callback() { export function AuthCallback() {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -25,5 +25,3 @@ function Callback() {
return <></>; return <></>;
} }
export default Callback;

View File

@ -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 <ThemeProvider theme={selectedTheme}>{children}</ThemeProvider>;
}

View File

@ -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}</>;
}

View File

@ -17,6 +17,10 @@ export class CommentThreadRelationsResolver {
where: { where: {
commentThreadId: commentThread.id, commentThreadId: commentThread.id,
}, },
orderBy: {
// TODO: find a way to pass it in the query
createdAt: 'desc',
},
}); });
} }
} }