Add "show company / people" view and "Notes" concept (#528)
* Begin adding show view and refactoring threads to become notes * Progress on design * Progress redesign timeline * Dropdown button, design improvement * Open comment thread edit mode in drawer * Autosave local storage and commentThreadcount * Improve display and fix missing key issue * Remove some hardcoded CSS properties * Create button * Split company show into ui/business + fix eslint * Fix font weight * Begin auto-save on edit mode * Save server-side query result to Apollo cache * Fix save behavior * Refetch timeline after creating note * Rename createCommentThreadWithComment * Improve styling * Revert "Improve styling" This reverts commit 9fbbf2db006e529330edc64f3eb8ff9ecdde6bb0. * Improve CSS styling * Bring back border radius inadvertently removed * padding adjustment * Improve blocknote design * Improve edit mode display * Remove Comments.tsx * Remove irrelevant comment stories * Removed un-necessary panel component * stop using fragment, move trash icon * Add a basic story for CompanyShow * Add a basic People show view * Fix storybook tests * Add very basic Person story * Refactor PR1 * Refactor part 2 * Refactor part 3 * Refactor part 4 * Fix tests --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -1,108 +0,0 @@
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
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 { GET_COMPANIES } from '@/companies/services';
|
||||
import { GET_PEOPLE } from '@/people/services';
|
||||
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 { GET_COMMENT_THREADS_BY_TARGETS } from '../services';
|
||||
|
||||
import { CommentThreadActionBar } from './CommentThreadActionBar';
|
||||
import { CommentThreadItem } from './CommentThreadItem';
|
||||
import { CommentThreadRelationPicker } from './CommentThreadRelationPicker';
|
||||
|
||||
type OwnProps = {
|
||||
commentThread: CommentThreadForDrawer;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
|
||||
justify-content: flex-start;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledThreadItemListContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
|
||||
justify-content: flex-start;
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export function CommentThread({ commentThread }: OwnProps) {
|
||||
const [createCommentMutation] = useCreateCommentMutation();
|
||||
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.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
createCommentMutation({
|
||||
variables: {
|
||||
commentId: v4(),
|
||||
authorId: currentUser.id,
|
||||
commentThreadId: commentThread.id,
|
||||
commentText,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
refetchQueries: [
|
||||
getOperationName(GET_COMPANIES) ?? '',
|
||||
getOperationName(GET_PEOPLE) ?? '',
|
||||
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
|
||||
],
|
||||
onError: (error) => {
|
||||
logError(
|
||||
`In handleSendComment, createCommentMutation onError, error: ${error}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledThreadItemListContainer>
|
||||
{commentThread.comments?.map((comment, index) => (
|
||||
<CommentThreadItem
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
actionBar={
|
||||
index === 0 ? (
|
||||
<CommentThreadActionBar commentThreadId={commentThread.id} />
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</StyledThreadItemListContainer>
|
||||
<CommentThreadRelationPicker commentThread={commentThread} />
|
||||
<AutosizeTextInput onValidate={handleSendComment} />
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
@ -1,148 +0,0 @@
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
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 { GET_COMPANIES } from '@/companies/services';
|
||||
import { GET_PEOPLE } from '@/people/services';
|
||||
import { AutosizeTextInput } from '@/ui/components/inputs/AutosizeTextInput';
|
||||
import { useOpenRightDrawer } from '@/ui/layout/right-drawer/hooks/useOpenRightDrawer';
|
||||
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 { GET_COMMENT_THREAD } from '../services';
|
||||
|
||||
import { CommentThreadItem } from './CommentThreadItem';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
|
||||
justify-content: flex-start;
|
||||
|
||||
max-height: calc(100% - 16px);
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledThreadItemListContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
|
||||
flex-direction: column-reverse;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
|
||||
justify-content: flex-start;
|
||||
overflow: auto;
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export function CommentThreadCreateMode() {
|
||||
const [commentableEntityArray] = useRecoilState(commentableEntityArrayState);
|
||||
|
||||
const [createdCommmentThreadId, setCreatedCommentThreadId] = useRecoilState(
|
||||
createdCommentThreadIdState,
|
||||
);
|
||||
|
||||
const openRightDrawer = useOpenRightDrawer();
|
||||
|
||||
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: [
|
||||
getOperationName(GET_COMPANIES) ?? '',
|
||||
getOperationName(GET_PEOPLE) ?? '',
|
||||
getOperationName(GET_COMMENT_THREAD) ?? '',
|
||||
],
|
||||
onCompleted(data) {
|
||||
setCreatedCommentThreadId(data.createOneCommentThread.id);
|
||||
openRightDrawer('comments');
|
||||
},
|
||||
});
|
||||
} else {
|
||||
createCommentMutation({
|
||||
variables: {
|
||||
commentId: v4(),
|
||||
authorId: currentUser.id,
|
||||
commentThreadId: createdCommmentThreadId,
|
||||
commentText,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_COMMENT_THREAD) ?? ''],
|
||||
onError: (error) => {
|
||||
logError(
|
||||
`In handleCreateCommentThread, createCommentMutation onError, error: ${error}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{displayCommentList && (
|
||||
<StyledThreadItemListContainer>
|
||||
{comments?.map((comment) => (
|
||||
<CommentThreadItem key={comment.id} comment={comment} />
|
||||
))}
|
||||
</StyledThreadItemListContainer>
|
||||
)}
|
||||
<AutosizeTextInput minRows={5} onValidate={handleNewComment} />
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
|
||||
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
|
||||
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
||||
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 {
|
||||
SortOrder,
|
||||
useGetCommentThreadsByTargetsQuery,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { commentableEntityArrayState } from '../states/commentableEntityArrayState';
|
||||
|
||||
import { CommentThread } from './CommentThread';
|
||||
|
||||
export function RightDrawerComments() {
|
||||
const [commentableEntityArray] = useRecoilState(commentableEntityArrayState);
|
||||
useHotkeysScopeOnMountOnly({
|
||||
scope: InternalHotkeysScope.RightDrawer,
|
||||
customScopes: { goto: false, 'command-menu': true },
|
||||
});
|
||||
|
||||
const { data: queryResult } = useGetCommentThreadsByTargetsQuery({
|
||||
variables: {
|
||||
commentThreadTargetIds: commentableEntityArray.map(
|
||||
(commentableEntity) => commentableEntity.id,
|
||||
),
|
||||
orderBy: [
|
||||
{
|
||||
createdAt: SortOrder.Desc,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const commentThreads: CommentThreadForDrawer[] =
|
||||
queryResult?.findManyCommentThreads ?? [];
|
||||
|
||||
return (
|
||||
<RightDrawerPage>
|
||||
<RightDrawerTopBar title="Comments" />
|
||||
<RightDrawerBody>
|
||||
{commentThreads.map((commentThread) => (
|
||||
<CommentThread key={commentThread.id} commentThread={commentThread} />
|
||||
))}
|
||||
</RightDrawerBody>
|
||||
</RightDrawerPage>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
import { useMemo } from 'react';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { BlockNoteEditor } from '@blocknote/core';
|
||||
import { useBlockNote } from '@blocknote/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/comments/services';
|
||||
import { BlockEditor } from '@/ui/components/editor/BlockEditor';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import {
|
||||
CommentThread,
|
||||
useUpdateCommentThreadBodyMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
const BlockNoteStyledContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
commentThread: Pick<CommentThread, 'id' | 'body'>;
|
||||
onChange?: (commentThreadBody: string) => void;
|
||||
};
|
||||
|
||||
export function CommentThreadBodyEditor({ commentThread, onChange }: OwnProps) {
|
||||
const [updateCommentThreadBodyMutation] =
|
||||
useUpdateCommentThreadBodyMutation();
|
||||
|
||||
const debounceOnChange = useMemo(() => {
|
||||
function onInternalChange(commentThreadBody: string) {
|
||||
onChange?.(commentThreadBody);
|
||||
updateCommentThreadBodyMutation({
|
||||
variables: {
|
||||
commentThreadId: commentThread.id,
|
||||
commentThreadBody: commentThreadBody,
|
||||
},
|
||||
refetchQueries: [
|
||||
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return debounce(onInternalChange, 200);
|
||||
}, [commentThread, updateCommentThreadBodyMutation, onChange]);
|
||||
|
||||
const editor: BlockNoteEditor | null = useBlockNote({
|
||||
initialContent: commentThread.body
|
||||
? JSON.parse(commentThread.body)
|
||||
: undefined,
|
||||
editorDOMAttributes: { class: 'editor-edit-mode' },
|
||||
onEditorContentChange: (editor) => {
|
||||
debounceOnChange(JSON.stringify(editor.topLevelBlocks) ?? '');
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<BlockNoteStyledContainer>
|
||||
<BlockEditor editor={editor} />
|
||||
</BlockNoteStyledContainer>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/comments/services';
|
||||
import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
|
||||
import { AutosizeTextInput } from '@/ui/components/inputs/AutosizeTextInput';
|
||||
import { isNonEmptyString } from '@/utils/type-guards/isNonEmptyString';
|
||||
import { CommentThread, useCreateCommentMutation } from '~/generated/graphql';
|
||||
|
||||
import { CommentThreadItem } from '../comment/CommentThreadItem';
|
||||
|
||||
type OwnProps = {
|
||||
commentThread: Pick<CommentThread, 'id'> & {
|
||||
comments: Array<CommentForDrawer>;
|
||||
};
|
||||
};
|
||||
|
||||
const StyledThreadItemListContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
|
||||
justify-content: flex-start;
|
||||
padding: ${({ theme }) => theme.spacing(8)};
|
||||
padding-left: ${({ theme }) => theme.spacing(12)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledCommentActionBar = styled.div`
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
display: flex;
|
||||
padding: 16px 24px 16px 48px;
|
||||
width: calc(${({ theme }) => theme.rightDrawerWidth} - 48px - 24px);
|
||||
`;
|
||||
|
||||
export function CommentThreadComments({ commentThread }: OwnProps) {
|
||||
const [createCommentMutation] = useCreateCommentMutation();
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
|
||||
if (!currentUser) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
function handleSendComment(commentText: string) {
|
||||
if (!isNonEmptyString(commentText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
createCommentMutation({
|
||||
variables: {
|
||||
commentId: v4(),
|
||||
authorId: currentUser?.id ?? '',
|
||||
commentThreadId: commentThread?.id ?? '',
|
||||
commentText: commentText,
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
refetchQueries: [getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? ''],
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{commentThread?.comments.length > 0 && (
|
||||
<StyledThreadItemListContainer>
|
||||
{commentThread?.comments?.map((comment, index) => (
|
||||
<CommentThreadItem key={comment.id} comment={comment} />
|
||||
))}
|
||||
</StyledThreadItemListContainer>
|
||||
)}
|
||||
|
||||
<StyledCommentActionBar>
|
||||
{currentUser && <AutosizeTextInput onValidate={handleSendComment} />}
|
||||
</StyledCommentActionBar>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
autoUpdate,
|
||||
@ -8,30 +7,33 @@ import {
|
||||
size,
|
||||
useFloating,
|
||||
} from '@floating-ui/react';
|
||||
import { IconArrowUpRight } from '@tabler/icons-react';
|
||||
|
||||
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
|
||||
import { useHandleCheckableCommentThreadTargetChange } from '@/comments/hooks/useHandleCheckableCommentThreadTargetChange';
|
||||
import { CommentableEntityForSelect } from '@/comments/types/CommentableEntityForSelect';
|
||||
import CompanyChip from '@/companies/components/CompanyChip';
|
||||
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
|
||||
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
||||
import { PersonChip } from '@/people/components/PersonChip';
|
||||
import { RecoilScope } from '@/recoil-scope/components/RecoilScope';
|
||||
import { MultipleEntitySelect } from '@/relation-picker/components/MultipleEntitySelect';
|
||||
import { useFilteredSearchEntityQuery } from '@/relation-picker/hooks/useFilteredSearchEntityQuery';
|
||||
import { useListenClickOutsideArrayOfRef } from '@/ui/hooks/useListenClickOutsideArrayOfRef';
|
||||
import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '@/ui/utils/flatMapAndSortEntityForSelectArrayByName';
|
||||
import { getLogoUrlFromDomainName } from '@/utils/utils';
|
||||
import {
|
||||
CommentableType,
|
||||
CommentThread,
|
||||
CommentThreadTarget,
|
||||
useSearchCompanyQuery,
|
||||
useSearchPeopleQuery,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { MultipleEntitySelect } from '../../relation-picker/components/MultipleEntitySelect';
|
||||
import { useHandleCheckableCommentThreadTargetChange } from '../hooks/useHandleCheckableCommentThreadTargetChange';
|
||||
import { CommentableEntityForSelect } from '../types/CommentableEntityForSelect';
|
||||
|
||||
type OwnProps = {
|
||||
commentThread: CommentThreadForDrawer;
|
||||
commentThread?: Pick<CommentThread, 'id'> & {
|
||||
commentThreadTargets: Array<
|
||||
Pick<CommentThreadTarget, 'id' | 'commentableId' | 'commentableType'>
|
||||
>;
|
||||
};
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -44,25 +46,6 @@ const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledLabelContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledRelationLabel = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
const StyledRelationContainer = styled.div`
|
||||
--horizontal-padding: ${({ theme }) => theme.spacing(1)};
|
||||
--vertical-padding: ${({ theme }) => theme.spacing(1.5)};
|
||||
@ -97,15 +80,12 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [searchFilter, setSearchFilter] = useState('');
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const peopleIds =
|
||||
commentThread.commentThreadTargets
|
||||
commentThread?.commentThreadTargets
|
||||
?.filter((relation) => relation.commentableType === 'Person')
|
||||
.map((relation) => relation.commentableId) ?? [];
|
||||
|
||||
const companyIds =
|
||||
commentThread.commentThreadTargets
|
||||
commentThread?.commentThreadTargets
|
||||
?.filter((relation) => relation.commentableType === 'Company')
|
||||
.map((relation) => relation.commentableId) ?? [];
|
||||
|
||||
@ -203,10 +183,6 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledLabelContainer>
|
||||
<IconArrowUpRight size={16} color={theme.font.color.tertiary} />
|
||||
<StyledRelationLabel>Relations</StyledRelationLabel>
|
||||
</StyledLabelContainer>
|
||||
<StyledRelationContainer
|
||||
ref={refs.setReference}
|
||||
onClick={handleRelationContainerClick}
|
||||
@ -215,11 +191,12 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
|
||||
entity.entityType === CommentableType.Company ? (
|
||||
<CompanyChip
|
||||
key={entity.id}
|
||||
id={entity.id}
|
||||
name={entity.name}
|
||||
picture={entity.avatarUrl}
|
||||
/>
|
||||
) : (
|
||||
<PersonChip key={entity.id} name={entity.name} />
|
||||
<PersonChip key={entity.id} name={entity.name} id={entity.id} />
|
||||
),
|
||||
)}
|
||||
</StyledRelationContainer>
|
||||
@ -0,0 +1,18 @@
|
||||
import {
|
||||
DropdownButton,
|
||||
DropdownOptionType,
|
||||
} from '@/ui/components/buttons/DropdownButton';
|
||||
import { IconNotes } from '@/ui/icons/index';
|
||||
|
||||
export function CommentThreadTypeDropdown() {
|
||||
const options: DropdownOptionType[] = [
|
||||
{ label: 'Notes', icon: <IconNotes /> },
|
||||
// { label: 'Call', icon: <IconPhone /> },
|
||||
];
|
||||
|
||||
const handleSelect = (selectedOption: DropdownOptionType) => {
|
||||
// console.log(`You selected: ${selectedOption.label}`);
|
||||
};
|
||||
|
||||
return <DropdownButton options={options} onSelection={handleSelect} />;
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import styled from '@emotion/styled';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
@ -24,8 +25,10 @@ type Story = StoryObj<typeof CommentThreadRelationPicker>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: getRenderWrapperForComponent(
|
||||
<StyledContainer>
|
||||
<CommentThreadRelationPicker commentThread={mockedCommentThreads[0]} />
|
||||
</StyledContainer>,
|
||||
<MemoryRouter>
|
||||
<StyledContainer>
|
||||
<CommentThreadRelationPicker commentThread={mockedCommentThreads[0]} />
|
||||
</StyledContainer>
|
||||
</MemoryRouter>,
|
||||
),
|
||||
};
|
||||
@ -22,7 +22,7 @@ const StyledCommentBody = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
|
||||
line-height: ${({ theme }) => theme.text.lineHeight};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.md};
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
padding-left: 24px;
|
||||
@ -6,8 +6,8 @@ import { CommentForDrawer } from '@/comments/types/CommentForDrawer';
|
||||
import { mockedUsersData } from '~/testing/mock-data/users';
|
||||
import { getRenderWrapperForComponent } from '~/testing/renderWrappers';
|
||||
|
||||
import { CommentThreadActionBar } from '../../right-drawer/CommentThreadActionBar';
|
||||
import { CommentHeader } from '../CommentHeader';
|
||||
import { CommentThreadActionBar } from '../CommentThreadActionBar';
|
||||
|
||||
const meta: Meta<typeof CommentHeader> = {
|
||||
title: 'Modules/Comments/CommentHeader',
|
||||
@ -0,0 +1,164 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/comments/services';
|
||||
import { PropertyBox } from '@/ui/components/property-box/PropertyBox';
|
||||
import { PropertyBoxItem } from '@/ui/components/property-box/PropertyBoxItem';
|
||||
import { IconArrowUpRight } from '@/ui/icons/index';
|
||||
import { debounce } from '@/utils/debounce';
|
||||
import {
|
||||
useGetCommentThreadQuery,
|
||||
useUpdateCommentThreadTitleMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { CommentThreadBodyEditor } from '../comment-thread/CommentThreadBodyEditor';
|
||||
import { CommentThreadComments } from '../comment-thread/CommentThreadComments';
|
||||
import { CommentThreadRelationPicker } from '../comment-thread/CommentThreadRelationPicker';
|
||||
import { CommentThreadTypeDropdown } from '../comment-thread/CommentThreadTypeDropdown';
|
||||
|
||||
import { CommentThreadActionBar } from './CommentThreadActionBar';
|
||||
|
||||
import '@blocknote/core/style.css';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
|
||||
justify-content: flex-start;
|
||||
`;
|
||||
|
||||
const StyledTopContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
padding: 24px 24px 24px 48px;
|
||||
`;
|
||||
|
||||
const StyledEditableTitleInput = styled.input`
|
||||
background: transparent;
|
||||
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
|
||||
flex-direction: column;
|
||||
font-family: Inter;
|
||||
font-size: ${({ theme }) => theme.font.size.xl};
|
||||
font-style: normal;
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
justify-content: center;
|
||||
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.md};
|
||||
outline: none;
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(2)});
|
||||
:placeholder {
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTopActionsContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
commentThreadId: string;
|
||||
showComment?: boolean;
|
||||
};
|
||||
|
||||
export function CommentThread({
|
||||
commentThreadId,
|
||||
showComment = true,
|
||||
}: OwnProps) {
|
||||
const { data } = useGetCommentThreadQuery({
|
||||
variables: {
|
||||
commentThreadId: commentThreadId ?? '',
|
||||
},
|
||||
skip: !commentThreadId,
|
||||
});
|
||||
|
||||
const [updateCommentThreadTitleMutation] =
|
||||
useUpdateCommentThreadTitleMutation();
|
||||
|
||||
const debounceUpdateTitle = useMemo(() => {
|
||||
function updateTitle(title: string) {
|
||||
updateCommentThreadTitleMutation({
|
||||
variables: {
|
||||
commentThreadId: commentThreadId,
|
||||
commentThreadTitle: title ?? '',
|
||||
},
|
||||
refetchQueries: [
|
||||
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
|
||||
],
|
||||
});
|
||||
}
|
||||
return debounce(updateTitle, 200);
|
||||
}, [commentThreadId, updateCommentThreadTitleMutation]);
|
||||
|
||||
function updateTitleFromBody(body: string) {
|
||||
const title = JSON.parse(body)[0]?.content[0]?.text;
|
||||
if (!commentThread?.title || commentThread?.title === '') {
|
||||
debounceUpdateTitle(title);
|
||||
}
|
||||
}
|
||||
|
||||
const commentThread = data?.findManyCommentThreads[0];
|
||||
|
||||
if (!commentThread) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledTopContainer>
|
||||
<StyledTopActionsContainer>
|
||||
<CommentThreadTypeDropdown />
|
||||
<CommentThreadActionBar commentThreadId={commentThread?.id ?? ''} />
|
||||
</StyledTopActionsContainer>
|
||||
<StyledEditableTitleInput
|
||||
placeholder="Note title (optional)"
|
||||
onChange={(event) => debounceUpdateTitle(event.target.value)}
|
||||
value={commentThread?.title ?? ''}
|
||||
/>
|
||||
<PropertyBox>
|
||||
<PropertyBoxItem
|
||||
icon={<IconArrowUpRight />}
|
||||
value={
|
||||
<CommentThreadRelationPicker
|
||||
commentThread={{
|
||||
id: commentThread.id,
|
||||
commentThreadTargets:
|
||||
commentThread.commentThreadTargets ?? [],
|
||||
}}
|
||||
/>
|
||||
}
|
||||
label="Relations"
|
||||
/>
|
||||
</PropertyBox>
|
||||
</StyledTopContainer>
|
||||
<CommentThreadBodyEditor
|
||||
commentThread={commentThread}
|
||||
onChange={updateTitleFromBody}
|
||||
/>
|
||||
{showComment && (
|
||||
<CommentThreadComments
|
||||
commentThread={{
|
||||
id: commentThread.id,
|
||||
comments: commentThread.comments ?? [],
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
@ -3,14 +3,13 @@ import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/comments/services';
|
||||
import { GET_COMPANIES } from '@/companies/services';
|
||||
import { GET_PEOPLE } from '@/people/services';
|
||||
import { IconTrash } from '@/ui/icons';
|
||||
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
|
||||
import { useDeleteCommentThreadMutation } from '~/generated/graphql';
|
||||
|
||||
import { GET_COMMENT_THREADS_BY_TARGETS } from '../services';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
cursor: pointer;
|
||||
@ -0,0 +1,36 @@
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { commentableEntityArrayState } from '@/comments/states/commentableEntityArrayState';
|
||||
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
|
||||
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
||||
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 { Timeline } from '../timeline/Timeline';
|
||||
|
||||
export function RightDrawerTimeline() {
|
||||
const [commentableEntityArray] = useRecoilState(commentableEntityArrayState);
|
||||
|
||||
useHotkeysScopeOnMountOnly({
|
||||
scope: InternalHotkeysScope.RightDrawer,
|
||||
customScopes: { goto: false, 'command-menu': true },
|
||||
});
|
||||
|
||||
return (
|
||||
<RightDrawerPage>
|
||||
<RightDrawerTopBar title="Timeline" />
|
||||
<RightDrawerBody>
|
||||
{commentableEntityArray.map((commentableEntity) => (
|
||||
<Timeline
|
||||
key={commentableEntity.id}
|
||||
entity={{
|
||||
id: commentableEntity?.id ?? '',
|
||||
type: commentableEntity.type,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</RightDrawerBody>
|
||||
</RightDrawerPage>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { viewableCommentThreadIdState } from '@/comments/states/viewableCommentThreadIdState';
|
||||
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
|
||||
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
||||
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 { CommentThread } from '../CommentThread';
|
||||
|
||||
export function RightDrawerCreateCommentThread() {
|
||||
const commentThreadId = useRecoilValue(viewableCommentThreadIdState);
|
||||
|
||||
useHotkeysScopeOnMountOnly({
|
||||
scope: InternalHotkeysScope.RightDrawer,
|
||||
customScopes: { goto: false, 'command-menu': true },
|
||||
});
|
||||
|
||||
return (
|
||||
<RightDrawerPage>
|
||||
<RightDrawerTopBar
|
||||
title="New note"
|
||||
onSave={() => {
|
||||
return;
|
||||
}}
|
||||
/>
|
||||
<RightDrawerBody>
|
||||
{commentThreadId && (
|
||||
<CommentThread
|
||||
commentThreadId={commentThreadId}
|
||||
showComment={false}
|
||||
/>
|
||||
)}
|
||||
</RightDrawerBody>
|
||||
</RightDrawerPage>
|
||||
);
|
||||
}
|
||||
@ -1,21 +1,25 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { viewableCommentThreadIdState } from '@/comments/states/viewableCommentThreadIdState';
|
||||
import { useHotkeysScopeOnMountOnly } from '@/hotkeys/hooks/useHotkeysScopeOnMountOnly';
|
||||
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
||||
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';
|
||||
import { CommentThread } from '../CommentThread';
|
||||
|
||||
export function RightDrawerCreateCommentThread() {
|
||||
export function RightDrawerEditCommentThread() {
|
||||
useHotkeysScopeOnMountOnly({
|
||||
scope: InternalHotkeysScope.RightDrawer,
|
||||
customScopes: { goto: false, 'command-menu': true },
|
||||
});
|
||||
const commentThreadId = useRecoilValue(viewableCommentThreadIdState);
|
||||
return (
|
||||
<RightDrawerPage>
|
||||
<RightDrawerTopBar title="New comment" />
|
||||
<RightDrawerTopBar title="" />
|
||||
<RightDrawerBody>
|
||||
<CommentThreadCreateMode />
|
||||
{commentThreadId && <CommentThread commentThreadId={commentThreadId} />}
|
||||
</RightDrawerBody>
|
||||
</RightDrawerPage>
|
||||
);
|
||||
@ -41,9 +41,8 @@ const StyledChip = styled.div`
|
||||
const StyledCount = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
|
||||
font-weight: 500;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
288
front/src/modules/comments/components/timeline/Timeline.tsx
Normal file
288
front/src/modules/comments/components/timeline/Timeline.tsx
Normal file
@ -0,0 +1,288 @@
|
||||
import React from 'react';
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useOpenCommentThreadRightDrawer } from '@/comments/hooks/useOpenCommentThreadRightDrawer';
|
||||
import { useOpenCreateCommentThreadDrawer } from '@/comments/hooks/useOpenCreateCommentThreadDrawer';
|
||||
import { CommentableEntity } from '@/comments/types/CommentableEntity';
|
||||
import { CommentThreadForDrawer } from '@/comments/types/CommentThreadForDrawer';
|
||||
import { TableActionBarButtonToggleComments } from '@/ui/components/table/action-bar/TableActionBarButtonOpenComments';
|
||||
import { IconCirclePlus, IconNotes } from '@/ui/icons/index';
|
||||
import {
|
||||
beautifyExactDate,
|
||||
beautifyPastDateRelativeToNow,
|
||||
} from '@/utils/datetime/date-utils';
|
||||
import {
|
||||
SortOrder,
|
||||
useGetCommentThreadsByTargetsQuery,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
const StyledMainContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledTimelineContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
justify-content: flex-start;
|
||||
overflow-y: auto;
|
||||
padding: 12px 16px 12px 16px;
|
||||
`;
|
||||
|
||||
const StyledTimelineEmptyContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledEmptyTimelineTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.xxl};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.md};
|
||||
`;
|
||||
|
||||
const StyledEmptyTimelineSubTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.extraLight};
|
||||
font-size: ${({ theme }) => theme.font.size.xxl};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.md};
|
||||
`;
|
||||
|
||||
const StyledTimelineItemContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
height: 20px;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
`;
|
||||
|
||||
const StyledItemTitleContainer = styled.div`
|
||||
align-content: flex-start;
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px 8px;
|
||||
height: 20px;
|
||||
span {
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledItemTitleDate = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
`;
|
||||
|
||||
const StyledVerticalLineContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
`;
|
||||
|
||||
const StyledVerticalLine = styled.div`
|
||||
align-self: stretch;
|
||||
background: ${({ theme }) => theme.border.color.light};
|
||||
flex-shrink: 0;
|
||||
width: 2px;
|
||||
`;
|
||||
|
||||
const StyledCardContainer = styled.div`
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 4px 0px 20px 0px;
|
||||
`;
|
||||
|
||||
const StyledCard = styled.div`
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
max-width: 400px;
|
||||
padding: 12px;
|
||||
`;
|
||||
|
||||
const StyledCardTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.lg};
|
||||
`;
|
||||
|
||||
const StyledCardContent = styled.div`
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
-webkit-line-clamp: 3;
|
||||
align-self: stretch;
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const StyledTooltip = styled(Tooltip)`
|
||||
background-color: ${({ theme }) => theme.background.primary};
|
||||
|
||||
box-shadow: 0px 2px 4px 3px
|
||||
${({ theme }) => theme.background.transparent.light};
|
||||
|
||||
box-shadow: 2px 4px 16px 6px
|
||||
${({ theme }) => theme.background.transparent.light};
|
||||
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
|
||||
opacity: 1;
|
||||
padding: 8px;
|
||||
`;
|
||||
|
||||
const StyledTopActionBar = styled.div`
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
backdrop-filter: blur(5px);
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-top-right-radius: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
left: 0px;
|
||||
padding: 12px 16px 12px 16px;
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
`;
|
||||
|
||||
export function Timeline({ entity }: { entity: CommentableEntity }) {
|
||||
const { data: queryResult } = useGetCommentThreadsByTargetsQuery({
|
||||
variables: {
|
||||
commentThreadTargetIds: [entity.id],
|
||||
orderBy: [
|
||||
{
|
||||
createdAt: SortOrder.Desc,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const openCommentThreadRightDrawer = useOpenCommentThreadRightDrawer();
|
||||
|
||||
const openCreateCommandThread = useOpenCreateCommentThreadDrawer();
|
||||
|
||||
const commentThreads: CommentThreadForDrawer[] =
|
||||
queryResult?.findManyCommentThreads ?? [];
|
||||
|
||||
if (!commentThreads.length) {
|
||||
return (
|
||||
<StyledTimelineEmptyContainer>
|
||||
<StyledEmptyTimelineTitle>No activity yet</StyledEmptyTimelineTitle>
|
||||
<StyledEmptyTimelineSubTitle>Create one:</StyledEmptyTimelineSubTitle>
|
||||
<TableActionBarButtonToggleComments
|
||||
onClick={() => openCreateCommandThread(entity)}
|
||||
/>
|
||||
</StyledTimelineEmptyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledMainContainer>
|
||||
<StyledTopActionBar>
|
||||
<StyledTimelineItemContainer>
|
||||
<StyledIconContainer>
|
||||
<IconCirclePlus />
|
||||
</StyledIconContainer>
|
||||
|
||||
<TableActionBarButtonToggleComments
|
||||
onClick={() => openCreateCommandThread(entity)}
|
||||
/>
|
||||
</StyledTimelineItemContainer>
|
||||
</StyledTopActionBar>
|
||||
<StyledTimelineContainer>
|
||||
{commentThreads.map((commentThread) => {
|
||||
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(
|
||||
commentThread.createdAt,
|
||||
);
|
||||
const exactCreatedAt = beautifyExactDate(commentThread.createdAt);
|
||||
const body = JSON.parse(commentThread.body ?? '{}')[0]?.content[0]
|
||||
?.text;
|
||||
|
||||
return (
|
||||
<React.Fragment key={commentThread.id}>
|
||||
<StyledTimelineItemContainer>
|
||||
<StyledIconContainer>
|
||||
<IconNotes />
|
||||
</StyledIconContainer>
|
||||
<StyledItemTitleContainer>
|
||||
<span>
|
||||
{commentThread.author.firstName}{' '}
|
||||
{commentThread.author.lastName}
|
||||
</span>
|
||||
created a note
|
||||
</StyledItemTitleContainer>
|
||||
<StyledItemTitleDate id={`id-${commentThread.id}`}>
|
||||
{beautifiedCreatedAt} ago
|
||||
</StyledItemTitleDate>
|
||||
<StyledTooltip
|
||||
anchorSelect={`#id-${commentThread.id}`}
|
||||
content={exactCreatedAt}
|
||||
clickable
|
||||
noArrow
|
||||
/>
|
||||
</StyledTimelineItemContainer>
|
||||
<StyledTimelineItemContainer>
|
||||
<StyledVerticalLineContainer>
|
||||
<StyledVerticalLine></StyledVerticalLine>
|
||||
</StyledVerticalLineContainer>
|
||||
<StyledCardContainer>
|
||||
<StyledCard
|
||||
onClick={() =>
|
||||
openCommentThreadRightDrawer(commentThread.id)
|
||||
}
|
||||
>
|
||||
<StyledCardTitle>
|
||||
{commentThread.title ? commentThread.title : '(No title)'}
|
||||
</StyledCardTitle>
|
||||
<StyledCardContent>
|
||||
{body ? body : '(No content)'}
|
||||
</StyledCardContent>
|
||||
</StyledCard>
|
||||
</StyledCardContainer>
|
||||
</StyledTimelineItemContainer>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</StyledTimelineContainer>
|
||||
</StyledMainContainer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user