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:
@ -14,6 +14,9 @@ module.exports = {
|
|||||||
withHooks: true,
|
withHooks: true,
|
||||||
withHOC: false,
|
withHOC: false,
|
||||||
withComponent: false,
|
withComponent: false,
|
||||||
|
scalars: {
|
||||||
|
DateTime: 'string',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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>,
|
||||||
|
|||||||
8
front/src/modules/auth/states/currentUserState.ts
Normal file
8
front/src/modules/auth/states/currentUserState.ts
Normal 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,
|
||||||
|
});
|
||||||
11
front/src/modules/auth/states/isAuthenticatedState.ts
Normal file
11
front/src/modules/auth/states/isAuthenticatedState.ts
Normal 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;
|
||||||
|
},
|
||||||
|
});
|
||||||
6
front/src/modules/auth/states/isAuthenticatingState.ts
Normal file
6
front/src/modules/auth/states/isAuthenticatingState.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const isAuthenticatingState = atom<boolean>({
|
||||||
|
key: 'auth/is-authenticating',
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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};
|
||||||
|
|||||||
@ -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() ?? '',
|
||||||
|
}}
|
||||||
/>,
|
/>,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -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}
|
/>,
|
||||||
/>
|
|
||||||
),
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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`
|
||||||
|
|||||||
@ -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>) {
|
||||||
@ -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 />),
|
||||||
|
};
|
||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
6
front/src/modules/ui/layout/states/themeEnabledState.ts
Normal file
6
front/src/modules/ui/layout/states/themeEnabledState.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { atom } from 'recoil';
|
||||||
|
|
||||||
|
export const themeEnabledState = atom<boolean>({
|
||||||
|
key: 'ui/theme-enabled',
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
@ -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`,
|
||||||
|
|
||||||
|
|||||||
@ -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 = {
|
||||||
|
|||||||
@ -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;
|
|
||||||
14
front/src/providers/AppThemeProvider.tsx
Normal file
14
front/src/providers/AppThemeProvider.tsx
Normal 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>;
|
||||||
|
}
|
||||||
29
front/src/providers/AuthProvider.tsx
Normal file
29
front/src/providers/AuthProvider.tsx
Normal 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}</>;
|
||||||
|
}
|
||||||
@ -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',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user