feat: rename comment thread into activity (#939)

* feat: rename commentThread into activity server

* feat: rename commentThread into activity front

* feat: migration only create tables


feat: migration only create tables

* Update activities

* fix: rebase partial fix

* fix: all rebase problems and drop activity target alter

* fix: lint

* Update migration

* Update migration

* Fix conflicts

* Fix conflicts

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Jérémy M
2023-07-28 08:22:16 +02:00
committed by GitHub
parent fcdde024a3
commit d0641084f9
95 changed files with 2112 additions and 1725 deletions

View File

@ -2,7 +2,7 @@ import type { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { CommentThreadActionBar } from '../../right-drawer/components/CommentThreadActionBar';
import { ActivityActionBar } from '../../right-drawer/components/ActivityActionBar';
import { Comment } from '../Comment';
import { mockComment, mockCommentWithLongValues } from './mock-comment';
@ -15,7 +15,7 @@ const meta: Meta<typeof Comment> = {
actionBar: {
type: 'boolean',
mapping: {
true: <CommentThreadActionBar commentThreadId="test-id" />,
true: <ActivityActionBar activityId="test-id" />,
false: undefined,
},
},

View File

@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/react';
import { DateTime } from 'luxon';
import { CommentThreadActionBar } from '@/activities/right-drawer/components/CommentThreadActionBar';
import { ActivityActionBar } from '@/activities/right-drawer/components/ActivityActionBar';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { avatarUrl } from '~/testing/mock-data/users';
@ -17,7 +17,7 @@ const meta: Meta<typeof CommentHeader> = {
actionBar: {
type: 'boolean',
mapping: {
true: <CommentThreadActionBar commentThreadId="test-id" />,
true: <ActivityActionBar activityId="test-id" />,
false: undefined,
},
},

View File

@ -6,24 +6,21 @@ import styled from '@emotion/styled';
import debounce from 'lodash.debounce';
import { BlockEditor } from '@/ui/editor/components/BlockEditor';
import {
CommentThread,
useUpdateCommentThreadMutation,
} from '~/generated/graphql';
import { Activity, useUpdateActivityMutation } from '~/generated/graphql';
import { GET_COMMENT_THREADS_BY_TARGETS } from '../queries/select';
import { GET_ACTIVITIES_BY_TARGETS } from '../queries/select';
const BlockNoteStyledContainer = styled.div`
width: 100%;
`;
type OwnProps = {
commentThread: Pick<CommentThread, 'id' | 'body'>;
onChange?: (commentThreadBody: string) => void;
activity: Pick<Activity, 'id' | 'body'>;
onChange?: (activityBody: string) => void;
};
export function CommentThreadBodyEditor({ commentThread, onChange }: OwnProps) {
const [updateCommentThreadMutation] = useUpdateCommentThreadMutation();
export function ActivityBodyEditor({ activity, onChange }: OwnProps) {
const [updateActivityMutation] = useUpdateActivityMutation();
const [body, setBody] = useState<string | null>(null);
@ -34,26 +31,22 @@ export function CommentThreadBodyEditor({ commentThread, onChange }: OwnProps) {
}, [body, onChange]);
const debounceOnChange = useMemo(() => {
function onInternalChange(commentThreadBody: string) {
setBody(commentThreadBody);
updateCommentThreadMutation({
function onInternalChange(activityBody: string) {
setBody(activityBody);
updateActivityMutation({
variables: {
id: commentThread.id,
body: commentThreadBody,
id: activity.id,
body: activityBody,
},
refetchQueries: [
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
],
refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''],
});
}
return debounce(onInternalChange, 200);
}, [commentThread, updateCommentThreadMutation, setBody]);
}, [activity, updateActivityMutation, setBody]);
const editor: BlockNoteEditor | null = useBlockNote({
initialContent: commentThread.body
? JSON.parse(commentThread.body)
: undefined,
initialContent: activity.body ? JSON.parse(activity.body) : undefined,
editorDOMAttributes: { class: 'editor' },
onEditorContentChange: (editor) => {
debounceOnChange(JSON.stringify(editor.topLevelBlocks) ?? '');

View File

@ -6,15 +6,15 @@ import { v4 } from 'uuid';
import { currentUserState } from '@/auth/states/currentUserState';
import { useIsMobile } from '@/ui/hooks/useIsMobile';
import { AutosizeTextInput } from '@/ui/input/components/AutosizeTextInput';
import { CommentThread, useCreateCommentMutation } from '~/generated/graphql';
import { Activity, useCreateCommentMutation } from '~/generated/graphql';
import { isNonEmptyString } from '~/utils/isNonEmptyString';
import { Comment } from '../comment/Comment';
import { GET_COMMENT_THREAD } from '../queries';
import { GET_ACTIVITY } from '../queries';
import { CommentForDrawer } from '../types/CommentForDrawer';
type OwnProps = {
commentThread: Pick<CommentThread, 'id'> & {
activity: Pick<Activity, 'id'> & {
comments: Array<CommentForDrawer>;
};
};
@ -52,7 +52,7 @@ const StyledThreadCommentTitle = styled.div`
text-transform: uppercase;
`;
export function CommentThreadComments({ commentThread }: OwnProps) {
export function ActivityComments({ activity }: OwnProps) {
const [createCommentMutation] = useCreateCommentMutation();
const currentUser = useRecoilValue(currentUserState);
@ -69,21 +69,21 @@ export function CommentThreadComments({ commentThread }: OwnProps) {
variables: {
commentId: v4(),
authorId: currentUser?.id ?? '',
commentThreadId: commentThread?.id ?? '',
activityId: activity?.id ?? '',
commentText: commentText,
createdAt: new Date().toISOString(),
},
refetchQueries: [getOperationName(GET_COMMENT_THREAD) ?? ''],
refetchQueries: [getOperationName(GET_ACTIVITY) ?? ''],
});
}
return (
<>
{commentThread?.comments.length > 0 && (
{activity?.comments.length > 0 && (
<>
<StyledThreadItemListContainer>
<StyledThreadCommentTitle>Comments</StyledThreadCommentTitle>
{commentThread?.comments?.map((comment) => (
{activity?.comments?.map((comment) => (
<Comment key={comment.id} comment={comment} />
))}
</StyledThreadItemListContainer>

View File

@ -4,17 +4,17 @@ import { Button, ButtonVariant } from '@/ui/button/components/Button';
import { ButtonGroup } from '@/ui/button/components/ButtonGroup';
import { IconCheckbox, IconNotes, IconTimelineEvent } from '@/ui/icon/index';
type CommentThreadCreateButtonProps = {
type ActivityCreateButtonProps = {
onNoteClick?: () => void;
onTaskClick?: () => void;
onActivityClick?: () => void;
};
export function CommentThreadCreateButton({
export function ActivityCreateButton({
onNoteClick,
onTaskClick,
onActivityClick,
}: CommentThreadCreateButtonProps) {
}: ActivityCreateButtonProps) {
const theme = useTheme();
return (
<ButtonGroup variant={ButtonVariant.Secondary}>

View File

@ -2,26 +2,26 @@ import React, { useCallback, useState } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { CommentThreadBodyEditor } from '@/activities/components/CommentThreadBodyEditor';
import { CommentThreadComments } from '@/activities/components/CommentThreadComments';
import { CommentThreadRelationPicker } from '@/activities/components/CommentThreadRelationPicker';
import { CommentThreadTypeDropdown } from '@/activities/components/CommentThreadTypeDropdown';
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/activities/queries';
import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor';
import { ActivityComments } from '@/activities/components/ActivityComments';
import { ActivityRelationPicker } from '@/activities/components/ActivityRelationPicker';
import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown';
import { GET_ACTIVITIES_BY_TARGETS } from '@/activities/queries';
import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox';
import { PropertyBoxItem } from '@/ui/editable-field/property-box/components/PropertyBoxItem';
import { useIsMobile } from '@/ui/hooks/useIsMobile';
import { IconArrowUpRight } from '@/ui/icon/index';
import {
CommentThread,
CommentThreadTarget,
useUpdateCommentThreadMutation,
Activity,
ActivityTarget,
useUpdateActivityMutation,
} from '~/generated/graphql';
import { debounce } from '~/utils/debounce';
import { CommentThreadActionBar } from '../right-drawer/components/CommentThreadActionBar';
import { ActivityActionBar } from '../right-drawer/components/ActivityActionBar';
import { CommentForDrawer } from '../types/CommentForDrawer';
import { CommentThreadTitle } from './CommentThreadTitle';
import { ActivityTitle } from './ActivityTitle';
import '@blocknote/core/style.css';
@ -65,64 +65,57 @@ const StyledTopActionsContainer = styled.div`
`;
type OwnProps = {
commentThread: Pick<
CommentThread,
'id' | 'title' | 'body' | 'type' | 'completedAt'
> & {
activity: Pick<Activity, 'id' | 'title' | 'body' | 'type' | 'completedAt'> & {
comments?: Array<CommentForDrawer> | null;
} & {
commentThreadTargets?: Array<
Pick<CommentThreadTarget, 'id' | 'commentableId' | 'commentableType'>
activityTargets?: Array<
Pick<ActivityTarget, 'id' | 'commentableId' | 'commentableType'>
> | null;
};
showComment?: boolean;
autoFillTitle?: boolean;
};
export function CommentThreadEditor({
commentThread,
export function ActivityEditor({
activity,
showComment = true,
autoFillTitle = false,
}: OwnProps) {
const [hasUserManuallySetTitle, setHasUserManuallySetTitle] =
useState<boolean>(false);
const [title, setTitle] = useState<string | null>(commentThread.title ?? '');
const [title, setTitle] = useState<string | null>(activity.title ?? '');
const [completedAt, setCompletedAt] = useState<string | null>(
commentThread.completedAt ?? '',
activity.completedAt ?? '',
);
const [updateCommentThreadMutation] = useUpdateCommentThreadMutation();
const [updateActivityMutation] = useUpdateActivityMutation();
const updateTitle = useCallback(
(newTitle: string) => {
updateCommentThreadMutation({
updateActivityMutation({
variables: {
id: commentThread.id,
id: activity.id,
title: newTitle ?? '',
},
refetchQueries: [
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
],
refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''],
});
},
[commentThread, updateCommentThreadMutation],
[activity, updateActivityMutation],
);
const handleActivityCompletionChange = useCallback(
(value: boolean) => {
updateCommentThreadMutation({
updateActivityMutation({
variables: {
id: commentThread.id,
id: activity.id,
completedAt: value ? new Date().toISOString() : null,
},
refetchQueries: [
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
],
refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''],
});
setCompletedAt(value ? new Date().toISOString() : null);
},
[commentThread, updateCommentThreadMutation],
[activity, updateActivityMutation],
);
const debouncedUpdateTitle = debounce(updateTitle, 200);
@ -135,7 +128,7 @@ export function CommentThreadEditor({
}
}
if (!commentThread) {
if (!activity) {
return <></>;
}
@ -144,13 +137,13 @@ export function CommentThreadEditor({
<StyledUpperPartContainer>
<StyledTopContainer>
<StyledTopActionsContainer>
<CommentThreadTypeDropdown commentThread={commentThread} />
<CommentThreadActionBar commentThreadId={commentThread?.id ?? ''} />
<ActivityTypeDropdown activity={activity} />
<ActivityActionBar activityId={activity?.id ?? ''} />
</StyledTopActionsContainer>
<CommentThreadTitle
<ActivityTitle
title={title ?? ''}
completed={!!completedAt}
type={commentThread.type}
type={activity.type}
onTitleChange={(newTitle) => {
setTitle(newTitle);
setHasUserManuallySetTitle(true);
@ -162,11 +155,10 @@ export function CommentThreadEditor({
<PropertyBoxItem
icon={<IconArrowUpRight />}
value={
<CommentThreadRelationPicker
commentThread={{
id: commentThread.id,
commentThreadTargets:
commentThread.commentThreadTargets ?? [],
<ActivityRelationPicker
activity={{
id: activity.id,
activityTargets: activity.activityTargets ?? [],
}}
/>
}
@ -174,16 +166,16 @@ export function CommentThreadEditor({
/>
</PropertyBox>
</StyledTopContainer>
<CommentThreadBodyEditor
commentThread={commentThread}
<ActivityBodyEditor
activity={activity}
onChange={updateTitleFromBody}
/>
</StyledUpperPartContainer>
{showComment && (
<CommentThreadComments
commentThread={{
id: commentThread.id,
comments: commentThread.comments ?? [],
<ActivityComments
activity={{
id: activity.id,
comments: activity.comments ?? [],
}}
/>
)}

View File

@ -18,19 +18,15 @@ import { useScopedHotkeys } from '@/ui/hotkey/hooks/useScopedHotkeys';
import { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
import { MultipleEntitySelect } from '@/ui/relation-picker/components/MultipleEntitySelect';
import { RelationPickerHotkeyScope } from '@/ui/relation-picker/types/RelationPickerHotkeyScope';
import {
CommentableType,
CommentThread,
CommentThreadTarget,
} from '~/generated/graphql';
import { Activity, ActivityTarget, CommentableType } from '~/generated/graphql';
import { useHandleCheckableCommentThreadTargetChange } from '../hooks/useHandleCheckableCommentThreadTargetChange';
import { useHandleCheckableActivityTargetChange } from '../hooks/useHandleCheckableActivityTargetChange';
import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '../utils/flatMapAndSortEntityForSelectArrayByName';
type OwnProps = {
commentThread?: Pick<CommentThread, 'id'> & {
commentThreadTargets: Array<
Pick<CommentThreadTarget, 'id' | 'commentableId' | 'commentableType'>
activity?: Pick<Activity, 'id'> & {
activityTargets: Array<
Pick<ActivityTarget, 'id' | 'commentableId' | 'commentableType'>
>;
};
};
@ -75,7 +71,7 @@ const StyledMenuWrapper = styled.div`
z-index: ${({ theme }) => theme.lastLayerZIndex};
`;
export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
export function ActivityRelationPicker({ activity }: OwnProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [searchFilter, setSearchFilter] = useState('');
const [selectedEntityIds, setSelectedEntityIds] = useState<
@ -88,17 +84,18 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
const initialPeopleIds = useMemo(
() =>
commentThread?.commentThreadTargets
activity?.activityTargets
?.filter((relation) => relation.commentableType === 'Person')
.map((relation) => relation.commentableId) ?? [],
[commentThread?.commentThreadTargets],
[activity?.activityTargets],
);
const initialCompanyIds = useMemo(
() =>
commentThread?.commentThreadTargets
activity?.activityTargets
?.filter((relation) => relation.commentableType === 'Company')
.map((relation) => relation.commentableId) ?? [],
[commentThread?.commentThreadTargets],
[activity?.activityTargets],
);
const initialSelectedEntityIds = useMemo(
@ -135,8 +132,8 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
companiesForMultiSelect.entitiesToSelect,
]);
const handleCheckItemsChange = useHandleCheckableCommentThreadTargetChange({
commentThread,
const handleCheckItemsChange = useHandleCheckableActivityTargetChange({
activity,
});
const exitEditMode = useCallback(() => {

View File

@ -51,7 +51,7 @@ type OwnProps = {
onCompletionChange: (value: boolean) => void;
};
export function CommentThreadTitle({
export function ActivityTitle({
title,
completed,
type,

View File

@ -7,17 +7,17 @@ import {
ChipVariant,
} from '@/ui/chip/components/Chip';
import { IconPhone } from '@/ui/icon';
import { CommentThread } from '~/generated/graphql';
import { Activity } from '~/generated/graphql';
type OwnProps = {
commentThread: Pick<CommentThread, 'type'>;
activity: Pick<Activity, 'type'>;
};
export function CommentThreadTypeDropdown({ commentThread }: OwnProps) {
export function ActivityTypeDropdown({ activity }: OwnProps) {
const theme = useTheme();
return (
<Chip
label={commentThread.type}
label={activity.type}
leftComponent={<IconPhone size={theme.icon.size.md} />}
size={ChipSize.Large}
accent={ChipAccent.TextSecondary}

View File

@ -4,17 +4,17 @@ import type { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedCommentThreads } from '~/testing/mock-data/comment-threads';
import { mockedActivities } from '~/testing/mock-data/activities';
import { CommentThreadRelationPicker } from '../CommentThreadRelationPicker';
import { ActivityRelationPicker } from '../ActivityRelationPicker';
const StyledContainer = styled.div`
width: 400px;
`;
const meta: Meta<typeof CommentThreadRelationPicker> = {
title: 'Modules/Comments/CommentThreadRelationPicker',
component: CommentThreadRelationPicker,
const meta: Meta<typeof ActivityRelationPicker> = {
title: 'Modules/Comments/ActivityRelationPicker',
component: ActivityRelationPicker,
decorators: [
(Story) => (
<MemoryRouter>
@ -25,13 +25,13 @@ const meta: Meta<typeof CommentThreadRelationPicker> = {
),
ComponentDecorator,
],
args: { commentThread: mockedCommentThreads[0] },
args: { activity: mockedActivities[0] },
parameters: {
msw: graphqlMocks,
},
};
export default meta;
type Story = StoryObj<typeof CommentThreadRelationPicker>;
type Story = StoryObj<typeof ActivityRelationPicker>;
export const Default: Story = {};

View File

@ -0,0 +1,84 @@
import { getOperationName } from '@apollo/client/utilities';
import { v4 } from 'uuid';
import { GET_COMPANIES } from '@/companies/queries';
import { GET_PEOPLE } from '@/people/queries';
import {
Activity,
ActivityTarget,
useAddActivityTargetsOnActivityMutation,
useRemoveActivityTargetsOnActivityMutation,
} from '~/generated/graphql';
import { GET_ACTIVITIES_BY_TARGETS } from '../queries';
import { CommentableEntityForSelect } from '../types/CommentableEntityForSelect';
export function useHandleCheckableActivityTargetChange({
activity,
}: {
activity?: Pick<Activity, 'id'> & {
activityTargets: Array<
Pick<ActivityTarget, 'id' | 'commentableId' | 'commentableType'>
>;
};
}) {
const [addActivityTargetsOnActivity] =
useAddActivityTargetsOnActivityMutation({
refetchQueries: [
getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? '',
],
});
const [removeActivityTargetsOnActivity] =
useRemoveActivityTargetsOnActivityMutation({
refetchQueries: [
getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? '',
],
});
return async function handleCheckItemsChange(
entityValues: Record<string, boolean>,
entities: CommentableEntityForSelect[],
) {
if (!activity) {
return;
}
const currentEntityIds = activity.activityTargets.map(
({ commentableId }) => commentableId,
);
const entitiesToAdd = entities.filter(
({ id }) => entityValues[id] && !currentEntityIds.includes(id),
);
if (entitiesToAdd.length)
await addActivityTargetsOnActivity({
variables: {
activityId: activity.id,
activityTargetInputs: entitiesToAdd.map((entity) => ({
id: v4(),
createdAt: new Date().toISOString(),
commentableType: entity.entityType,
commentableId: entity.id,
})),
},
});
const activityTargetIdsToDelete = activity.activityTargets
.filter(({ commentableId }) => !entityValues[commentableId])
.map(({ id }) => id);
if (activityTargetIdsToDelete.length)
await removeActivityTargetsOnActivity({
variables: {
activityId: activity.id,
activityTargetIds: activityTargetIdsToDelete,
},
});
};
}

View File

@ -1,84 +0,0 @@
import { getOperationName } from '@apollo/client/utilities';
import { v4 } from 'uuid';
import { GET_COMPANIES } from '@/companies/queries';
import { GET_PEOPLE } from '@/people/queries';
import {
CommentThread,
CommentThreadTarget,
useAddCommentThreadTargetsOnCommentThreadMutation,
useRemoveCommentThreadTargetsOnCommentThreadMutation,
} from '~/generated/graphql';
import { GET_COMMENT_THREADS_BY_TARGETS } from '../queries';
import { CommentableEntityForSelect } from '../types/CommentableEntityForSelect';
export function useHandleCheckableCommentThreadTargetChange({
commentThread,
}: {
commentThread?: Pick<CommentThread, 'id'> & {
commentThreadTargets: Array<
Pick<CommentThreadTarget, 'id' | 'commentableId'>
>;
};
}) {
const [addCommentThreadTargetsOnCommentThread] =
useAddCommentThreadTargetsOnCommentThreadMutation({
refetchQueries: [
getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
],
});
const [removeCommentThreadTargetsOnCommentThread] =
useRemoveCommentThreadTargetsOnCommentThreadMutation({
refetchQueries: [
getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
],
});
return async function handleCheckItemsChange(
entityValues: Record<string, boolean>,
entities: CommentableEntityForSelect[],
) {
if (!commentThread) {
return;
}
const currentEntityIds = commentThread.commentThreadTargets.map(
({ commentableId }) => commentableId,
);
const entitiesToAdd = entities.filter(
({ id }) => entityValues[id] && !currentEntityIds.includes(id),
);
if (entitiesToAdd.length)
await addCommentThreadTargetsOnCommentThread({
variables: {
commentThreadId: commentThread.id,
commentThreadTargetInputs: entitiesToAdd.map((entity) => ({
id: v4(),
createdAt: new Date().toISOString(),
commentableType: entity.entityType,
commentableId: entity.id,
})),
},
});
const commentThreadTargetIdsToDelete = commentThread.commentThreadTargets
.filter(({ commentableId }) => !entityValues[commentableId])
.map(({ id }) => id);
if (commentThreadTargetIdsToDelete.length)
await removeCommentThreadTargetsOnCommentThread({
variables: {
commentThreadId: commentThread.id,
commentThreadTargetIds: commentThreadTargetIdsToDelete,
},
});
};
}

View File

@ -5,18 +5,16 @@ import { useRightDrawer } from '@/ui/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/right-drawer/types/RightDrawerPages';
import { viewableCommentThreadIdState } from '../states/viewableCommentThreadIdState';
import { viewableActivityIdState } from '../states/viewableActivityIdState';
export function useOpenCommentThreadRightDrawer() {
export function useOpenActivityRightDrawer() {
const { openRightDrawer } = useRightDrawer();
const [, setViewableCommentThreadId] = useRecoilState(
viewableCommentThreadIdState,
);
const [, setViewableActivityId] = useRecoilState(viewableActivityIdState);
const setHotkeyScope = useSetHotkeyScope();
return function openCommentThreadRightDrawer(commentThreadId: string) {
return function openActivityRightDrawer(activityId: string) {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableCommentThreadId(commentThreadId);
openRightDrawer(RightDrawerPages.EditCommentThread);
setViewableActivityId(activityId);
openRightDrawer(RightDrawerPages.EditActivity);
};
}

View File

@ -9,40 +9,35 @@ import { useSetHotkeyScope } from '@/ui/hotkey/hooks/useSetHotkeyScope';
import { useRightDrawer } from '@/ui/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/right-drawer/types/RightDrawerPages';
import {
ActivityType,
useCreateCommentThreadMutation,
} from '~/generated/graphql';
import { ActivityType, useCreateActivityMutation } from '~/generated/graphql';
import { GET_COMMENT_THREAD, GET_COMMENT_THREADS_BY_TARGETS } from '../queries';
import { GET_ACTIVITIES_BY_TARGETS, GET_ACTIVITY } from '../queries';
import { commentableEntityArrayState } from '../states/commentableEntityArrayState';
import { viewableCommentThreadIdState } from '../states/viewableCommentThreadIdState';
import { viewableActivityIdState } from '../states/viewableActivityIdState';
import { CommentableEntity } from '../types/CommentableEntity';
export function useOpenCreateCommentThreadDrawer() {
export function useOpenCreateActivityDrawer() {
const { openRightDrawer } = useRightDrawer();
const [createCommentThreadMutation] = useCreateCommentThreadMutation();
const [createActivityMutation] = useCreateActivityMutation();
const currentUser = useRecoilValue(currentUserState);
const setHotkeyScope = useSetHotkeyScope();
const [, setCommentableEntityArray] = useRecoilState(
commentableEntityArrayState,
);
const [, setViewableCommentThreadId] = useRecoilState(
viewableCommentThreadIdState,
);
const [, setViewableActivityId] = useRecoilState(viewableActivityIdState);
return function openCreateCommentThreadDrawer(
return function openCreateActivityDrawer(
entity: CommentableEntity,
type: ActivityType,
) {
createCommentThreadMutation({
createActivityMutation({
variables: {
authorId: currentUser?.id ?? '',
commentThreadId: v4(),
activityId: v4(),
createdAt: new Date().toISOString(),
type: type,
commentThreadTargetArray: [
activityTargetArray: [
{
commentableId: entity.id,
commentableType: entity.type,
@ -54,14 +49,14 @@ export function useOpenCreateCommentThreadDrawer() {
refetchQueries: [
getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_COMMENT_THREAD) ?? '',
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
getOperationName(GET_ACTIVITY) ?? '',
getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? '',
],
onCompleted(data) {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableCommentThreadId(data.createOneCommentThread.id);
setViewableActivityId(data.createOneActivity.id);
setCommentableEntityArray([entity]);
openRightDrawer(RightDrawerPages.CreateCommentThread);
openRightDrawer(RightDrawerPages.CreateActivity);
},
});
};

View File

@ -13,21 +13,19 @@ import { selectedRowIdsSelector } from '@/ui/table/states/selectedRowIdsSelector
import {
ActivityType,
CommentableType,
useCreateCommentThreadMutation,
useCreateActivityMutation,
} from '~/generated/graphql';
import { GET_COMMENT_THREAD, GET_COMMENT_THREADS_BY_TARGETS } from '../queries';
import { GET_ACTIVITIES_BY_TARGETS, GET_ACTIVITY } from '../queries';
import { commentableEntityArrayState } from '../states/commentableEntityArrayState';
import { viewableCommentThreadIdState } from '../states/viewableCommentThreadIdState';
import { viewableActivityIdState } from '../states/viewableActivityIdState';
import { CommentableEntity } from '../types/CommentableEntity';
export function useOpenCreateCommentThreadDrawerForSelectedRowIds() {
export function useOpenCreateActivityDrawerForSelectedRowIds() {
const { openRightDrawer } = useRightDrawer();
const [createCommentThreadMutation] = useCreateCommentThreadMutation();
const [createActivityMutation] = useCreateActivityMutation();
const currentUser = useRecoilValue(currentUserState);
const [, setViewableCommentThreadId] = useRecoilState(
viewableCommentThreadIdState,
);
const [, setViewableActivityId] = useRecoilState(viewableActivityIdState);
const setHotkeyScope = useSetHotkeyScope();
@ -47,13 +45,13 @@ export function useOpenCreateCommentThreadDrawerForSelectedRowIds() {
}),
);
createCommentThreadMutation({
createActivityMutation({
variables: {
authorId: currentUser?.id ?? '',
commentThreadId: v4(),
activityId: v4(),
createdAt: new Date().toISOString(),
type: ActivityType.Note,
commentThreadTargetArray: commentableEntityArray.map((entity) => ({
activityTargetArray: commentableEntityArray.map((entity) => ({
commentableId: entity.id,
commentableType: entity.type,
id: v4(),
@ -63,14 +61,14 @@ export function useOpenCreateCommentThreadDrawerForSelectedRowIds() {
refetchQueries: [
getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_COMMENT_THREAD) ?? '',
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
getOperationName(GET_ACTIVITY) ?? '',
getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? '',
],
onCompleted(data) {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableCommentThreadId(data.createOneCommentThread.id);
setViewableActivityId(data.createOneActivity.id);
setCommentableEntityArray(commentableEntityArray);
openRightDrawer(RightDrawerPages.CreateCommentThread);
openRightDrawer(RightDrawerPages.CreateActivity);
},
});
};

View File

@ -5,7 +5,7 @@ export const CREATE_COMMENT = gql`
$commentId: String!
$commentText: String!
$authorId: String!
$commentThreadId: String!
$activityId: String!
$createdAt: DateTime!
) {
createOneComment(
@ -14,7 +14,7 @@ export const CREATE_COMMENT = gql`
createdAt: $createdAt
body: $commentText
author: { connect: { id: $authorId } }
commentThread: { connect: { id: $commentThreadId } }
activity: { connect: { id: $activityId } }
}
) {
id
@ -27,32 +27,32 @@ export const CREATE_COMMENT = gql`
lastName
avatarUrl
}
commentThreadId
activityId
}
}
`;
export const CREATE_COMMENT_THREAD_WITH_COMMENT = gql`
mutation CreateCommentThread(
$commentThreadId: String!
export const CREATE_ACTIVITY_WITH_COMMENT = gql`
mutation CreateActivity(
$activityId: String!
$body: String
$title: String
$type: ActivityType!
$authorId: String!
$createdAt: DateTime!
$commentThreadTargetArray: [CommentThreadTargetCreateManyCommentThreadInput!]!
$activityTargetArray: [ActivityTargetCreateManyActivityInput!]!
) {
createOneCommentThread(
createOneActivity(
data: {
id: $commentThreadId
id: $activityId
createdAt: $createdAt
updatedAt: $createdAt
author: { connect: { id: $authorId } }
body: $body
title: $title
type: $type
commentThreadTargets: {
createMany: { data: $commentThreadTargetArray, skipDuplicates: true }
activityTargets: {
createMany: { data: $activityTargetArray, skipDuplicates: true }
}
}
) {
@ -61,11 +61,11 @@ export const CREATE_COMMENT_THREAD_WITH_COMMENT = gql`
updatedAt
authorId
type
commentThreadTargets {
activityTargets {
id
createdAt
updatedAt
commentThreadId
activityId
commentableType
commentableId
}

View File

@ -1,16 +1,14 @@
import { gql } from '@apollo/client';
export const GET_COMMENT_THREADS_BY_TARGETS = gql`
query GetCommentThreadsByTargets(
$commentThreadTargetIds: [String!]!
$orderBy: [CommentThreadOrderByWithRelationInput!]
export const GET_ACTIVITIES_BY_TARGETS = gql`
query GetActivitiesByTargets(
$activityTargetIds: [String!]!
$orderBy: [ActivityOrderByWithRelationInput!]
) {
findManyCommentThreads(
findManyActivities(
orderBy: $orderBy
where: {
commentThreadTargets: {
some: { commentableId: { in: $commentThreadTargetIds } }
}
activityTargets: { some: { commentableId: { in: $activityTargetIds } } }
}
) {
id
@ -38,18 +36,18 @@ export const GET_COMMENT_THREADS_BY_TARGETS = gql`
avatarUrl
}
}
commentThreadTargets {
activityTargets {
id
commentableId
commentableType
commentableId
}
}
}
`;
export const GET_COMMENT_THREAD = gql`
query GetCommentThread($commentThreadId: String!) {
findManyCommentThreads(where: { id: { equals: $commentThreadId } }) {
export const GET_ACTIVITY = gql`
query GetActivity($activityId: String!) {
findManyActivities(where: { id: { equals: $activityId } }) {
id
createdAt
body
@ -75,10 +73,10 @@ export const GET_COMMENT_THREAD = gql`
avatarUrl
}
}
commentThreadTargets {
activityTargets {
id
commentableId
commentableType
commentableId
}
}
}

View File

@ -1,22 +1,18 @@
import { gql } from '@apollo/client';
export const ADD_COMMENT_THREAD_TARGETS = gql`
mutation AddCommentThreadTargetsOnCommentThread(
$commentThreadId: String!
$commentThreadTargetInputs: [CommentThreadTargetCreateManyCommentThreadInput!]!
export const ADD_ACTIVITY_TARGETS = gql`
mutation AddActivityTargetsOnActivity(
$activityId: String!
$activityTargetInputs: [ActivityTargetCreateManyActivityInput!]!
) {
updateOneCommentThread(
where: { id: $commentThreadId }
data: {
commentThreadTargets: {
createMany: { data: $commentThreadTargetInputs }
}
}
updateOneActivity(
where: { id: $activityId }
data: { activityTargets: { createMany: { data: $activityTargetInputs } } }
) {
id
createdAt
updatedAt
commentThreadTargets {
activityTargets {
id
createdAt
updatedAt
@ -27,23 +23,21 @@ export const ADD_COMMENT_THREAD_TARGETS = gql`
}
`;
export const REMOVE_COMMENT_THREAD_TARGETS = gql`
mutation RemoveCommentThreadTargetsOnCommentThread(
$commentThreadId: String!
$commentThreadTargetIds: [String!]!
export const REMOVE_ACTIVITY_TARGETS = gql`
mutation RemoveActivityTargetsOnActivity(
$activityId: String!
$activityTargetIds: [String!]!
) {
updateOneCommentThread(
where: { id: $commentThreadId }
updateOneActivity(
where: { id: $activityId }
data: {
commentThreadTargets: {
deleteMany: { id: { in: $commentThreadTargetIds } }
}
activityTargets: { deleteMany: { id: { in: $activityTargetIds } } }
}
) {
id
createdAt
updatedAt
commentThreadTargets {
activityTargets {
id
createdAt
updatedAt
@ -54,23 +48,23 @@ export const REMOVE_COMMENT_THREAD_TARGETS = gql`
}
`;
export const DELETE_COMMENT_THREAD = gql`
mutation DeleteCommentThread($commentThreadId: String!) {
deleteManyCommentThreads(where: { id: { equals: $commentThreadId } }) {
export const DELETE_ACTIVITY = gql`
mutation DeleteActivity($activityId: String!) {
deleteManyActivities(where: { id: { equals: $activityId } }) {
count
}
}
`;
export const UPDATE_COMMENT_THREAD = gql`
mutation UpdateCommentThread(
export const UPDATE_ACTIVITY = gql`
mutation UpdateActivity(
$id: String!
$body: String
$title: String
$type: ActivityType
$completedAt: DateTime
) {
updateOneCommentThread(
updateOneActivity(
where: { id: $id }
data: {
body: $body

View File

@ -0,0 +1,201 @@
import React, { useEffect, useMemo, useState } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor';
import { ActivityComments } from '@/activities/components/ActivityComments';
import { ActivityRelationPicker } from '@/activities/components/ActivityRelationPicker';
import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown';
import { GET_ACTIVITY } from '@/activities/queries';
import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox';
import { PropertyBoxItem } from '@/ui/editable-field/property-box/components/PropertyBoxItem';
import { useIsMobile } from '@/ui/hooks/useIsMobile';
import { IconArrowUpRight } from '@/ui/icon/index';
import {
useGetActivityQuery,
useUpdateActivityMutation,
} from '~/generated/graphql';
import { debounce } from '~/utils/debounce';
import { ActivityActionBar } from './ActivityActionBar';
import '@blocknote/core/style.css';
const StyledContainer = styled.div`
box-sizing: border-box;
display: flex;
flex-direction: column;
height: 100%;
justify-content: space-between;
overflow-y: auto;
`;
const StyledUpperPartContainer = styled.div`
align-items: flex-start;
box-sizing: border-box;
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: ${({ theme }) =>
useIsMobile() ? 'none' : `1px solid ${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 = {
activityId: string;
showComment?: boolean;
autoFillTitle?: boolean;
};
export function Activity({
activityId,
showComment = true,
autoFillTitle = false,
}: OwnProps) {
const { data } = useGetActivityQuery({
variables: {
activityId: activityId ?? '',
},
skip: !activityId,
});
const activity = data?.findManyActivities[0];
const [hasUserManuallySetTitle, setHasUserManuallySetTitle] =
useState<boolean>(false);
const [title, setTitle] = useState<string | null>(null);
useEffect(() => {
if (!hasUserManuallySetTitle) {
setTitle(activity?.title ?? '');
}
}, [setTitle, activity?.title, hasUserManuallySetTitle]);
const [updateActivityMutation] = useUpdateActivityMutation();
const debounceUpdateTitle = useMemo(() => {
function updateTitle(title: string) {
if (activity) {
updateActivityMutation({
variables: {
id: activityId,
title: title ?? '',
},
refetchQueries: [getOperationName(GET_ACTIVITY) ?? ''],
optimisticResponse: {
__typename: 'Mutation',
updateOneActivity: {
__typename: 'Activity',
id: activityId,
title: title,
type: activity.type,
},
},
});
}
}
return debounce(updateTitle, 200);
}, [activityId, updateActivityMutation, activity]);
function updateTitleFromBody(body: string) {
const parsedTitle = JSON.parse(body)[0]?.content[0]?.text;
if (!hasUserManuallySetTitle && autoFillTitle) {
setTitle(parsedTitle);
debounceUpdateTitle(parsedTitle);
}
}
if (!activity) {
return <></>;
}
return (
<StyledContainer>
<StyledUpperPartContainer>
<StyledTopContainer>
<StyledTopActionsContainer>
<ActivityTypeDropdown activity={activity} />
<ActivityActionBar activityId={activity?.id ?? ''} />
</StyledTopActionsContainer>
<StyledEditableTitleInput
autoFocus
placeholder={`${activity.type} title (optional)`}
onChange={(event) => {
setHasUserManuallySetTitle(true);
setTitle(event.target.value);
debounceUpdateTitle(event.target.value);
}}
value={title ?? ''}
/>
<PropertyBox>
<PropertyBoxItem
icon={<IconArrowUpRight />}
value={
<ActivityRelationPicker
activity={{
id: activity.id,
activityTargets: activity.activityTargets ?? [],
}}
/>
}
label="Relations"
/>
</PropertyBox>
</StyledTopContainer>
<ActivityBodyEditor
activity={activity}
onChange={updateTitleFromBody}
/>
</StyledUpperPartContainer>
{showComment && (
<ActivityComments
activity={{
id: activity.id,
comments: activity.comments ?? [],
}}
/>
)}
</StyledContainer>
);
}

View File

@ -3,13 +3,13 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/activities/queries';
import { GET_ACTIVITIES_BY_TARGETS } from '@/activities/queries';
import { GET_COMPANIES } from '@/companies/queries';
import { GET_PEOPLE } from '@/people/queries';
import { Button, ButtonVariant } from '@/ui/button/components/Button';
import { IconTrash } from '@/ui/icon';
import { isRightDrawerOpenState } from '@/ui/right-drawer/states/isRightDrawerOpenState';
import { useDeleteCommentThreadMutation } from '~/generated/graphql';
import { useDeleteActivityMutation } from '~/generated/graphql';
const StyledContainer = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
@ -17,21 +17,21 @@ const StyledContainer = styled.div`
`;
type OwnProps = {
commentThreadId: string;
activityId: string;
};
export function CommentThreadActionBar({ commentThreadId }: OwnProps) {
export function ActivityActionBar({ activityId }: OwnProps) {
const theme = useTheme();
const [createCommentMutation] = useDeleteCommentThreadMutation();
const [createCommentMutation] = useDeleteActivityMutation();
const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
function deleteCommentThread() {
function deleteActivity() {
createCommentMutation({
variables: { commentThreadId },
variables: { activityId },
refetchQueries: [
getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? '',
],
});
setIsRightDrawerOpen(false);
@ -43,7 +43,7 @@ export function CommentThreadActionBar({ commentThreadId }: OwnProps) {
icon={
<IconTrash size={theme.icon.size.sm} stroke={theme.icon.stroke.md} />
}
onClick={deleteCommentThread}
onClick={deleteActivity}
variant={ButtonVariant.Tertiary}
/>
</StyledContainer>

View File

@ -1,34 +0,0 @@
import { CommentThreadEditor } from '@/activities/components/CommentThreadEditor';
import { useGetCommentThreadQuery } from '~/generated/graphql';
import '@blocknote/core/style.css';
type OwnProps = {
commentThreadId: string;
showComment?: boolean;
autoFillTitle?: boolean;
};
export function CommentThread({
commentThreadId,
showComment = true,
autoFillTitle = false,
}: OwnProps) {
const { data } = useGetCommentThreadQuery({
variables: {
commentThreadId: commentThreadId ?? '',
},
skip: !commentThreadId,
});
const commentThread = data?.findManyCommentThreads[0];
return commentThread ? (
<CommentThreadEditor
commentThread={commentThread}
showComment={showComment}
autoFillTitle={autoFillTitle}
/>
) : (
<></>
);
}

View File

@ -1,22 +1,22 @@
import { useRecoilValue } from 'recoil';
import { viewableCommentThreadIdState } from '@/activities/states/viewableCommentThreadIdState';
import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState';
import { RightDrawerBody } from '@/ui/right-drawer/components/RightDrawerBody';
import { RightDrawerPage } from '@/ui/right-drawer/components/RightDrawerPage';
import { RightDrawerTopBar } from '@/ui/right-drawer/components/RightDrawerTopBar';
import { CommentThread } from '../CommentThread';
import { Activity } from '../Activity';
export function RightDrawerCreateCommentThread() {
const commentThreadId = useRecoilValue(viewableCommentThreadIdState);
export function RightDrawerCreateActivity() {
const activityId = useRecoilValue(viewableActivityIdState);
return (
<RightDrawerPage>
<RightDrawerTopBar />
<RightDrawerBody>
{commentThreadId && (
<CommentThread
commentThreadId={commentThreadId}
{activityId && (
<Activity
activityId={activityId}
showComment={false}
autoFillTitle={true}
/>

View File

@ -1,20 +1,20 @@
import { useRecoilValue } from 'recoil';
import { viewableCommentThreadIdState } from '@/activities/states/viewableCommentThreadIdState';
import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState';
import { RightDrawerBody } from '@/ui/right-drawer/components/RightDrawerBody';
import { RightDrawerPage } from '@/ui/right-drawer/components/RightDrawerPage';
import { RightDrawerTopBar } from '@/ui/right-drawer/components/RightDrawerTopBar';
import { CommentThread } from '../CommentThread';
import { Activity } from '../Activity';
export function RightDrawerEditCommentThread() {
const commentThreadId = useRecoilValue(viewableCommentThreadIdState);
export function RightDrawerEditActivity() {
const activityId = useRecoilValue(viewableActivityIdState);
return (
<RightDrawerPage>
<RightDrawerTopBar />
<RightDrawerBody>
{commentThreadId && <CommentThread commentThreadId={commentThreadId} />}
{activityId && <Activity activityId={activityId} />}
</RightDrawerBody>
</RightDrawerPage>
);

View File

@ -3,6 +3,6 @@ import { atom } from 'recoil';
import { CommentableEntity } from '../types/CommentableEntity';
export const commentableEntityArrayState = atom<CommentableEntity[]>({
key: 'comments/commentable-entity-array',
key: 'activities/commentable-entity-array',
default: [],
});

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const viewableActivityIdState = atom<string | null>({
key: 'activities/viewable-activity-id',
default: null,
});

View File

@ -1,6 +0,0 @@
import { atom } from 'recoil';
export const viewableCommentThreadIdState = atom<string | null>({
key: 'comments/viewable-comment-thread-id',
default: null,
});

View File

@ -2,16 +2,16 @@ import React from 'react';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { CommentThreadCreateButton } from '@/activities/components/CommentThreadCreateButton';
import { useOpenCreateCommentThreadDrawer } from '@/activities/hooks/useOpenCreateCommentThreadDrawer';
import { ActivityCreateButton } from '@/activities/components/ActivityCreateButton';
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
import { ActivityForDrawer } from '@/activities/types/ActivityForDrawer';
import { CommentableEntity } from '@/activities/types/CommentableEntity';
import { CommentThreadForDrawer } from '@/activities/types/CommentThreadForDrawer';
import { useIsMobile } from '@/ui/hooks/useIsMobile';
import { IconCircleDot } from '@/ui/icon';
import {
ActivityType,
SortOrder,
useGetCommentThreadsByTargetsQuery,
useGetActivitiesByTargetsQuery,
} from '~/generated/graphql';
import { TimelineActivity } from './TimelineActivity';
@ -96,9 +96,9 @@ const StyledStartIcon = styled.div`
export function Timeline({ entity }: { entity: CommentableEntity }) {
const theme = useTheme();
const { data: queryResult, loading } = useGetCommentThreadsByTargetsQuery({
const { data: queryResult, loading } = useGetActivitiesByTargetsQuery({
variables: {
commentThreadTargetIds: [entity.id],
activityTargetIds: [entity.id],
orderBy: [
{
createdAt: SortOrder.Desc,
@ -107,21 +107,20 @@ export function Timeline({ entity }: { entity: CommentableEntity }) {
},
});
const openCreateCommandThread = useOpenCreateCommentThreadDrawer();
const openCreateCommandThread = useOpenCreateActivityDrawer();
const commentThreads: CommentThreadForDrawer[] =
queryResult?.findManyCommentThreads ?? [];
const activities: ActivityForDrawer[] = queryResult?.findManyActivities ?? [];
if (loading) {
return <></>;
}
if (!commentThreads.length) {
if (!activities.length) {
return (
<StyledTimelineEmptyContainer>
<StyledEmptyTimelineTitle>No activity yet</StyledEmptyTimelineTitle>
<StyledEmptyTimelineSubTitle>Create one:</StyledEmptyTimelineSubTitle>
<CommentThreadCreateButton
<ActivityCreateButton
onNoteClick={() => openCreateCommandThread(entity, ActivityType.Note)}
onTaskClick={() => openCreateCommandThread(entity, ActivityType.Task)}
/>
@ -132,17 +131,14 @@ export function Timeline({ entity }: { entity: CommentableEntity }) {
return (
<StyledMainContainer>
<StyledTopActionBar>
<CommentThreadCreateButton
<ActivityCreateButton
onNoteClick={() => openCreateCommandThread(entity, ActivityType.Note)}
onTaskClick={() => openCreateCommandThread(entity, ActivityType.Task)}
/>
</StyledTopActionBar>
<StyledTimelineContainer>
{commentThreads.map((commentThread) => (
<TimelineActivity
key={commentThread.id}
commentThread={commentThread}
/>
{activities.map((activity) => (
<TimelineActivity key={activity.id} activity={activity} />
))}
<StyledStartIcon>
<IconCircleDot size={theme.icon.size.lg} />

View File

@ -3,14 +3,11 @@ import { Tooltip } from 'react-tooltip';
import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled';
import { useOpenCommentThreadRightDrawer } from '@/activities/hooks/useOpenCommentThreadRightDrawer';
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/activities/queries';
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { GET_ACTIVITIES_BY_TARGETS } from '@/activities/queries';
import { IconNotes } from '@/ui/icon';
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
import {
CommentThread,
useUpdateCommentThreadMutation,
} from '~/generated/graphql';
import { Activity, useUpdateActivityMutation } from '~/generated/graphql';
import {
beautifyExactDate,
beautifyPastDateRelativeToNow,
@ -115,35 +112,31 @@ const StyledTimelineItemContainer = styled.div`
`;
type OwnProps = {
commentThread: Pick<
CommentThread,
activity: Pick<
Activity,
'id' | 'title' | 'body' | 'createdAt' | 'completedAt' | 'type'
> & { author: Pick<CommentThread['author'], 'displayName'> };
> & { author: Pick<Activity['author'], 'displayName'> };
};
export function TimelineActivity({ commentThread }: OwnProps) {
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(
commentThread.createdAt,
);
const exactCreatedAt = beautifyExactDate(commentThread.createdAt);
const body = JSON.parse(commentThread.body ?? '{}')[0]?.content[0]?.text;
export function TimelineActivity({ activity }: OwnProps) {
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(activity.createdAt);
const exactCreatedAt = beautifyExactDate(activity.createdAt);
const body = JSON.parse(activity.body ?? '{}')[0]?.content[0]?.text;
const openCommentThreadRightDrawer = useOpenCommentThreadRightDrawer();
const [updateCommentThreadMutation] = useUpdateCommentThreadMutation();
const openActivityRightDrawer = useOpenActivityRightDrawer();
const [updateActivityMutation] = useUpdateActivityMutation();
const handleActivityCompletionChange = useCallback(
(value: boolean) => {
updateCommentThreadMutation({
updateActivityMutation({
variables: {
id: commentThread.id,
id: activity.id,
completedAt: value ? new Date().toISOString() : null,
},
refetchQueries: [
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
],
refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''],
});
},
[commentThread, updateCommentThreadMutation],
[activity, updateActivityMutation],
);
return (
@ -153,14 +146,14 @@ export function TimelineActivity({ commentThread }: OwnProps) {
<IconNotes />
</StyledIconContainer>
<StyledItemTitleContainer>
<span>{commentThread.author.displayName}</span>
created a {commentThread.type.toLowerCase()}
<span>{activity.author.displayName}</span>
created a note created a {activity.type.toLowerCase()}
</StyledItemTitleContainer>
<StyledItemTitleDate id={`id-${commentThread.id}`}>
<StyledItemTitleDate id={`id-${activity.id}`}>
{beautifiedCreatedAt} ago
</StyledItemTitleDate>
<StyledTooltip
anchorSelect={`#id-${commentThread.id}`}
anchorSelect={`#id-${activity.id}`}
content={exactCreatedAt}
clickable
noArrow
@ -171,13 +164,11 @@ export function TimelineActivity({ commentThread }: OwnProps) {
<StyledVerticalLine></StyledVerticalLine>
</StyledVerticalLineContainer>
<StyledCardContainer>
<StyledCard
onClick={() => openCommentThreadRightDrawer(commentThread.id)}
>
<StyledCard onClick={() => openActivityRightDrawer(activity.id)}>
<TimelineActivityTitle
title={commentThread.title ?? ''}
completed={!!commentThread.completedAt}
type={commentThread.type}
title={activity.title ?? ''}
completed={!!activity.completedAt}
type={activity.type}
onCompletionChange={handleActivityCompletionChange}
/>
<StyledCardContent>

View File

@ -0,0 +1,4 @@
import { GetActivitiesByTargetsQuery } from '~/generated/graphql';
export type ActivityForDrawer =
GetActivitiesByTargetsQuery['findManyActivities'][0];

View File

@ -1,5 +1,3 @@
import { CommentThreadForDrawer } from './CommentThreadForDrawer';
import { ActivityForDrawer } from './ActivityForDrawer';
export type CommentForDrawer = NonNullable<
CommentThreadForDrawer['comments']
>[0];
export type CommentForDrawer = NonNullable<ActivityForDrawer['comments']>[0];

View File

@ -1,4 +0,0 @@
import { GetCommentThreadsByTargetsQuery } from '~/generated/graphql';
export type CommentThreadForDrawer =
GetCommentThreadsByTargetsQuery['findManyCommentThreads'][0];

View File

@ -6,7 +6,7 @@ import { useRecoilState } from 'recoil';
import { tokenPairState } from '@/auth/states/tokenPairState';
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { AppPath } from '@/types/AppPath';
import { CommentThreadTarget } from '~/generated/graphql';
import { ActivityTarget } from '~/generated/graphql';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { useUpdateEffect } from '~/hooks/useUpdateEffect';
@ -25,12 +25,12 @@ export function useApolloFactory() {
uri: `${process.env.REACT_APP_API_URL}`,
cache: new InMemoryCache({
typePolicies: {
CommentThread: {
Activity: {
fields: {
commentThreadTargets: {
activityTargets: {
merge(
_existing: CommentThreadTarget[] = [],
incoming: CommentThreadTarget[],
_existing: ActivityTarget[] = [],
incoming: ActivityTarget[],
) {
return [...incoming];
},

View File

@ -14,7 +14,7 @@ import { CompanyChip } from './CompanyChip';
type OwnProps = {
company: Pick<
GetCompaniesQuery['companies'][0],
'id' | 'name' | 'domainName' | '_commentThreadCount'
'id' | 'name' | 'domainName' | '_activityCount'
>;
};

View File

@ -28,7 +28,7 @@ export const GET_COMPANIES = gql`
address
linkedinUrl
employees
_commentThreadCount
_activityCount
accountOwner {
id
email

View File

@ -12,7 +12,7 @@ export const GET_COMPANY = gql`
address
linkedinUrl
employees
_commentThreadCount
_activityCount
accountOwner {
id
email

View File

@ -25,7 +25,7 @@ export function EditableCompanyNameCell() {
id: currentRowEntityId ?? '',
name: name ?? '',
domainName: domainName ?? '',
_commentThreadCount: commentCount ?? 0,
_activityCount: commentCount ?? 0,
}}
/>
);

View File

@ -0,0 +1,14 @@
import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds';
import { TableActionBarButtonToggleComments } from '@/ui/table/action-bar/components/TableActionBarButtonOpenComments';
import { CommentableType } from '~/generated/graphql';
export function TableActionBarButtonCreateActivityCompany() {
const openCreateActivityRightDrawer =
useOpenCreateActivityDrawerForSelectedRowIds();
async function handleButtonClick() {
openCreateActivityRightDrawer(CommentableType.Company);
}
return <TableActionBarButtonToggleComments onClick={handleButtonClick} />;
}

View File

@ -1,14 +0,0 @@
import { useOpenCreateCommentThreadDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateCommentDrawerForSelectedRowIds';
import { TableActionBarButtonToggleComments } from '@/ui/table/action-bar/components/TableActionBarButtonOpenComments';
import { CommentableType } from '~/generated/graphql';
export function TableActionBarButtonCreateCommentThreadCompany() {
const openCreateCommentThreadRightDrawer =
useOpenCreateCommentThreadDrawerForSelectedRowIds();
async function handleButtonClick() {
openCreateCommentThreadRightDrawer(CommentableType.Company);
}
return <TableActionBarButtonToggleComments onClick={handleButtonClick} />;
}

View File

@ -10,7 +10,7 @@ type MockedCompany = Pick<
| 'address'
| 'employees'
| 'linkedinUrl'
| '_commentThreadCount'
| '_activityCount'
> & {
accountOwner: Pick<
User,
@ -33,7 +33,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
createdAt: '2023-04-26T10:08:54.724515+00:00',
address: 'San Francisco, CA',
employees: 5000,
_commentThreadCount: 0,
_activityCount: 0,
accountOwner: {
email: 'charles@test.com',
displayName: 'Charles Test',
@ -53,7 +53,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
createdAt: '2023-04-26T10:12:42.33625+00:00',
address: 'Paris, France',
employees: 800,
_commentThreadCount: 0,
_activityCount: 0,
accountOwner: null,
__typename: 'Company',
},
@ -65,7 +65,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
createdAt: '2023-04-26T10:10:32.530184+00:00',
address: 'San Francisco, CA',
employees: 8000,
_commentThreadCount: 0,
_activityCount: 0,
accountOwner: null,
__typename: 'Company',
},
@ -77,7 +77,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
createdAt: '2023-03-21T06:30:25.39474+00:00',
address: 'San Francisco, CA',
employees: 800,
_commentThreadCount: 0,
_activityCount: 0,
accountOwner: null,
__typename: 'Company',
},
@ -89,7 +89,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
createdAt: '2023-04-26T10:13:29.712485+00:00',
address: 'San Francisco, CA',
employees: 400,
_commentThreadCount: 0,
_activityCount: 0,
accountOwner: null,
__typename: 'Company',
},

View File

@ -83,10 +83,10 @@ export function useSetCompanyEntityTable() {
.getLoadable(companyCommentCountFamilyState(company.id))
.valueOrThrow();
if (currentCommentCount !== company._commentThreadCount) {
if (currentCommentCount !== company._activityCount) {
set(
companyCommentCountFamilyState(company.id),
company._commentThreadCount,
company._activityCount,
);
}

View File

@ -10,11 +10,7 @@ type OwnProps = {
| Partial<
Pick<
Person,
| 'id'
| 'firstName'
| 'lastName'
| 'displayName'
| '_commentThreadCount'
'id' | 'firstName' | 'lastName' | 'displayName' | '_activityCount'
>
>
| null

View File

@ -101,12 +101,12 @@ export function useSetPeopleEntityTable() {
if (
currentNameCell.firstName !== person.firstName ||
currentNameCell.lastName !== person.lastName ||
currentNameCell.commentCount !== person._commentThreadCount
currentNameCell.commentCount !== person._activityCount
) {
set(peopleNameCellFamilyState(person.id), {
firstName: person.firstName ?? null,
lastName: person.lastName ?? null,
commentCount: person._commentThreadCount,
commentCount: person._activityCount,
displayName: person.displayName ?? null,
});
}

View File

@ -31,7 +31,7 @@ export const GET_PEOPLE = gql`
jobTitle
linkedinUrl
createdAt
_commentThreadCount
_activityCount
company {
id
name
@ -107,7 +107,7 @@ export const GET_PERSON_NAMES_AND_COMMENT_COUNT = gql`
firstName
lastName
displayName
_commentThreadCount
_activityCount
}
}
`;
@ -129,7 +129,7 @@ export const GET_PERSON_COMMENT_COUNT = gql`
query GetPersonCommentCountById($id: String!) {
person: findUniquePerson(id: $id) {
id
_commentThreadCount
_activityCount
}
}
`;

View File

@ -15,7 +15,7 @@ export const GET_PERSON = gql`
jobTitle
linkedinUrl
phone
_commentThreadCount
_activityCount
company {
id
name

View File

@ -21,7 +21,7 @@ export function EditablePeopleFullNameCell() {
<EditablePeopleFullName
person={{
id: currentRowEntityId ?? undefined,
_commentThreadCount: commentCount ?? undefined,
_activityCount: commentCount ?? undefined,
firstName,
lastName,
displayName: displayName ?? undefined,

View File

@ -0,0 +1,14 @@
import { useOpenCreateActivityDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds';
import { TableActionBarButtonToggleComments } from '@/ui/table/action-bar/components/TableActionBarButtonOpenComments';
import { CommentableType } from '~/generated/graphql';
export function TableActionBarButtonCreateActivityPeople() {
const openCreateActivityRightDrawer =
useOpenCreateActivityDrawerForSelectedRowIds();
async function handleButtonClick() {
openCreateActivityRightDrawer(CommentableType.Person);
}
return <TableActionBarButtonToggleComments onClick={handleButtonClick} />;
}

View File

@ -1,14 +0,0 @@
import { useOpenCreateCommentThreadDrawerForSelectedRowIds } from '@/activities/hooks/useOpenCreateCommentDrawerForSelectedRowIds';
import { TableActionBarButtonToggleComments } from '@/ui/table/action-bar/components/TableActionBarButtonOpenComments';
import { CommentableType } from '~/generated/graphql';
export function TableActionBarButtonCreateCommentThreadPeople() {
const openCreateCommentThreadRightDrawer =
useOpenCreateCommentThreadDrawerForSelectedRowIds();
async function handleButtonClick() {
openCreateCommentThreadRightDrawer(CommentableType.Person);
}
return <TableActionBarButtonToggleComments onClick={handleButtonClick} />;
}

View File

@ -0,0 +1,144 @@
import React, { useEffect, useState } from 'react';
import styled from '@emotion/styled';
import { IconChevronDown } from '@/ui/icon/index';
type ButtonProps = React.ComponentProps<'button'>;
export type DropdownOptionType = {
key: string;
label: string;
icon: React.ReactNode;
};
type OwnProps = {
options: DropdownOptionType[];
selectedOptionKey?: string;
onSelection: (value: DropdownOptionType) => void;
} & ButtonProps;
const StyledButton = styled.button<ButtonProps & { isOpen: boolean }>`
align-items: center;
background: ${({ theme }) => theme.background.tertiary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-bottom-left-radius: ${({ isOpen, theme }) =>
isOpen ? 0 : theme.border.radius.sm};
border-bottom-right-radius: ${({ isOpen, theme }) =>
isOpen ? 0 : theme.border.radius.sm};
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
border-top-right-radius: ${({ theme }) => theme.border.radius.sm};
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
svg {
align-items: center;
display: flex;
height: 14px;
justify-content: center;
width: 14px;
}
`;
const StyledDropdownItem = styled.button<ButtonProps>`
align-items: center;
background: ${({ theme }) => theme.background.tertiary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm};
border-top: none;
border-top-left-radius: 0;
border-top-right-radius: 0;
color: ${({ theme }) => theme.font.color.secondary};
cursor: pointer;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(1)} ${({ theme }) => theme.spacing(2)};
svg {
align-items: center;
display: flex;
height: 14px;
justify-content: center;
width: 14px;
}
`;
const DropdownContainer = styled.div`
position: relative;
`;
const DropdownMenu = styled.div`
display: flex;
flex-direction: column;
position: absolute;
width: 100%;
`;
export function DropdownButton({
options,
selectedOptionKey,
onSelection,
...buttonProps
}: OwnProps) {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState<
DropdownOptionType | undefined
>(undefined);
useEffect(() => {
if (selectedOptionKey) {
const option = options.find((option) => option.key === selectedOptionKey);
setSelectedOption(option);
} else {
setSelectedOption(options[0]);
}
}, [selectedOptionKey, options]);
if (!options.length) {
throw new Error('You must provide at least one option.');
}
const handleSelect =
(option: DropdownOptionType) =>
(event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
onSelection(option);
setSelectedOption(option);
setIsOpen(false);
};
return (
<>
{selectedOption && (
<DropdownContainer>
<StyledButton
onClick={() => setIsOpen(!isOpen)}
{...buttonProps}
isOpen={isOpen}
>
{selectedOption.icon}
{selectedOption.label}
{options.length > 1 && <IconChevronDown />}
</StyledButton>
{isOpen && (
<DropdownMenu>
{options
.filter((option) => option.label !== selectedOption.label)
.map((option, index) => (
<StyledDropdownItem
key={index}
onClick={handleSelect(option)}
>
{option.icon}
{option.label}
</StyledDropdownItem>
))}
</DropdownMenu>
)}
</DropdownContainer>
)}
</>
);
}

View File

@ -1,7 +1,7 @@
import { useRecoilState } from 'recoil';
import { RightDrawerCreateCommentThread } from '@/activities/right-drawer/components/create/RightDrawerCreateCommentThread';
import { RightDrawerEditCommentThread } from '@/activities/right-drawer/components/edit/RightDrawerEditCommentThread';
import { RightDrawerCreateActivity } from '@/activities/right-drawer/components/create/RightDrawerCreateActivity';
import { RightDrawerEditActivity } from '@/activities/right-drawer/components/edit/RightDrawerEditActivity';
import { RightDrawerTimeline } from '@/activities/right-drawer/components/RightDrawerTimeline';
import { isDefined } from '~/utils/isDefined';
@ -18,10 +18,10 @@ export function RightDrawerRouter() {
switch (rightDrawerPage) {
case RightDrawerPages.Timeline:
return <RightDrawerTimeline />;
case RightDrawerPages.CreateCommentThread:
return <RightDrawerCreateCommentThread />;
case RightDrawerPages.EditCommentThread:
return <RightDrawerEditCommentThread />;
case RightDrawerPages.CreateActivity:
return <RightDrawerCreateActivity />;
case RightDrawerPages.EditActivity:
return <RightDrawerEditActivity />;
default:
return <></>;
}

View File

@ -1,5 +1,5 @@
export enum RightDrawerPages {
Timeline = 'timeline',
CreateCommentThread = 'create-comment-thread',
EditCommentThread = 'edit-comment-thread',
CreateActivity = 'create-activity',
EditActivity = 'edit-activity',
}

View File

@ -9,7 +9,7 @@ export type EditableChipProps = {
value: string;
editModeHorizontalAlign?: 'left' | 'right';
ChipComponent: React.ReactNode;
commentThreadCount?: number;
activityCount?: number;
onCommentClick?: (event: React.MouseEvent<HTMLDivElement>) => void;
rightEndContents?: ReactNode[];
onSubmit?: (newValue: string) => void;