diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 755d7d39b4..ddd7846cec 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -1092,6 +1092,18 @@ export type CreateCommentMutationVariables = Exact<{ 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 CreateCommentThreadWithCommentMutationVariables = Exact<{ + commentThreadId: Scalars['String']; + commentText: Scalars['String']; + authorId: Scalars['String']; + createdAt: Scalars['DateTime']; + commentId: Scalars['String']; + commentThreadTargetArray: Array | CommentThreadTargetCreateManyCommentThreadInput; +}>; + + +export type CreateCommentThreadWithCommentMutation = { __typename?: 'Mutation', createOneCommentThread: { __typename?: 'CommentThread', id: string, createdAt: string, updatedAt: string, commentThreadTargets?: Array<{ __typename?: 'CommentThreadTarget', id: string, createdAt: string, updatedAt: string, commentThreadId: string, commentableType: CommentableType, commentableId: string }> | null, comments?: Array<{ __typename?: 'Comment', id: string, createdAt: string, updatedAt: string, body: string, author: { __typename?: 'User', id: string } }> | null } }; + export type GetCompanyCommentsCountQueryVariables = Exact<{ where?: InputMaybe; }>; @@ -1113,6 +1125,13 @@ export type GetCommentThreadsByTargetsQueryVariables = Exact<{ 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 GetCommentThreadQueryVariables = Exact<{ + commentThreadId: Scalars['String']; +}>; + + +export type GetCommentThreadQuery = { __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>; where?: InputMaybe; @@ -1285,6 +1304,65 @@ export function useCreateCommentMutation(baseOptions?: Apollo.MutationHookOption export type CreateCommentMutationHookResult = ReturnType; export type CreateCommentMutationResult = Apollo.MutationResult; export type CreateCommentMutationOptions = Apollo.BaseMutationOptions; +export const CreateCommentThreadWithCommentDocument = gql` + mutation CreateCommentThreadWithComment($commentThreadId: String!, $commentText: String!, $authorId: String!, $createdAt: DateTime!, $commentId: String!, $commentThreadTargetArray: [CommentThreadTargetCreateManyCommentThreadInput!]!) { + createOneCommentThread( + data: {id: $commentThreadId, createdAt: $createdAt, updatedAt: $createdAt, comments: {createMany: {data: {authorId: $authorId, id: $commentId, createdAt: $createdAt, body: $commentText}}}, commentThreadTargets: {createMany: {data: $commentThreadTargetArray, skipDuplicates: true}}} + ) { + id + createdAt + updatedAt + commentThreadTargets { + id + createdAt + updatedAt + commentThreadId + commentableType + commentableId + } + comments { + id + createdAt + updatedAt + body + author { + id + } + } + } +} + `; +export type CreateCommentThreadWithCommentMutationFn = Apollo.MutationFunction; + +/** + * __useCreateCommentThreadWithCommentMutation__ + * + * To run a mutation, you first call `useCreateCommentThreadWithCommentMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useCreateCommentThreadWithCommentMutation` 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 [createCommentThreadWithCommentMutation, { data, loading, error }] = useCreateCommentThreadWithCommentMutation({ + * variables: { + * commentThreadId: // value for 'commentThreadId' + * commentText: // value for 'commentText' + * authorId: // value for 'authorId' + * createdAt: // value for 'createdAt' + * commentId: // value for 'commentId' + * commentThreadTargetArray: // value for 'commentThreadTargetArray' + * }, + * }); + */ +export function useCreateCommentThreadWithCommentMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(CreateCommentThreadWithCommentDocument, options); + } +export type CreateCommentThreadWithCommentMutationHookResult = ReturnType; +export type CreateCommentThreadWithCommentMutationResult = Apollo.MutationResult; +export type CreateCommentThreadWithCommentMutationOptions = Apollo.BaseMutationOptions; export const GetCompanyCommentsCountDocument = gql` query GetCompanyCommentsCount($where: CompanyWhereInput) { companies: findManyCompany(where: $where) { @@ -1403,6 +1481,52 @@ export function useGetCommentThreadsByTargetsLazyQuery(baseOptions?: Apollo.Lazy export type GetCommentThreadsByTargetsQueryHookResult = ReturnType; export type GetCommentThreadsByTargetsLazyQueryHookResult = ReturnType; export type GetCommentThreadsByTargetsQueryResult = Apollo.QueryResult; +export const GetCommentThreadDocument = gql` + query GetCommentThread($commentThreadId: String!) { + findManyCommentThreads(where: {id: {equals: $commentThreadId}}) { + id + comments { + id + body + createdAt + updatedAt + author { + id + displayName + avatarUrl + } + } + } +} + `; + +/** + * __useGetCommentThreadQuery__ + * + * To run a query within a React component, call `useGetCommentThreadQuery` and pass it any options that fit your needs. + * When your component renders, `useGetCommentThreadQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useGetCommentThreadQuery({ + * variables: { + * commentThreadId: // value for 'commentThreadId' + * }, + * }); + */ +export function useGetCommentThreadQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(GetCommentThreadDocument, options); + } +export function useGetCommentThreadLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(GetCommentThreadDocument, options); + } +export type GetCommentThreadQueryHookResult = ReturnType; +export type GetCommentThreadLazyQueryHookResult = ReturnType; +export type GetCommentThreadQueryResult = Apollo.QueryResult; export const GetCompaniesDocument = gql` query GetCompanies($orderBy: [CompanyOrderByWithRelationInput!], $where: CompanyWhereInput) { companies: findManyCompany(orderBy: $orderBy, where: $where) { diff --git a/front/src/modules/comments/components/comments/CellCommentChip.tsx b/front/src/modules/comments/components/comments/CellCommentChip.tsx index 14b17422d9..42a2f63cd5 100644 --- a/front/src/modules/comments/components/comments/CellCommentChip.tsx +++ b/front/src/modules/comments/components/comments/CellCommentChip.tsx @@ -2,18 +2,28 @@ import styled from '@emotion/styled'; import { CommentChip, CommentChipProps } from './CommentChip'; +// TODO: tie those fixed values to the other components in the cell const StyledCellWrapper = styled.div` + position: absolute; + right: -46px; + top: 3px; +`; + +const StyledCommentChipContainer = styled.div` position: relative; - right: 34px; - top: -13px; - width: 0; - height: 0; + right: 50px; + width: 50px; + + display: flex; + justify-content: flex-end; `; export function CellCommentChip(props: CommentChipProps) { return ( - + + + ); } diff --git a/front/src/modules/comments/components/comments/CommentChip.tsx b/front/src/modules/comments/components/comments/CommentChip.tsx index 66e9124e38..647566b6cd 100644 --- a/front/src/modules/comments/components/comments/CommentChip.tsx +++ b/front/src/modules/comments/components/comments/CommentChip.tsx @@ -9,7 +9,7 @@ export type CommentChipProps = { const StyledChip = styled.div` height: 26px; - width: fit-content; + max-width: 42px; padding-left: 4px; padding-right: 4px; diff --git a/front/src/modules/comments/components/comments/CommentThread.tsx b/front/src/modules/comments/components/comments/CommentThread.tsx index 9aca2079c4..c48e586a4b 100644 --- a/front/src/modules/comments/components/comments/CommentThread.tsx +++ b/front/src/modules/comments/components/comments/CommentThread.tsx @@ -37,6 +37,7 @@ const StyledThreadItemListContainer = styled.div` max-height: 400px; overflow: auto; + width: 100%; gap: ${(props) => props.theme.spacing(4)}; `; @@ -46,6 +47,10 @@ export function CommentThread({ commentThread }: OwnProps) { const currentUser = useRecoilValue(currentUserState); function handleSendComment(commentText: string) { + if (!isNonEmptyString(commentText)) { + return; + } + if (!isDefined(currentUser)) { logError( 'In handleSendComment, currentUser is not defined, this should not happen.', @@ -53,35 +58,27 @@ export function CommentThread({ commentThread }: OwnProps) { 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}`, - ); - }, - }); - } + 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 + // Also it cannot refetch queries than are not in the cache + refetchQueries: [ + 'GetCommentThreadsByTargets', + 'GetPeopleCommentsCount', + 'GetCompanyCommentsCount', + ], + onError: (error) => { + logError( + `In handleSendComment, createCommentMutation onError, error: ${error}`, + ); + }, + }); } return ( @@ -91,7 +88,7 @@ export function CommentThread({ commentThread }: OwnProps) { ))} - + ); } diff --git a/front/src/modules/comments/components/comments/CommentThreadCreateMode.tsx b/front/src/modules/comments/components/comments/CommentThreadCreateMode.tsx new file mode 100644 index 0000000000..39a8146896 --- /dev/null +++ b/front/src/modules/comments/components/comments/CommentThreadCreateMode.tsx @@ -0,0 +1,140 @@ +import styled from '@emotion/styled'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { v4 } from 'uuid'; + +import { currentUserState } from '@/auth/states/currentUserState'; +import { commentableEntityArrayState } from '@/comments/states/commentableEntityArrayState'; +import { createdCommentThreadIdState } from '@/comments/states/createdCommentThreadIdState'; +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, + useCreateCommentThreadWithCommentMutation, + useGetCommentThreadQuery, +} from '~/generated/graphql'; + +import { CommentThreadItem } from './CommentThreadItem'; + +const StyledContainer = styled.div` + display: flex; + align-items: flex-start; + flex-direction: column; + justify-content: flex-start; + + max-height: calc(100% - 16px); + + gap: ${(props) => props.theme.spacing(4)}; + padding: ${(props) => props.theme.spacing(2)}; +`; + +const StyledThreadItemListContainer = styled.div` + display: flex; + flex-direction: column-reverse; + + align-items: flex-start; + justify-content: flex-start; + + overflow: auto; + width: 100%; + + gap: ${(props) => props.theme.spacing(4)}; +`; + +export function CommentThreadCreateMode() { + const [commentableEntityArray] = useRecoilState(commentableEntityArrayState); + + const [createdCommmentThreadId, setCreatedCommentThreadId] = useRecoilState( + createdCommentThreadIdState, + ); + + const [createCommentMutation] = useCreateCommentMutation(); + + const [createCommentThreadWithComment] = + useCreateCommentThreadWithCommentMutation(); + + const { data } = useGetCommentThreadQuery({ + variables: { + commentThreadId: createdCommmentThreadId ?? '', + }, + skip: !createdCommmentThreadId, + }); + + const comments = data?.findManyCommentThreads[0]?.comments; + + const displayCommentList = (comments?.length ?? 0) > 0; + + const currentUser = useRecoilValue(currentUserState); + + function handleNewComment(commentText: string) { + if (!isNonEmptyString(commentText)) { + return; + } + + if (!isDefined(currentUser)) { + logError( + 'In handleCreateCommentThread, currentUser is not defined, this should not happen.', + ); + return; + } + + if (!createdCommmentThreadId) { + createCommentThreadWithComment({ + variables: { + authorId: currentUser.id, + commentId: v4(), + commentText: commentText, + commentThreadId: v4(), + createdAt: new Date().toISOString(), + commentThreadTargetArray: commentableEntityArray.map( + (commentableEntity) => ({ + commentableId: commentableEntity.id, + commentableType: commentableEntity.type, + id: v4(), + createdAt: new Date().toISOString(), + }), + ), + }, + refetchQueries: ['GetCommentThread'], + onCompleted(data) { + setCreatedCommentThreadId(data.createOneCommentThread.id); + }, + }); + } else { + createCommentMutation({ + variables: { + commentId: v4(), + authorId: currentUser.id, + commentThreadId: createdCommmentThreadId, + commentText, + createdAt: new Date().toISOString(), + }, + // TODO: find a way to have this configuration dynamic and typed + refetchQueries: [ + 'GetCommentThread', + 'GetPeopleCommentsCount', + 'GetCompanyCommentsCount', + ], + onError: (error) => { + logError( + `In handleCreateCommentThread, createCommentMutation onError, error: ${error}`, + ); + }, + }); + } + } + + return ( + + {displayCommentList && ( + + {comments?.map((comment) => ( + + ))} + + )} + + + ); +} diff --git a/front/src/modules/comments/components/comments/RightDrawerCreateCommentThread.tsx b/front/src/modules/comments/components/comments/RightDrawerCreateCommentThread.tsx new file mode 100644 index 0000000000..d099851a48 --- /dev/null +++ b/front/src/modules/comments/components/comments/RightDrawerCreateCommentThread.tsx @@ -0,0 +1,16 @@ +import { RightDrawerBody } from '@/ui/layout/right-drawer/components/RightDrawerBody'; +import { RightDrawerPage } from '@/ui/layout/right-drawer/components/RightDrawerPage'; +import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDrawerTopBar'; + +import { CommentThreadCreateMode } from './CommentThreadCreateMode'; + +export function RightDrawerCreateCommentThread() { + return ( + + + + + + + ); +} diff --git a/front/src/modules/comments/hooks/useOpenCreateCommentDrawerForSelectedRowIds.ts b/front/src/modules/comments/hooks/useOpenCreateCommentDrawerForSelectedRowIds.ts new file mode 100644 index 0000000000..63a327a984 --- /dev/null +++ b/front/src/modules/comments/hooks/useOpenCreateCommentDrawerForSelectedRowIds.ts @@ -0,0 +1,38 @@ +import { useRecoilState, useRecoilValue } from 'recoil'; + +import { selectedRowIdsState } from '@/ui/tables/states/selectedRowIdsState'; +import { CommentableType } from '~/generated/graphql'; + +import { useOpenRightDrawer } from '../../ui/layout/right-drawer/hooks/useOpenRightDrawer'; +import { commentableEntityArrayState } from '../states/commentableEntityArrayState'; +import { createdCommentThreadIdState } from '../states/createdCommentThreadIdState'; +import { CommentableEntity } from '../types/CommentableEntity'; + +export function useOpenCreateCommentThreadDrawerForSelectedRowIds() { + const openRightDrawer = useOpenRightDrawer(); + + const [, setCommentableEntityArray] = useRecoilState( + commentableEntityArrayState, + ); + + const [, setCreatedCommentThreadId] = useRecoilState( + createdCommentThreadIdState, + ); + + const selectedPeopleIds = useRecoilValue(selectedRowIdsState); + + return function openCreateCommentDrawerForSelectedRowIds( + entityType: CommentableType, + ) { + const commentableEntityArray: CommentableEntity[] = selectedPeopleIds.map( + (id) => ({ + type: entityType, + id, + }), + ); + + setCreatedCommentThreadId(null); + setCommentableEntityArray(commentableEntityArray); + openRightDrawer('create-comment-thread'); + }; +} diff --git a/front/src/modules/comments/services/create.ts b/front/src/modules/comments/services/create.ts index fac426d2ac..9b68c508df 100644 --- a/front/src/modules/comments/services/create.ts +++ b/front/src/modules/comments/services/create.ts @@ -29,3 +29,56 @@ export const CREATE_COMMENT = gql` } } `; + +export const CREATE_COMMENT_THREAD_WITH_COMMENT = gql` + mutation CreateCommentThreadWithComment( + $commentThreadId: String! + $commentText: String! + $authorId: String! + $createdAt: DateTime! + $commentId: String! + $commentThreadTargetArray: [CommentThreadTargetCreateManyCommentThreadInput!]! + ) { + createOneCommentThread( + data: { + id: $commentThreadId + createdAt: $createdAt + updatedAt: $createdAt + comments: { + createMany: { + data: { + authorId: $authorId + id: $commentId + createdAt: $createdAt + body: $commentText + } + } + } + commentThreadTargets: { + createMany: { data: $commentThreadTargetArray, skipDuplicates: true } + } + } + ) { + id + createdAt + updatedAt + commentThreadTargets { + id + createdAt + updatedAt + commentThreadId + commentableType + commentableId + } + comments { + id + createdAt + updatedAt + body + author { + id + } + } + } + } +`; diff --git a/front/src/modules/comments/services/select.ts b/front/src/modules/comments/services/select.ts index 19828c9fb3..3aeb5f4b6f 100644 --- a/front/src/modules/comments/services/select.ts +++ b/front/src/modules/comments/services/select.ts @@ -59,3 +59,22 @@ export const GET_COMMENT_THREADS_BY_TARGETS = gql` } } `; + +export const GET_COMMENT_THREAD = gql` + query GetCommentThread($commentThreadId: String!) { + findManyCommentThreads(where: { id: { equals: $commentThreadId } }) { + id + comments { + id + body + createdAt + updatedAt + author { + id + displayName + avatarUrl + } + } + } + } +`; diff --git a/front/src/modules/comments/states/createdCommentThreadIdState.ts b/front/src/modules/comments/states/createdCommentThreadIdState.ts new file mode 100644 index 0000000000..47809154ea --- /dev/null +++ b/front/src/modules/comments/states/createdCommentThreadIdState.ts @@ -0,0 +1,6 @@ +import { atom } from 'recoil'; + +export const createdCommentThreadIdState = atom({ + key: 'comments/created-comment-thread-id', + default: null, +}); diff --git a/front/src/modules/comments/types/CommentableEntity.ts b/front/src/modules/comments/types/CommentableEntity.ts index e33bdfd42d..1b50711f7e 100644 --- a/front/src/modules/comments/types/CommentableEntity.ts +++ b/front/src/modules/comments/types/CommentableEntity.ts @@ -1,6 +1,6 @@ import { CommentableType } from '~/generated/graphql'; export type CommentableEntity = { - type: keyof typeof CommentableType; + type: CommentableType; id: string; }; diff --git a/front/src/modules/companies/components/CompanyEditableNameCell.tsx b/front/src/modules/companies/components/CompanyEditableNameCell.tsx index e3723ac66d..7ac1712e7d 100644 --- a/front/src/modules/companies/components/CompanyEditableNameCell.tsx +++ b/front/src/modules/companies/components/CompanyEditableNameCell.tsx @@ -3,6 +3,7 @@ import { useOpenCommentRightDrawer } from '@/comments/hooks/useOpenCommentRightD import { useCompanyCommentsCountQuery } from '@/comments/services'; import EditableChip from '@/ui/components/editable-cell/types/EditableChip'; import { getLogoUrlFromDomainName } from '@/utils/utils'; +import { CommentableType } from '~/generated/graphql'; import { Company } from '../interfaces/company.interface'; import { updateCompany } from '../services'; @@ -22,7 +23,7 @@ export function CompanyEditableNameChipCell({ company }: OwnProps) { openCommentRightDrawer([ { - type: 'Company', + type: CommentableType.Company, id: company.id, }, ]); diff --git a/front/src/modules/people/components/EditablePeopleFullName.tsx b/front/src/modules/people/components/EditablePeopleFullName.tsx index e40ebbdbfd..d60c396254 100644 --- a/front/src/modules/people/components/EditablePeopleFullName.tsx +++ b/front/src/modules/people/components/EditablePeopleFullName.tsx @@ -4,6 +4,7 @@ import styled from '@emotion/styled'; import { CellCommentChip } from '@/comments/components/comments/CellCommentChip'; import { useOpenCommentRightDrawer } from '@/comments/hooks/useOpenCommentRightDrawer'; import { EditableDoubleText } from '@/ui/components/editable-cell/types/EditableDoubleText'; +import { CommentableType } from '~/generated/graphql'; import { usePeopleCommentsCountQuery } from '../../comments/services'; @@ -49,7 +50,7 @@ export function EditablePeopleFullName({ openCommentRightDrawer([ { - type: 'Person', + type: CommentableType.Person, id: personId, }, ]); diff --git a/front/src/modules/people/components/PeopleCompanyCell.tsx b/front/src/modules/people/components/PeopleCompanyCell.tsx index 517af06ea3..bd29308978 100644 --- a/front/src/modules/people/components/PeopleCompanyCell.tsx +++ b/front/src/modules/people/components/PeopleCompanyCell.tsx @@ -11,6 +11,7 @@ import { import { SearchConfigType } from '@/search/interfaces/interface'; import { SEARCH_COMPANY_QUERY } from '@/search/services/search'; import { EditableRelation } from '@/ui/components/editable-cell/types/EditableRelation'; +import { logError } from '@/utils/logs/logError'; import { getLogoUrlFromDomainName } from '@/utils/utils'; import { QueryMode, @@ -57,7 +58,7 @@ export function PeopleCompanyCell({ people }: OwnProps) { }); } catch (error) { // TODO: handle error better - console.log(error); + logError(error); } setIsCreating(false); diff --git a/front/src/modules/ui/components/inputs/AutosizeTextInput.tsx b/front/src/modules/ui/components/inputs/AutosizeTextInput.tsx index 7515f0d161..50bd9799b6 100644 --- a/front/src/modules/ui/components/inputs/AutosizeTextInput.tsx +++ b/front/src/modules/ui/components/inputs/AutosizeTextInput.tsx @@ -5,16 +5,19 @@ import { HiArrowSmRight } from 'react-icons/hi'; import TextareaAutosize from 'react-textarea-autosize'; import styled from '@emotion/styled'; -import { IconButton } from '../buttons/IconButton'; +import { IconButton } from '@/ui/components/buttons/IconButton'; + +const MAX_ROWS = 5; type OwnProps = { - onSend?: (text: string) => void; + onValidate?: (text: string) => void; + minRows?: number; placeholder?: string; }; const StyledContainer = styled.div` display: flex; - min-height: 32px; + width: 100%; `; @@ -43,14 +46,21 @@ const StyledTextArea = styled(TextareaAutosize)` } `; +// TODO: this messes with the layout, fix it const StyledBottomRightIconButton = styled.div` width: 0px; position: relative; top: calc(100% - 26.5px); right: 26px; + height: 0; `; -export function AutosizeTextInput({ placeholder, onSend }: OwnProps) { +export function AutosizeTextInput({ + placeholder, + onValidate, + minRows = 1, +}: OwnProps) { + const [isFocused, setIsFocused] = useState(false); const [text, setText] = useState(''); const isSendButtonDisabled = !text; @@ -58,12 +68,12 @@ export function AutosizeTextInput({ placeholder, onSend }: OwnProps) { useHotkeys( ['shift+enter', 'enter'], (event: KeyboardEvent, handler: HotkeysEvent) => { - if (handler.shift) { + if (handler.shift || !isFocused) { return; } else { event.preventDefault(); - onSend?.(text); + onValidate?.(text); setText(''); } @@ -72,12 +82,16 @@ export function AutosizeTextInput({ placeholder, onSend }: OwnProps) { enableOnContentEditable: true, enableOnFormTags: true, }, - [onSend, text, setText], + [onValidate, text, setText, isFocused], ); useHotkeys( 'esc', (event: KeyboardEvent) => { + if (!isFocused) { + return; + } + event.preventDefault(); setText(''); @@ -86,7 +100,7 @@ export function AutosizeTextInput({ placeholder, onSend }: OwnProps) { enableOnContentEditable: true, enableOnFormTags: true, }, - [onSend, setText], + [onValidate, setText, isFocused], ); function handleInputChange(event: React.FormEvent) { @@ -96,19 +110,24 @@ export function AutosizeTextInput({ placeholder, onSend }: OwnProps) { } function handleOnClickSendButton() { - onSend?.(text); + onValidate?.(text); setText(''); } + const computedMinRows = minRows > MAX_ROWS ? MAX_ROWS : minRows; + return ( <> setIsFocused(true)} + onBlur={() => setIsFocused(false)} /> = { title: 'Components/Common/AutosizeTextInput', component: AutosizeTextInput, - argTypes: { - onSend: { - action: 'onSend', - }, - }, }; export default meta; diff --git a/front/src/modules/ui/components/table/action-bar/TableActionBarButtonOpenComments.tsx b/front/src/modules/ui/components/table/action-bar/TableActionBarButtonOpenComments.tsx index 52608dfdf4..6337c87e66 100644 --- a/front/src/modules/ui/components/table/action-bar/TableActionBarButtonOpenComments.tsx +++ b/front/src/modules/ui/components/table/action-bar/TableActionBarButtonOpenComments.tsx @@ -1,23 +1,17 @@ import { FaRegComment } from 'react-icons/fa'; -import { useOpenRightDrawer } from '@/ui/layout/right-drawer/hooks/useOpenRightDrawer'; - import { EntityTableActionBarButton } from './EntityTableActionBarButton'; -export function TableActionBarButtonToggleComments() { - // TODO: here it would be nice to access the table context - // But let's see when we have custom entities and properties - const openRightDrawer = useOpenRightDrawer(); - - async function handleButtonClick() { - openRightDrawer('comments'); - } +type OwnProps = { + onClick: () => void; +}; +export function TableActionBarButtonToggleComments({ onClick }: OwnProps) { return ( } - onClick={handleButtonClick} + onClick={onClick} /> ); } diff --git a/front/src/modules/ui/layout/containers/WithTopBarContainer.tsx b/front/src/modules/ui/layout/containers/WithTopBarContainer.tsx index 8ec4882925..28e9bdd00d 100644 --- a/front/src/modules/ui/layout/containers/WithTopBarContainer.tsx +++ b/front/src/modules/ui/layout/containers/WithTopBarContainer.tsx @@ -5,7 +5,7 @@ import { useRecoilState } from 'recoil'; import { Panel } from '../Panel'; import { RightDrawer } from '../right-drawer/components/RightDrawer'; import { isRightDrawerOpenState } from '../right-drawer/states/isRightDrawerOpenState'; -import { TopBar } from '../top-bar/TopBar'; +import { TOP_BAR_MIN_HEIGHT, TopBar } from '../top-bar/TopBar'; type OwnProps = { children: JSX.Element; @@ -20,13 +20,14 @@ const StyledContainer = styled.div` width: 100%; `; -const TOPBAR_HEIGHT = '48px'; - const MainContainer = styled.div` display: flex; flex-direction: row; width: calc(100% - ${(props) => props.theme.spacing(3)}); - height: calc(100% - ${TOPBAR_HEIGHT} - ${(props) => props.theme.spacing(3)}); + height: calc( + 100% - ${TOP_BAR_MIN_HEIGHT} - ${(props) => props.theme.spacing(2)} - + ${(props) => props.theme.spacing(5)} + ); background: ${(props) => props.theme.noisyBackground}; padding-right: ${(props) => props.theme.spacing(3)}; padding-bottom: ${(props) => props.theme.spacing(3)}; diff --git a/front/src/modules/ui/layout/right-drawer/components/RightDrawerBody.tsx b/front/src/modules/ui/layout/right-drawer/components/RightDrawerBody.tsx index a1444d0047..3abc1b49f2 100644 --- a/front/src/modules/ui/layout/right-drawer/components/RightDrawerBody.tsx +++ b/front/src/modules/ui/layout/right-drawer/components/RightDrawerBody.tsx @@ -3,4 +3,5 @@ import styled from '@emotion/styled'; export const RightDrawerBody = styled.div` display: flex; flex-direction: column; + overflow: auto; `; diff --git a/front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx b/front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx index 8a6a0f49dc..646da7e025 100644 --- a/front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx +++ b/front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx @@ -1,6 +1,7 @@ import { useRecoilState } from 'recoil'; import { RightDrawerComments } from '@/comments/components/comments/RightDrawerComments'; +import { RightDrawerCreateCommentThread } from '@/comments/components/comments/RightDrawerCreateCommentThread'; import { isDefined } from '@/utils/type-guards/isDefined'; import { rightDrawerPageState } from '../states/rightDrawerPageState'; @@ -12,5 +13,11 @@ export function RightDrawerRouter() { return <>; } - return rightDrawerPage === 'comments' ? : <>; + return rightDrawerPage === 'comments' ? ( + + ) : rightDrawerPage === 'create-comment-thread' ? ( + + ) : ( + <> + ); } diff --git a/front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx b/front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx index ca13d5a05b..81ec723f42 100644 --- a/front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx +++ b/front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx @@ -5,7 +5,7 @@ import { RightDrawerTopBarCloseButton } from './RightDrawerTopBarCloseButton'; const StyledRightDrawerTopBar = styled.div` display: flex; flex-direction: row; - height: 40px; + min-height: 40px; align-items: center; justify-content: space-between; padding-left: 8px; diff --git a/front/src/modules/ui/layout/right-drawer/states/rightDrawerPageState.ts b/front/src/modules/ui/layout/right-drawer/states/rightDrawerPageState.ts index 4c161d90ef..9089e2ce1d 100644 --- a/front/src/modules/ui/layout/right-drawer/states/rightDrawerPageState.ts +++ b/front/src/modules/ui/layout/right-drawer/states/rightDrawerPageState.ts @@ -4,5 +4,5 @@ import { RightDrawerPage } from '../types/RightDrawerPage'; export const rightDrawerPageState = atom({ key: 'ui/layout/right-drawer-page', - default: 'comments', + default: null, }); diff --git a/front/src/modules/ui/layout/right-drawer/types/RightDrawerPage.ts b/front/src/modules/ui/layout/right-drawer/types/RightDrawerPage.ts index 8fc92dbce3..9478a99b25 100644 --- a/front/src/modules/ui/layout/right-drawer/types/RightDrawerPage.ts +++ b/front/src/modules/ui/layout/right-drawer/types/RightDrawerPage.ts @@ -1 +1 @@ -export type RightDrawerPage = 'comments'; +export type RightDrawerPage = 'comments' | 'create-comment-thread'; diff --git a/front/src/modules/ui/layout/top-bar/TopBar.tsx b/front/src/modules/ui/layout/top-bar/TopBar.tsx index b911ffbc07..0cbe8146a6 100644 --- a/front/src/modules/ui/layout/top-bar/TopBar.tsx +++ b/front/src/modules/ui/layout/top-bar/TopBar.tsx @@ -2,13 +2,15 @@ import { ReactNode } from 'react'; import { TbPlus } from 'react-icons/tb'; import styled from '@emotion/styled'; +export const TOP_BAR_MIN_HEIGHT = '40px'; + const TopBarContainer = styled.div` display: flex; flex-direction: row; - height: 38px; + min-height: ${TOP_BAR_MIN_HEIGHT}; align-items: center; background: ${(props) => props.theme.noisyBackground}; - padding: 8px; + padding: ${(props) => props.theme.spacing(2)}; font-size: 14px; color: ${(props) => props.theme.text80}; `; diff --git a/front/src/pages/companies/Companies.tsx b/front/src/pages/companies/Companies.tsx index f960196336..aba7d312aa 100644 --- a/front/src/pages/companies/Companies.tsx +++ b/front/src/pages/companies/Companies.tsx @@ -20,12 +20,12 @@ import { } from '@/filters-and-sorts/helpers'; import { SelectedFilterType } from '@/filters-and-sorts/interfaces/filters/interface'; import { EntityTableActionBar } from '@/ui/components/table/action-bar/EntityTableActionBar'; -import { TableActionBarButtonToggleComments } from '@/ui/components/table/action-bar/TableActionBarButtonOpenComments'; import { EntityTable } from '@/ui/components/table/EntityTable'; import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer'; import { BoolExpType } from '@/utils/interfaces/generic.interface'; import { CompanyOrderByWithRelationInput as Companies_Order_By } from '~/generated/graphql'; +import { TableActionBarButtonCreateCommentThreadCompany } from './table/TableActionBarButtonCreateCommentThreadCompany'; import { TableActionBarButtonDeleteCompanies } from './table/TableActionBarButtonDeleteCompanies'; import { useCompaniesColumns } from './companies-columns'; import { availableFilters } from './companies-filters'; @@ -93,7 +93,7 @@ export function Companies() { /> - + diff --git a/front/src/pages/companies/table/TableActionBarButtonCreateCommentThreadCompany.tsx b/front/src/pages/companies/table/TableActionBarButtonCreateCommentThreadCompany.tsx new file mode 100644 index 0000000000..b60a7b4661 --- /dev/null +++ b/front/src/pages/companies/table/TableActionBarButtonCreateCommentThreadCompany.tsx @@ -0,0 +1,14 @@ +import { useOpenCreateCommentThreadDrawerForSelectedRowIds } from '@/comments/hooks/useOpenCreateCommentDrawerForSelectedRowIds'; +import { TableActionBarButtonToggleComments } from '@/ui/components/table/action-bar/TableActionBarButtonOpenComments'; +import { CommentableType } from '~/generated/graphql'; + +export function TableActionBarButtonCreateCommentThreadCompany() { + const openCreateCommentThreadRightDrawer = + useOpenCreateCommentThreadDrawerForSelectedRowIds(); + + async function handleButtonClick() { + openCreateCommentThreadRightDrawer(CommentableType.Company); + } + + return ; +} diff --git a/front/src/pages/people/People.tsx b/front/src/pages/people/People.tsx index 07e09d3fdc..afb50b86d5 100644 --- a/front/src/pages/people/People.tsx +++ b/front/src/pages/people/People.tsx @@ -17,11 +17,11 @@ import { usePeopleQuery, } from '@/people/services'; import { EntityTableActionBar } from '@/ui/components/table/action-bar/EntityTableActionBar'; -import { TableActionBarButtonToggleComments } from '@/ui/components/table/action-bar/TableActionBarButtonOpenComments'; import { EntityTable } from '@/ui/components/table/EntityTable'; import { WithTopBarContainer } from '@/ui/layout/containers/WithTopBarContainer'; import { BoolExpType } from '@/utils/interfaces/generic.interface'; +import { TableActionBarButtonCreateCommentThreadPeople } from './table/TableActionBarButtonCreateCommentThreadPeople'; import { TableActionBarButtonDeletePeople } from './table/TableActionBarButtonDeletePeople'; import { usePeopleColumns } from './people-columns'; import { availableFilters } from './people-filters'; @@ -91,7 +91,7 @@ export function People() { /> - + diff --git a/front/src/pages/people/table/TableActionBarButtonCreateCommentThreadPeople.tsx b/front/src/pages/people/table/TableActionBarButtonCreateCommentThreadPeople.tsx new file mode 100644 index 0000000000..0b15ab33e4 --- /dev/null +++ b/front/src/pages/people/table/TableActionBarButtonCreateCommentThreadPeople.tsx @@ -0,0 +1,14 @@ +import { useOpenCreateCommentThreadDrawerForSelectedRowIds } from '@/comments/hooks/useOpenCreateCommentDrawerForSelectedRowIds'; +import { TableActionBarButtonToggleComments } from '@/ui/components/table/action-bar/TableActionBarButtonOpenComments'; +import { CommentableType } from '~/generated/graphql'; + +export function TableActionBarButtonCreateCommentThreadPeople() { + const openCreateCommentThreadRightDrawer = + useOpenCreateCommentThreadDrawerForSelectedRowIds(); + + async function handleButtonClick() { + openCreateCommentThreadRightDrawer(CommentableType.Person); + } + + return ; +} diff --git a/server/src/api/resolvers/comment-thread.resolver.ts b/server/src/api/resolvers/comment-thread.resolver.ts index 031a098d8e..c76785465e 100644 --- a/server/src/api/resolvers/comment-thread.resolver.ts +++ b/server/src/api/resolvers/comment-thread.resolver.ts @@ -32,13 +32,48 @@ export class CommentThreadResolver { ...{ workspaceId: workspace.id }, })) : []; - return this.prismaService.commentThread.create({ + + const createdCommentThread = await this.prismaService.commentThread.create({ data: { ...args.data, + ...{ commentThreadTargets: undefined }, ...{ comments: { createMany: { data: newCommentData } } }, ...{ workspace: { connect: { id: workspace.id } } }, }, }); + + if (args.data.commentThreadTargets?.createMany?.data) { + await this.prismaService.commentThreadTarget.createMany({ + data: args.data.commentThreadTargets?.createMany?.data?.map( + (target) => ({ + ...target, + commentThreadId: args.data.id, + }), + ), + skipDuplicates: + args.data.commentThreadTargets?.createMany?.skipDuplicates ?? false, + }); + + return await this.prismaService.commentThread.update({ + where: { id: args.data.id }, + data: { + commentThreadTargets: { + connect: args.data.commentThreadTargets?.connect, + }, + }, + }); + } + + return createdCommentThread; + + // return this.prismaService.commentThread.create({ + // data: { + // ...args.data, + // ...{ commentThreadTargets: undefined }, + // ...{ comments: { createMany: { data: newCommentData } } }, + // ...{ workspace: { connect: { id: workspace.id } } }, + // }, + // }); } @Query(() => [CommentThread]) diff --git a/server/src/api/resolvers/guards/create-one-comment-thread.guard.ts b/server/src/api/resolvers/guards/create-one-comment-thread.guard.ts index 34d4920251..ef8e6b6bad 100644 --- a/server/src/api/resolvers/guards/create-one-comment-thread.guard.ts +++ b/server/src/api/resolvers/guards/create-one-comment-thread.guard.ts @@ -14,12 +14,13 @@ export class CreateOneCommentThreadGuard implements CanActivate { async canActivate(context: ExecutionContext): Promise { const gqlContext = GqlExecutionContext.create(context); + // TODO: type request const request = gqlContext.getContext().req; const args = gqlContext.getArgs(); const targets = args.data?.commentThreadTargets?.createMany?.data; const comments = args.data?.comments?.createMany?.data; - const workspaceId = await request.workspace; + const workspace = await request.workspace; if (!targets || targets.length === 0) { throw new HttpException( @@ -52,7 +53,7 @@ export class CreateOneCommentThreadGuard implements CanActivate { where: { id: target.commentableId }, }); - if (!targetEntity || targetEntity.workspaceId !== workspaceId) { + if (!targetEntity || targetEntity.workspaceId !== workspace.id) { throw new HttpException( { reason: 'CommentThreadTarget not found' }, HttpStatus.NOT_FOUND, @@ -90,10 +91,10 @@ export class CreateOneCommentThreadGuard implements CanActivate { if ( !userWorkspaceMember || - userWorkspaceMember.workspaceId !== workspaceId + userWorkspaceMember.workspaceId !== workspace.id ) { throw new HttpException( - { reason: 'Comment.authorId not found' }, + { reason: 'userWorkspaceMember.workspaceId not found' }, HttpStatus.NOT_FOUND, ); } diff --git a/server/src/api/resolvers/guards/update-one.guard.ts b/server/src/api/resolvers/guards/update-one.guard.ts index 3ec28e8e17..e7a724c340 100644 --- a/server/src/api/resolvers/guards/update-one.guard.ts +++ b/server/src/api/resolvers/guards/update-one.guard.ts @@ -18,7 +18,6 @@ export class UpdateOneGuard implements CanActivate { const entity = gqlContext.getArgByIndex(3).returnType?.name; const args = gqlContext.getArgs(); - console.log(args.data); if (!entity || !args.where?.id) { throw new HttpException( { reason: 'Invalid Request' },