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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,26 +2,26 @@ import React, { useCallback, useState } from 'react';
import { getOperationName } from '@apollo/client/utilities'; import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { CommentThreadBodyEditor } from '@/activities/components/CommentThreadBodyEditor'; import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor';
import { CommentThreadComments } from '@/activities/components/CommentThreadComments'; import { ActivityComments } from '@/activities/components/ActivityComments';
import { CommentThreadRelationPicker } from '@/activities/components/CommentThreadRelationPicker'; import { ActivityRelationPicker } from '@/activities/components/ActivityRelationPicker';
import { CommentThreadTypeDropdown } from '@/activities/components/CommentThreadTypeDropdown'; import { ActivityTypeDropdown } from '@/activities/components/ActivityTypeDropdown';
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/activities/queries'; import { GET_ACTIVITIES_BY_TARGETS } from '@/activities/queries';
import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox'; import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox';
import { PropertyBoxItem } from '@/ui/editable-field/property-box/components/PropertyBoxItem'; import { PropertyBoxItem } from '@/ui/editable-field/property-box/components/PropertyBoxItem';
import { useIsMobile } from '@/ui/hooks/useIsMobile'; import { useIsMobile } from '@/ui/hooks/useIsMobile';
import { IconArrowUpRight } from '@/ui/icon/index'; import { IconArrowUpRight } from '@/ui/icon/index';
import { import {
CommentThread, Activity,
CommentThreadTarget, ActivityTarget,
useUpdateCommentThreadMutation, useUpdateActivityMutation,
} from '~/generated/graphql'; } from '~/generated/graphql';
import { debounce } from '~/utils/debounce'; 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 { CommentForDrawer } from '../types/CommentForDrawer';
import { CommentThreadTitle } from './CommentThreadTitle'; import { ActivityTitle } from './ActivityTitle';
import '@blocknote/core/style.css'; import '@blocknote/core/style.css';
@ -65,64 +65,57 @@ const StyledTopActionsContainer = styled.div`
`; `;
type OwnProps = { type OwnProps = {
commentThread: Pick< activity: Pick<Activity, 'id' | 'title' | 'body' | 'type' | 'completedAt'> & {
CommentThread,
'id' | 'title' | 'body' | 'type' | 'completedAt'
> & {
comments?: Array<CommentForDrawer> | null; comments?: Array<CommentForDrawer> | null;
} & { } & {
commentThreadTargets?: Array< activityTargets?: Array<
Pick<CommentThreadTarget, 'id' | 'commentableId' | 'commentableType'> Pick<ActivityTarget, 'id' | 'commentableId' | 'commentableType'>
> | null; > | null;
}; };
showComment?: boolean; showComment?: boolean;
autoFillTitle?: boolean; autoFillTitle?: boolean;
}; };
export function CommentThreadEditor({ export function ActivityEditor({
commentThread, activity,
showComment = true, showComment = true,
autoFillTitle = false, autoFillTitle = false,
}: OwnProps) { }: OwnProps) {
const [hasUserManuallySetTitle, setHasUserManuallySetTitle] = const [hasUserManuallySetTitle, setHasUserManuallySetTitle] =
useState<boolean>(false); 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>( const [completedAt, setCompletedAt] = useState<string | null>(
commentThread.completedAt ?? '', activity.completedAt ?? '',
); );
const [updateCommentThreadMutation] = useUpdateCommentThreadMutation(); const [updateActivityMutation] = useUpdateActivityMutation();
const updateTitle = useCallback( const updateTitle = useCallback(
(newTitle: string) => { (newTitle: string) => {
updateCommentThreadMutation({ updateActivityMutation({
variables: { variables: {
id: commentThread.id, id: activity.id,
title: newTitle ?? '', title: newTitle ?? '',
}, },
refetchQueries: [ refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''],
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
],
}); });
}, },
[commentThread, updateCommentThreadMutation], [activity, updateActivityMutation],
); );
const handleActivityCompletionChange = useCallback( const handleActivityCompletionChange = useCallback(
(value: boolean) => { (value: boolean) => {
updateCommentThreadMutation({ updateActivityMutation({
variables: { variables: {
id: commentThread.id, id: activity.id,
completedAt: value ? new Date().toISOString() : null, completedAt: value ? new Date().toISOString() : null,
}, },
refetchQueries: [ refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''],
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
],
}); });
setCompletedAt(value ? new Date().toISOString() : null); setCompletedAt(value ? new Date().toISOString() : null);
}, },
[commentThread, updateCommentThreadMutation], [activity, updateActivityMutation],
); );
const debouncedUpdateTitle = debounce(updateTitle, 200); const debouncedUpdateTitle = debounce(updateTitle, 200);
@ -135,7 +128,7 @@ export function CommentThreadEditor({
} }
} }
if (!commentThread) { if (!activity) {
return <></>; return <></>;
} }
@ -144,13 +137,13 @@ export function CommentThreadEditor({
<StyledUpperPartContainer> <StyledUpperPartContainer>
<StyledTopContainer> <StyledTopContainer>
<StyledTopActionsContainer> <StyledTopActionsContainer>
<CommentThreadTypeDropdown commentThread={commentThread} /> <ActivityTypeDropdown activity={activity} />
<CommentThreadActionBar commentThreadId={commentThread?.id ?? ''} /> <ActivityActionBar activityId={activity?.id ?? ''} />
</StyledTopActionsContainer> </StyledTopActionsContainer>
<CommentThreadTitle <ActivityTitle
title={title ?? ''} title={title ?? ''}
completed={!!completedAt} completed={!!completedAt}
type={commentThread.type} type={activity.type}
onTitleChange={(newTitle) => { onTitleChange={(newTitle) => {
setTitle(newTitle); setTitle(newTitle);
setHasUserManuallySetTitle(true); setHasUserManuallySetTitle(true);
@ -162,11 +155,10 @@ export function CommentThreadEditor({
<PropertyBoxItem <PropertyBoxItem
icon={<IconArrowUpRight />} icon={<IconArrowUpRight />}
value={ value={
<CommentThreadRelationPicker <ActivityRelationPicker
commentThread={{ activity={{
id: commentThread.id, id: activity.id,
commentThreadTargets: activityTargets: activity.activityTargets ?? [],
commentThread.commentThreadTargets ?? [],
}} }}
/> />
} }
@ -174,16 +166,16 @@ export function CommentThreadEditor({
/> />
</PropertyBox> </PropertyBox>
</StyledTopContainer> </StyledTopContainer>
<CommentThreadBodyEditor <ActivityBodyEditor
commentThread={commentThread} activity={activity}
onChange={updateTitleFromBody} onChange={updateTitleFromBody}
/> />
</StyledUpperPartContainer> </StyledUpperPartContainer>
{showComment && ( {showComment && (
<CommentThreadComments <ActivityComments
commentThread={{ activity={{
id: commentThread.id, id: activity.id,
comments: commentThread.comments ?? [], 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 { RecoilScope } from '@/ui/recoil-scope/components/RecoilScope';
import { MultipleEntitySelect } from '@/ui/relation-picker/components/MultipleEntitySelect'; import { MultipleEntitySelect } from '@/ui/relation-picker/components/MultipleEntitySelect';
import { RelationPickerHotkeyScope } from '@/ui/relation-picker/types/RelationPickerHotkeyScope'; import { RelationPickerHotkeyScope } from '@/ui/relation-picker/types/RelationPickerHotkeyScope';
import { import { Activity, ActivityTarget, CommentableType } from '~/generated/graphql';
CommentableType,
CommentThread,
CommentThreadTarget,
} from '~/generated/graphql';
import { useHandleCheckableCommentThreadTargetChange } from '../hooks/useHandleCheckableCommentThreadTargetChange'; import { useHandleCheckableActivityTargetChange } from '../hooks/useHandleCheckableActivityTargetChange';
import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '../utils/flatMapAndSortEntityForSelectArrayByName'; import { flatMapAndSortEntityForSelectArrayOfArrayByName } from '../utils/flatMapAndSortEntityForSelectArrayByName';
type OwnProps = { type OwnProps = {
commentThread?: Pick<CommentThread, 'id'> & { activity?: Pick<Activity, 'id'> & {
commentThreadTargets: Array< activityTargets: Array<
Pick<CommentThreadTarget, 'id' | 'commentableId' | 'commentableType'> Pick<ActivityTarget, 'id' | 'commentableId' | 'commentableType'>
>; >;
}; };
}; };
@ -75,7 +71,7 @@ const StyledMenuWrapper = styled.div`
z-index: ${({ theme }) => theme.lastLayerZIndex}; z-index: ${({ theme }) => theme.lastLayerZIndex};
`; `;
export function CommentThreadRelationPicker({ commentThread }: OwnProps) { export function ActivityRelationPicker({ activity }: OwnProps) {
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
const [searchFilter, setSearchFilter] = useState(''); const [searchFilter, setSearchFilter] = useState('');
const [selectedEntityIds, setSelectedEntityIds] = useState< const [selectedEntityIds, setSelectedEntityIds] = useState<
@ -88,17 +84,18 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
const initialPeopleIds = useMemo( const initialPeopleIds = useMemo(
() => () =>
commentThread?.commentThreadTargets activity?.activityTargets
?.filter((relation) => relation.commentableType === 'Person') ?.filter((relation) => relation.commentableType === 'Person')
.map((relation) => relation.commentableId) ?? [], .map((relation) => relation.commentableId) ?? [],
[commentThread?.commentThreadTargets], [activity?.activityTargets],
); );
const initialCompanyIds = useMemo( const initialCompanyIds = useMemo(
() => () =>
commentThread?.commentThreadTargets activity?.activityTargets
?.filter((relation) => relation.commentableType === 'Company') ?.filter((relation) => relation.commentableType === 'Company')
.map((relation) => relation.commentableId) ?? [], .map((relation) => relation.commentableId) ?? [],
[commentThread?.commentThreadTargets], [activity?.activityTargets],
); );
const initialSelectedEntityIds = useMemo( const initialSelectedEntityIds = useMemo(
@ -135,8 +132,8 @@ export function CommentThreadRelationPicker({ commentThread }: OwnProps) {
companiesForMultiSelect.entitiesToSelect, companiesForMultiSelect.entitiesToSelect,
]); ]);
const handleCheckItemsChange = useHandleCheckableCommentThreadTargetChange({ const handleCheckItemsChange = useHandleCheckableActivityTargetChange({
commentThread, activity,
}); });
const exitEditMode = useCallback(() => { const exitEditMode = useCallback(() => {

View File

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

View File

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

View File

@ -4,17 +4,17 @@ import type { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator'; import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; 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` const StyledContainer = styled.div`
width: 400px; width: 400px;
`; `;
const meta: Meta<typeof CommentThreadRelationPicker> = { const meta: Meta<typeof ActivityRelationPicker> = {
title: 'Modules/Comments/CommentThreadRelationPicker', title: 'Modules/Comments/ActivityRelationPicker',
component: CommentThreadRelationPicker, component: ActivityRelationPicker,
decorators: [ decorators: [
(Story) => ( (Story) => (
<MemoryRouter> <MemoryRouter>
@ -25,13 +25,13 @@ const meta: Meta<typeof CommentThreadRelationPicker> = {
), ),
ComponentDecorator, ComponentDecorator,
], ],
args: { commentThread: mockedCommentThreads[0] }, args: { activity: mockedActivities[0] },
parameters: { parameters: {
msw: graphqlMocks, msw: graphqlMocks,
}, },
}; };
export default meta; export default meta;
type Story = StoryObj<typeof CommentThreadRelationPicker>; type Story = StoryObj<typeof ActivityRelationPicker>;
export const Default: Story = {}; 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 { RightDrawerHotkeyScope } from '@/ui/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/right-drawer/types/RightDrawerPages'; 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 { openRightDrawer } = useRightDrawer();
const [, setViewableCommentThreadId] = useRecoilState( const [, setViewableActivityId] = useRecoilState(viewableActivityIdState);
viewableCommentThreadIdState,
);
const setHotkeyScope = useSetHotkeyScope(); const setHotkeyScope = useSetHotkeyScope();
return function openCommentThreadRightDrawer(commentThreadId: string) { return function openActivityRightDrawer(activityId: string) {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableCommentThreadId(commentThreadId); setViewableActivityId(activityId);
openRightDrawer(RightDrawerPages.EditCommentThread); 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 { useRightDrawer } from '@/ui/right-drawer/hooks/useRightDrawer';
import { RightDrawerHotkeyScope } from '@/ui/right-drawer/types/RightDrawerHotkeyScope'; import { RightDrawerHotkeyScope } from '@/ui/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/right-drawer/types/RightDrawerPages'; import { RightDrawerPages } from '@/ui/right-drawer/types/RightDrawerPages';
import { import { ActivityType, useCreateActivityMutation } from '~/generated/graphql';
ActivityType,
useCreateCommentThreadMutation,
} 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 { commentableEntityArrayState } from '../states/commentableEntityArrayState';
import { viewableCommentThreadIdState } from '../states/viewableCommentThreadIdState'; import { viewableActivityIdState } from '../states/viewableActivityIdState';
import { CommentableEntity } from '../types/CommentableEntity'; import { CommentableEntity } from '../types/CommentableEntity';
export function useOpenCreateCommentThreadDrawer() { export function useOpenCreateActivityDrawer() {
const { openRightDrawer } = useRightDrawer(); const { openRightDrawer } = useRightDrawer();
const [createCommentThreadMutation] = useCreateCommentThreadMutation(); const [createActivityMutation] = useCreateActivityMutation();
const currentUser = useRecoilValue(currentUserState); const currentUser = useRecoilValue(currentUserState);
const setHotkeyScope = useSetHotkeyScope(); const setHotkeyScope = useSetHotkeyScope();
const [, setCommentableEntityArray] = useRecoilState( const [, setCommentableEntityArray] = useRecoilState(
commentableEntityArrayState, commentableEntityArrayState,
); );
const [, setViewableCommentThreadId] = useRecoilState( const [, setViewableActivityId] = useRecoilState(viewableActivityIdState);
viewableCommentThreadIdState,
);
return function openCreateCommentThreadDrawer( return function openCreateActivityDrawer(
entity: CommentableEntity, entity: CommentableEntity,
type: ActivityType, type: ActivityType,
) { ) {
createCommentThreadMutation({ createActivityMutation({
variables: { variables: {
authorId: currentUser?.id ?? '', authorId: currentUser?.id ?? '',
commentThreadId: v4(), activityId: v4(),
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
type: type, type: type,
commentThreadTargetArray: [ activityTargetArray: [
{ {
commentableId: entity.id, commentableId: entity.id,
commentableType: entity.type, commentableType: entity.type,
@ -54,14 +49,14 @@ export function useOpenCreateCommentThreadDrawer() {
refetchQueries: [ refetchQueries: [
getOperationName(GET_COMPANIES) ?? '', getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '', getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_COMMENT_THREAD) ?? '', getOperationName(GET_ACTIVITY) ?? '',
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '', getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? '',
], ],
onCompleted(data) { onCompleted(data) {
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
setViewableCommentThreadId(data.createOneCommentThread.id); setViewableActivityId(data.createOneActivity.id);
setCommentableEntityArray([entity]); setCommentableEntityArray([entity]);
openRightDrawer(RightDrawerPages.CreateCommentThread); openRightDrawer(RightDrawerPages.CreateActivity);
}, },
}); });
}; };

View File

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

View File

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

View File

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

View File

@ -1,22 +1,18 @@
import { gql } from '@apollo/client'; import { gql } from '@apollo/client';
export const ADD_COMMENT_THREAD_TARGETS = gql` export const ADD_ACTIVITY_TARGETS = gql`
mutation AddCommentThreadTargetsOnCommentThread( mutation AddActivityTargetsOnActivity(
$commentThreadId: String! $activityId: String!
$commentThreadTargetInputs: [CommentThreadTargetCreateManyCommentThreadInput!]! $activityTargetInputs: [ActivityTargetCreateManyActivityInput!]!
) { ) {
updateOneCommentThread( updateOneActivity(
where: { id: $commentThreadId } where: { id: $activityId }
data: { data: { activityTargets: { createMany: { data: $activityTargetInputs } } }
commentThreadTargets: {
createMany: { data: $commentThreadTargetInputs }
}
}
) { ) {
id id
createdAt createdAt
updatedAt updatedAt
commentThreadTargets { activityTargets {
id id
createdAt createdAt
updatedAt updatedAt
@ -27,23 +23,21 @@ export const ADD_COMMENT_THREAD_TARGETS = gql`
} }
`; `;
export const REMOVE_COMMENT_THREAD_TARGETS = gql` export const REMOVE_ACTIVITY_TARGETS = gql`
mutation RemoveCommentThreadTargetsOnCommentThread( mutation RemoveActivityTargetsOnActivity(
$commentThreadId: String! $activityId: String!
$commentThreadTargetIds: [String!]! $activityTargetIds: [String!]!
) { ) {
updateOneCommentThread( updateOneActivity(
where: { id: $commentThreadId } where: { id: $activityId }
data: { data: {
commentThreadTargets: { activityTargets: { deleteMany: { id: { in: $activityTargetIds } } }
deleteMany: { id: { in: $commentThreadTargetIds } }
}
} }
) { ) {
id id
createdAt createdAt
updatedAt updatedAt
commentThreadTargets { activityTargets {
id id
createdAt createdAt
updatedAt updatedAt
@ -54,23 +48,23 @@ export const REMOVE_COMMENT_THREAD_TARGETS = gql`
} }
`; `;
export const DELETE_COMMENT_THREAD = gql` export const DELETE_ACTIVITY = gql`
mutation DeleteCommentThread($commentThreadId: String!) { mutation DeleteActivity($activityId: String!) {
deleteManyCommentThreads(where: { id: { equals: $commentThreadId } }) { deleteManyActivities(where: { id: { equals: $activityId } }) {
count count
} }
} }
`; `;
export const UPDATE_COMMENT_THREAD = gql` export const UPDATE_ACTIVITY = gql`
mutation UpdateCommentThread( mutation UpdateActivity(
$id: String! $id: String!
$body: String $body: String
$title: String $title: String
$type: ActivityType $type: ActivityType
$completedAt: DateTime $completedAt: DateTime
) { ) {
updateOneCommentThread( updateOneActivity(
where: { id: $id } where: { id: $id }
data: { data: {
body: $body 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 styled from '@emotion/styled';
import { useRecoilState } from 'recoil'; 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_COMPANIES } from '@/companies/queries';
import { GET_PEOPLE } from '@/people/queries'; import { GET_PEOPLE } from '@/people/queries';
import { Button, ButtonVariant } from '@/ui/button/components/Button'; import { Button, ButtonVariant } from '@/ui/button/components/Button';
import { IconTrash } from '@/ui/icon'; import { IconTrash } from '@/ui/icon';
import { isRightDrawerOpenState } from '@/ui/right-drawer/states/isRightDrawerOpenState'; import { isRightDrawerOpenState } from '@/ui/right-drawer/states/isRightDrawerOpenState';
import { useDeleteCommentThreadMutation } from '~/generated/graphql'; import { useDeleteActivityMutation } from '~/generated/graphql';
const StyledContainer = styled.div` const StyledContainer = styled.div`
color: ${({ theme }) => theme.font.color.tertiary}; color: ${({ theme }) => theme.font.color.tertiary};
@ -17,21 +17,21 @@ const StyledContainer = styled.div`
`; `;
type OwnProps = { type OwnProps = {
commentThreadId: string; activityId: string;
}; };
export function CommentThreadActionBar({ commentThreadId }: OwnProps) { export function ActivityActionBar({ activityId }: OwnProps) {
const theme = useTheme(); const theme = useTheme();
const [createCommentMutation] = useDeleteCommentThreadMutation(); const [createCommentMutation] = useDeleteActivityMutation();
const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState); const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
function deleteCommentThread() { function deleteActivity() {
createCommentMutation({ createCommentMutation({
variables: { commentThreadId }, variables: { activityId },
refetchQueries: [ refetchQueries: [
getOperationName(GET_COMPANIES) ?? '', getOperationName(GET_COMPANIES) ?? '',
getOperationName(GET_PEOPLE) ?? '', getOperationName(GET_PEOPLE) ?? '',
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '', getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? '',
], ],
}); });
setIsRightDrawerOpen(false); setIsRightDrawerOpen(false);
@ -43,7 +43,7 @@ export function CommentThreadActionBar({ commentThreadId }: OwnProps) {
icon={ icon={
<IconTrash size={theme.icon.size.sm} stroke={theme.icon.stroke.md} /> <IconTrash size={theme.icon.size.sm} stroke={theme.icon.stroke.md} />
} }
onClick={deleteCommentThread} onClick={deleteActivity}
variant={ButtonVariant.Tertiary} variant={ButtonVariant.Tertiary}
/> />
</StyledContainer> </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 { useRecoilValue } from 'recoil';
import { viewableCommentThreadIdState } from '@/activities/states/viewableCommentThreadIdState'; import { viewableActivityIdState } from '@/activities/states/viewableActivityIdState';
import { RightDrawerBody } from '@/ui/right-drawer/components/RightDrawerBody'; import { RightDrawerBody } from '@/ui/right-drawer/components/RightDrawerBody';
import { RightDrawerPage } from '@/ui/right-drawer/components/RightDrawerPage'; import { RightDrawerPage } from '@/ui/right-drawer/components/RightDrawerPage';
import { RightDrawerTopBar } from '@/ui/right-drawer/components/RightDrawerTopBar'; import { RightDrawerTopBar } from '@/ui/right-drawer/components/RightDrawerTopBar';
import { CommentThread } from '../CommentThread'; import { Activity } from '../Activity';
export function RightDrawerCreateCommentThread() { export function RightDrawerCreateActivity() {
const commentThreadId = useRecoilValue(viewableCommentThreadIdState); const activityId = useRecoilValue(viewableActivityIdState);
return ( return (
<RightDrawerPage> <RightDrawerPage>
<RightDrawerTopBar /> <RightDrawerTopBar />
<RightDrawerBody> <RightDrawerBody>
{commentThreadId && ( {activityId && (
<CommentThread <Activity
commentThreadId={commentThreadId} activityId={activityId}
showComment={false} showComment={false}
autoFillTitle={true} autoFillTitle={true}
/> />

View File

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

View File

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

View File

@ -3,14 +3,11 @@ import { Tooltip } from 'react-tooltip';
import { getOperationName } from '@apollo/client/utilities'; import { getOperationName } from '@apollo/client/utilities';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useOpenCommentThreadRightDrawer } from '@/activities/hooks/useOpenCommentThreadRightDrawer'; import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
import { GET_COMMENT_THREADS_BY_TARGETS } from '@/activities/queries'; import { GET_ACTIVITIES_BY_TARGETS } from '@/activities/queries';
import { IconNotes } from '@/ui/icon'; import { IconNotes } from '@/ui/icon';
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip'; import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
import { import { Activity, useUpdateActivityMutation } from '~/generated/graphql';
CommentThread,
useUpdateCommentThreadMutation,
} from '~/generated/graphql';
import { import {
beautifyExactDate, beautifyExactDate,
beautifyPastDateRelativeToNow, beautifyPastDateRelativeToNow,
@ -115,35 +112,31 @@ const StyledTimelineItemContainer = styled.div`
`; `;
type OwnProps = { type OwnProps = {
commentThread: Pick< activity: Pick<
CommentThread, Activity,
'id' | 'title' | 'body' | 'createdAt' | 'completedAt' | 'type' 'id' | 'title' | 'body' | 'createdAt' | 'completedAt' | 'type'
> & { author: Pick<CommentThread['author'], 'displayName'> }; > & { author: Pick<Activity['author'], 'displayName'> };
}; };
export function TimelineActivity({ commentThread }: OwnProps) { export function TimelineActivity({ activity }: OwnProps) {
const beautifiedCreatedAt = beautifyPastDateRelativeToNow( const beautifiedCreatedAt = beautifyPastDateRelativeToNow(activity.createdAt);
commentThread.createdAt, const exactCreatedAt = beautifyExactDate(activity.createdAt);
); const body = JSON.parse(activity.body ?? '{}')[0]?.content[0]?.text;
const exactCreatedAt = beautifyExactDate(commentThread.createdAt);
const body = JSON.parse(commentThread.body ?? '{}')[0]?.content[0]?.text;
const openCommentThreadRightDrawer = useOpenCommentThreadRightDrawer(); const openActivityRightDrawer = useOpenActivityRightDrawer();
const [updateCommentThreadMutation] = useUpdateCommentThreadMutation(); const [updateActivityMutation] = useUpdateActivityMutation();
const handleActivityCompletionChange = useCallback( const handleActivityCompletionChange = useCallback(
(value: boolean) => { (value: boolean) => {
updateCommentThreadMutation({ updateActivityMutation({
variables: { variables: {
id: commentThread.id, id: activity.id,
completedAt: value ? new Date().toISOString() : null, completedAt: value ? new Date().toISOString() : null,
}, },
refetchQueries: [ refetchQueries: [getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? ''],
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
],
}); });
}, },
[commentThread, updateCommentThreadMutation], [activity, updateActivityMutation],
); );
return ( return (
@ -153,14 +146,14 @@ export function TimelineActivity({ commentThread }: OwnProps) {
<IconNotes /> <IconNotes />
</StyledIconContainer> </StyledIconContainer>
<StyledItemTitleContainer> <StyledItemTitleContainer>
<span>{commentThread.author.displayName}</span> <span>{activity.author.displayName}</span>
created a {commentThread.type.toLowerCase()} created a note created a {activity.type.toLowerCase()}
</StyledItemTitleContainer> </StyledItemTitleContainer>
<StyledItemTitleDate id={`id-${commentThread.id}`}> <StyledItemTitleDate id={`id-${activity.id}`}>
{beautifiedCreatedAt} ago {beautifiedCreatedAt} ago
</StyledItemTitleDate> </StyledItemTitleDate>
<StyledTooltip <StyledTooltip
anchorSelect={`#id-${commentThread.id}`} anchorSelect={`#id-${activity.id}`}
content={exactCreatedAt} content={exactCreatedAt}
clickable clickable
noArrow noArrow
@ -171,13 +164,11 @@ export function TimelineActivity({ commentThread }: OwnProps) {
<StyledVerticalLine></StyledVerticalLine> <StyledVerticalLine></StyledVerticalLine>
</StyledVerticalLineContainer> </StyledVerticalLineContainer>
<StyledCardContainer> <StyledCardContainer>
<StyledCard <StyledCard onClick={() => openActivityRightDrawer(activity.id)}>
onClick={() => openCommentThreadRightDrawer(commentThread.id)}
>
<TimelineActivityTitle <TimelineActivityTitle
title={commentThread.title ?? ''} title={activity.title ?? ''}
completed={!!commentThread.completedAt} completed={!!activity.completedAt}
type={commentThread.type} type={activity.type}
onCompletionChange={handleActivityCompletionChange} onCompletionChange={handleActivityCompletionChange}
/> />
<StyledCardContent> <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< export type CommentForDrawer = NonNullable<ActivityForDrawer['comments']>[0];
CommentThreadForDrawer['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 { tokenPairState } from '@/auth/states/tokenPairState';
import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { CommentThreadTarget } from '~/generated/graphql'; import { ActivityTarget } from '~/generated/graphql';
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
import { useUpdateEffect } from '~/hooks/useUpdateEffect'; import { useUpdateEffect } from '~/hooks/useUpdateEffect';
@ -25,12 +25,12 @@ export function useApolloFactory() {
uri: `${process.env.REACT_APP_API_URL}`, uri: `${process.env.REACT_APP_API_URL}`,
cache: new InMemoryCache({ cache: new InMemoryCache({
typePolicies: { typePolicies: {
CommentThread: { Activity: {
fields: { fields: {
commentThreadTargets: { activityTargets: {
merge( merge(
_existing: CommentThreadTarget[] = [], _existing: ActivityTarget[] = [],
incoming: CommentThreadTarget[], incoming: ActivityTarget[],
) { ) {
return [...incoming]; return [...incoming];
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ export function EditablePeopleFullNameCell() {
<EditablePeopleFullName <EditablePeopleFullName
person={{ person={{
id: currentRowEntityId ?? undefined, id: currentRowEntityId ?? undefined,
_commentThreadCount: commentCount ?? undefined, _activityCount: commentCount ?? undefined,
firstName, firstName,
lastName, lastName,
displayName: displayName ?? undefined, 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 { useRecoilState } from 'recoil';
import { RightDrawerCreateCommentThread } from '@/activities/right-drawer/components/create/RightDrawerCreateCommentThread'; import { RightDrawerCreateActivity } from '@/activities/right-drawer/components/create/RightDrawerCreateActivity';
import { RightDrawerEditCommentThread } from '@/activities/right-drawer/components/edit/RightDrawerEditCommentThread'; import { RightDrawerEditActivity } from '@/activities/right-drawer/components/edit/RightDrawerEditActivity';
import { RightDrawerTimeline } from '@/activities/right-drawer/components/RightDrawerTimeline'; import { RightDrawerTimeline } from '@/activities/right-drawer/components/RightDrawerTimeline';
import { isDefined } from '~/utils/isDefined'; import { isDefined } from '~/utils/isDefined';
@ -18,10 +18,10 @@ export function RightDrawerRouter() {
switch (rightDrawerPage) { switch (rightDrawerPage) {
case RightDrawerPages.Timeline: case RightDrawerPages.Timeline:
return <RightDrawerTimeline />; return <RightDrawerTimeline />;
case RightDrawerPages.CreateCommentThread: case RightDrawerPages.CreateActivity:
return <RightDrawerCreateCommentThread />; return <RightDrawerCreateActivity />;
case RightDrawerPages.EditCommentThread: case RightDrawerPages.EditActivity:
return <RightDrawerEditCommentThread />; return <RightDrawerEditActivity />;
default: default:
return <></>; return <></>;
} }

View File

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

View File

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

View File

@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import { GET_COMPANIES } from '@/companies/queries'; import { GET_COMPANIES } from '@/companies/queries';
import { CompanyTable } from '@/companies/table/components/CompanyTable'; import { CompanyTable } from '@/companies/table/components/CompanyTable';
import { TableActionBarButtonCreateCommentThreadCompany } from '@/companies/table/components/TableActionBarButtonCreateCommentThreadCompany'; import { TableActionBarButtonCreateActivityCompany } from '@/companies/table/components/TableActionBarButtonCreateActivityCompany';
import { TableActionBarButtonDeleteCompanies } from '@/companies/table/components/TableActionBarButtonDeleteCompanies'; import { TableActionBarButtonDeleteCompanies } from '@/companies/table/components/TableActionBarButtonDeleteCompanies';
import { IconBuildingSkyscraper } from '@/ui/icon'; import { IconBuildingSkyscraper } from '@/ui/icon';
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer'; import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
@ -53,7 +53,7 @@ export function Companies() {
<CompanyTable /> <CompanyTable />
</StyledTableContainer> </StyledTableContainer>
<EntityTableActionBar> <EntityTableActionBar>
<TableActionBarButtonCreateCommentThreadCompany /> <TableActionBarButtonCreateActivityCompany />
<TableActionBarButtonDeleteCompanies /> <TableActionBarButtonDeleteCompanies />
</EntityTableActionBar> </EntityTableActionBar>
</RecoilScope> </RecoilScope>

View File

@ -2,7 +2,7 @@ import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { CompanyTableMockMode } from '@/companies/table/components/CompanyTableMockMode'; import { CompanyTableMockMode } from '@/companies/table/components/CompanyTableMockMode';
import { TableActionBarButtonCreateCommentThreadCompany } from '@/companies/table/components/TableActionBarButtonCreateCommentThreadCompany'; import { TableActionBarButtonCreateActivityCompany } from '@/companies/table/components/TableActionBarButtonCreateActivityCompany';
import { TableActionBarButtonDeleteCompanies } from '@/companies/table/components/TableActionBarButtonDeleteCompanies'; import { TableActionBarButtonDeleteCompanies } from '@/companies/table/components/TableActionBarButtonDeleteCompanies';
import { IconBuildingSkyscraper } from '@/ui/icon'; import { IconBuildingSkyscraper } from '@/ui/icon';
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer'; import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
@ -29,7 +29,7 @@ export function CompaniesMockMode() {
<CompanyTableMockMode /> <CompanyTableMockMode />
</StyledTableContainer> </StyledTableContainer>
<EntityTableActionBar> <EntityTableActionBar>
<TableActionBarButtonCreateCommentThreadCompany /> <TableActionBarButtonCreateActivityCompany />
<TableActionBarButtonDeleteCompanies /> <TableActionBarButtonDeleteCompanies />
</EntityTableActionBar> </EntityTableActionBar>
</RecoilScope> </RecoilScope>

View File

@ -4,18 +4,15 @@ import type { Meta, StoryObj } from '@storybook/react';
import { fireEvent, within } from '@storybook/testing-library'; import { fireEvent, within } from '@storybook/testing-library';
import { graphql } from 'msw'; import { graphql } from 'msw';
import { import { GET_ACTIVITIES_BY_TARGETS, GET_ACTIVITY } from '@/activities/queries';
GET_COMMENT_THREAD, import { CREATE_ACTIVITY_WITH_COMMENT } from '@/activities/queries/create';
GET_COMMENT_THREADS_BY_TARGETS,
} from '@/activities/queries';
import { CREATE_COMMENT_THREAD_WITH_COMMENT } from '@/activities/queries/create';
import { GET_COMPANY } from '@/companies/queries'; import { GET_COMPANY } from '@/companies/queries';
import { import {
PageDecorator, PageDecorator,
type PageDecoratorArgs, type PageDecoratorArgs,
} from '~/testing/decorators/PageDecorator'; } from '~/testing/decorators/PageDecorator';
import { graphqlMocks } from '~/testing/graphqlMocks'; import { graphqlMocks } from '~/testing/graphqlMocks';
import { mockedCommentThreads } from '~/testing/mock-data/comment-threads'; import { mockedActivities } from '~/testing/mock-data/activities';
import { mockedCompaniesData } from '~/testing/mock-data/companies'; import { mockedCompaniesData } from '~/testing/mock-data/companies';
import { CompanyShow } from '../CompanyShow'; import { CompanyShow } from '../CompanyShow';
@ -30,11 +27,11 @@ const meta: Meta<PageDecoratorArgs> = {
msw: [ msw: [
...graphqlMocks, ...graphqlMocks,
graphql.query( graphql.query(
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '', getOperationName(GET_ACTIVITIES_BY_TARGETS) ?? '',
(req, res, ctx) => { (req, res, ctx) => {
return res( return res(
ctx.data({ ctx.data({
findManyCommentThreads: mockedCommentThreads, findManyActivities: mockedActivities,
}), }),
); );
}, },
@ -82,25 +79,22 @@ export const EditNote: Story = {
msw: [ msw: [
...meta.parameters?.msw, ...meta.parameters?.msw,
graphql.mutation( graphql.mutation(
getOperationName(CREATE_COMMENT_THREAD_WITH_COMMENT) ?? '', getOperationName(CREATE_ACTIVITY_WITH_COMMENT) ?? '',
(req, res, ctx) => { (req, res, ctx) => {
return res( return res(
ctx.data({ ctx.data({
createOneCommentThread: mockedCommentThreads[0], createOneActivity: mockedActivities[0],
}),
);
},
),
graphql.query(
getOperationName(GET_COMMENT_THREAD) ?? '',
(req, res, ctx) => {
return res(
ctx.data({
findManyCommentThreads: [mockedCommentThreads[0]],
}), }),
); );
}, },
), ),
graphql.query(getOperationName(GET_ACTIVITY) ?? '', (req, res, ctx) => {
return res(
ctx.data({
findManyActivitys: [mockedActivities[0]],
}),
);
}),
], ],
}, },
}; };

View File

@ -4,7 +4,7 @@ import styled from '@emotion/styled';
import { GET_PEOPLE } from '@/people/queries'; import { GET_PEOPLE } from '@/people/queries';
import { PeopleTable } from '@/people/table/components/PeopleTable'; import { PeopleTable } from '@/people/table/components/PeopleTable';
import { TableActionBarButtonCreateCommentThreadPeople } from '@/people/table/components/TableActionBarButtonCreateCommentThreadPeople'; import { TableActionBarButtonCreateActivityPeople } from '@/people/table/components/TableActionBarButtonCreateActivityPeople';
import { TableActionBarButtonDeletePeople } from '@/people/table/components/TableActionBarButtonDeletePeople'; import { TableActionBarButtonDeletePeople } from '@/people/table/components/TableActionBarButtonDeletePeople';
import { IconUser } from '@/ui/icon'; import { IconUser } from '@/ui/icon';
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer'; import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
@ -46,7 +46,7 @@ export function People() {
<PeopleTable /> <PeopleTable />
</StyledTableContainer> </StyledTableContainer>
<EntityTableActionBar> <EntityTableActionBar>
<TableActionBarButtonCreateCommentThreadPeople /> <TableActionBarButtonCreateActivityPeople />
<TableActionBarButtonDeletePeople /> <TableActionBarButtonDeletePeople />
</EntityTableActionBar> </EntityTableActionBar>
</WithTopBarContainer> </WithTopBarContainer>

View File

@ -1,13 +1,13 @@
import { import {
Activity,
ActivityTarget,
ActivityType, ActivityType,
Comment, Comment,
CommentableType, CommentableType,
CommentThread,
CommentThreadTarget,
} from '~/generated/graphql'; } from '~/generated/graphql';
type MockedCommentThread = Pick< type MockedActivity = Pick<
CommentThread, Activity,
| 'id' | 'id'
| 'createdAt' | 'createdAt'
| 'updatedAt' | 'updatedAt'
@ -25,21 +25,21 @@ type MockedCommentThread = Pick<
displayName: string; displayName: string;
}; };
comments: Array<Pick<Comment, 'body'>>; comments: Array<Pick<Comment, 'body'>>;
commentThreadTargets: Array< activityTargets: Array<
Pick< Pick<
CommentThreadTarget, ActivityTarget,
| 'id' | 'id'
| '__typename' | '__typename'
| 'createdAt' | 'createdAt'
| 'updatedAt' | 'updatedAt'
| 'commentableType' | 'activityId'
| 'commentableId' | 'commentableId'
| 'commentThreadId' | 'commentableType'
> & { commentThread: Pick<CommentThread, 'id' | 'createdAt' | 'updatedAt'> } > & { activity: Pick<Activity, 'id' | 'createdAt' | 'updatedAt'> }
>; >;
}; };
export const mockedCommentThreads: Array<MockedCommentThread> = [ export const mockedActivities: Array<MockedActivity> = [
{ {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb230', id: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
createdAt: '2023-04-26T10:12:42.33625+00:00', createdAt: '2023-04-26T10:12:42.33625+00:00',
@ -55,20 +55,20 @@ export const mockedCommentThreads: Array<MockedCommentThread> = [
}, },
authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
comments: [], comments: [],
commentThreadTargets: [ activityTargets: [
{ {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300', id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
createdAt: '2023-04-26T10:12:42.33625+00:00', createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00', updatedAt: '2023-04-26T10:23:42.33625+00:00',
commentableType: CommentableType.Company, commentableType: CommentableType.Company,
commentableId: '89bb825c-171e-4bcc-9cf7-43448d6fb278', // airbnb commentableId: '89bb825c-171e-4bcc-9cf7-43448d6fb278', // airbnb
commentThreadId: '89bb825c-171e-4bcc-9cf7-43448d6fb230', activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
commentThread: { activity: {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb230', id: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
createdAt: '2023-04-26T10:12:42.33625+00:00', createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00', updatedAt: '2023-04-26T10:23:42.33625+00:00',
}, },
__typename: 'CommentThreadTarget', __typename: 'ActivityTarget',
}, },
{ {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb301', id: '89bb825c-171e-4bcc-9cf7-43448d6fb301',
@ -76,16 +76,16 @@ export const mockedCommentThreads: Array<MockedCommentThread> = [
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
commentableType: CommentableType.Company, commentableType: CommentableType.Company,
commentableId: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae', // aircall commentableId: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae', // aircall
commentThreadId: '89bb825c-171e-4bcc-9cf7-43448d6fb231', activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb231',
commentThread: { activity: {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb231', id: '89bb825c-171e-4bcc-9cf7-43448d6fb231',
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}, },
__typename: 'CommentThreadTarget', __typename: 'ActivityTarget',
}, },
], ],
__typename: 'CommentThread', __typename: 'Activity',
}, },
{ {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
@ -102,20 +102,20 @@ export const mockedCommentThreads: Array<MockedCommentThread> = [
}, },
authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e', authorId: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
comments: [], comments: [],
commentThreadTargets: [ activityTargets: [
{ {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
createdAt: '2023-04-26T10:12:42.33625+00:00', createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00', updatedAt: '2023-04-26T10:23:42.33625+00:00',
commentableType: CommentableType.Person, commentableType: CommentableType.Person,
commentableId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', // Alexandre commentableId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', // Alexandre
commentThreadId: '89bb825c-171e-4bcc-9cf7-43448d6fb278', activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
commentThread: { activity: {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
createdAt: '2023-04-26T10:12:42.33625+00:00', createdAt: '2023-04-26T10:12:42.33625+00:00',
updatedAt: '2023-04-26T10:23:42.33625+00:00', updatedAt: '2023-04-26T10:23:42.33625+00:00',
}, },
__typename: 'CommentThreadTarget', __typename: 'ActivityTarget',
}, },
{ {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
@ -123,15 +123,15 @@ export const mockedCommentThreads: Array<MockedCommentThread> = [
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
commentableType: CommentableType.Person, commentableType: CommentableType.Person,
commentableId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d', // Jean d'Eau commentableId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d', // Jean d'Eau
commentThreadId: '89bb825c-171e-4bcc-9cf7-43448d6fb278', activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
commentThread: { activity: {
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278', id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}, },
__typename: 'CommentThreadTarget', __typename: 'ActivityTarget',
}, },
], ],
__typename: 'CommentThread', __typename: 'Activity',
}, },
]; ];

View File

@ -10,7 +10,7 @@ type MockedCompany = Pick<
| 'address' | 'address'
| 'employees' | 'employees'
| 'linkedinUrl' | 'linkedinUrl'
| '_commentThreadCount' | '_activityCount'
> & { > & {
accountOwner: Pick< accountOwner: Pick<
User, User,
@ -33,7 +33,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
address: '17 rue de clignancourt', address: '17 rue de clignancourt',
employees: 12, employees: 12,
linkedinUrl: 'https://www.linkedin.com/company/airbnb/', linkedinUrl: 'https://www.linkedin.com/company/airbnb/',
_commentThreadCount: 1, _activityCount: 1,
accountOwner: { accountOwner: {
email: 'charles@test.com', email: 'charles@test.com',
displayName: 'Charles Test', displayName: 'Charles Test',
@ -53,7 +53,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
address: '', address: '',
employees: 1, employees: 1,
linkedinUrl: 'https://www.linkedin.com/company/aircall/', linkedinUrl: 'https://www.linkedin.com/company/aircall/',
_commentThreadCount: 1, _activityCount: 1,
accountOwner: null, accountOwner: null,
__typename: 'Company', __typename: 'Company',
}, },
@ -65,7 +65,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
address: '', address: '',
employees: 1, employees: 1,
linkedinUrl: 'https://www.linkedin.com/company/algolia/', linkedinUrl: 'https://www.linkedin.com/company/algolia/',
_commentThreadCount: 1, _activityCount: 1,
accountOwner: null, accountOwner: null,
__typename: 'Company', __typename: 'Company',
}, },
@ -77,7 +77,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
address: '', address: '',
employees: 10, employees: 10,
linkedinUrl: 'https://www.linkedin.com/company/apple/', linkedinUrl: 'https://www.linkedin.com/company/apple/',
_commentThreadCount: 0, _activityCount: 0,
accountOwner: null, accountOwner: null,
__typename: 'Company', __typename: 'Company',
}, },
@ -89,7 +89,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
address: '10 rue de la Paix', address: '10 rue de la Paix',
employees: 1, employees: 1,
linkedinUrl: 'https://www.linkedin.com/company/qonto/', linkedinUrl: 'https://www.linkedin.com/company/qonto/',
_commentThreadCount: 2, _activityCount: 2,
accountOwner: null, accountOwner: null,
__typename: 'Company', __typename: 'Company',
}, },
@ -101,7 +101,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
address: '', address: '',
employees: 1, employees: 1,
linkedinUrl: 'https://www.linkedin.com/company/facebook/', linkedinUrl: 'https://www.linkedin.com/company/facebook/',
_commentThreadCount: 13, _activityCount: 13,
accountOwner: null, accountOwner: null,
__typename: 'Company', __typename: 'Company',
}, },
@ -113,7 +113,7 @@ export const mockedCompaniesData: Array<MockedCompany> = [
address: '', address: '',
employees: 1, employees: 1,
linkedinUrl: 'https://www.linkedin.com/company/sequoia/', linkedinUrl: 'https://www.linkedin.com/company/sequoia/',
_commentThreadCount: 1, _activityCount: 1,
accountOwner: null, accountOwner: null,
__typename: 'Company', __typename: 'Company',
}, },

View File

@ -17,7 +17,7 @@ type MockedPerson = RequiredAndNotNull<
| '__typename' | '__typename'
| 'phone' | 'phone'
| 'city' | 'city'
| '_commentThreadCount' | '_activityCount'
| 'createdAt' | 'createdAt'
> & { > & {
company: Pick<Company, 'id' | 'name' | 'domainName' | '__typename'>; company: Pick<Company, 'id' | 'name' | 'domainName' | '__typename'>;
@ -41,7 +41,7 @@ export const mockedPeopleData: MockedPerson[] = [
__typename: 'Company', __typename: 'Company',
}, },
phone: '06 12 34 56 78', phone: '06 12 34 56 78',
_commentThreadCount: 1, _activityCount: 1,
createdAt: '2023-04-20T13:20:09.158312+00:00', createdAt: '2023-04-20T13:20:09.158312+00:00',
city: 'Paris', city: 'Paris',
@ -62,7 +62,7 @@ export const mockedPeopleData: MockedPerson[] = [
__typename: 'Company', __typename: 'Company',
}, },
phone: '06 12 34 56 78', phone: '06 12 34 56 78',
_commentThreadCount: 1, _activityCount: 1,
createdAt: '2023-04-20T13:20:09.158312+00:00', createdAt: '2023-04-20T13:20:09.158312+00:00',
city: 'Paris', city: 'Paris',
@ -83,7 +83,7 @@ export const mockedPeopleData: MockedPerson[] = [
__typename: 'Company', __typename: 'Company',
}, },
phone: '06 12 34 56 78', phone: '06 12 34 56 78',
_commentThreadCount: 1, _activityCount: 1,
createdAt: '2023-04-20T13:20:09.158312+00:00', createdAt: '2023-04-20T13:20:09.158312+00:00',
city: 'Paris', city: 'Paris',
@ -104,7 +104,7 @@ export const mockedPeopleData: MockedPerson[] = [
__typename: 'Company', __typename: 'Company',
}, },
phone: '06 12 34 56 78', phone: '06 12 34 56 78',
_commentThreadCount: 2, _activityCount: 2,
createdAt: '2023-04-20T13:20:09.158312+00:00', createdAt: '2023-04-20T13:20:09.158312+00:00',
city: 'Paris', city: 'Paris',

View File

@ -3,7 +3,8 @@ import { Injectable } from '@nestjs/common';
import { PureAbility, AbilityBuilder } from '@casl/ability'; import { PureAbility, AbilityBuilder } from '@casl/ability';
import { createPrismaAbility, PrismaQuery, Subjects } from '@casl/prisma'; import { createPrismaAbility, PrismaQuery, Subjects } from '@casl/prisma';
import { import {
CommentThread, Attachment,
Activity,
Company, Company,
Comment, Comment,
Person, Person,
@ -11,11 +12,10 @@ import {
User, User,
Workspace, Workspace,
WorkspaceMember, WorkspaceMember,
CommentThreadTarget, ActivityTarget,
Pipeline, Pipeline,
PipelineStage, PipelineStage,
PipelineProgress, PipelineProgress,
Attachment,
UserSettings, UserSettings,
ViewField, ViewField,
} from '@prisma/client'; } from '@prisma/client';
@ -29,9 +29,9 @@ type SubjectsAbility = Subjects<{
Company: Company; Company: Company;
Person: Person; Person: Person;
RefreshToken: RefreshToken; RefreshToken: RefreshToken;
CommentThread: CommentThread; Activity: Activity;
Comment: Comment; Comment: Comment;
CommentThreadTarget: CommentThreadTarget; ActivityTarget: ActivityTarget;
Pipeline: Pipeline; Pipeline: Pipeline;
PipelineStage: PipelineStage; PipelineStage: PipelineStage;
PipelineProgress: PipelineProgress; PipelineProgress: PipelineProgress;
@ -86,11 +86,11 @@ export class AbilityFactory {
// RefreshToken // RefreshToken
cannot(AbilityAction.Manage, 'RefreshToken'); cannot(AbilityAction.Manage, 'RefreshToken');
// CommentThread // Activity
can(AbilityAction.Read, 'CommentThread', { workspaceId: workspace.id }); can(AbilityAction.Read, 'Activity', { workspaceId: workspace.id });
can(AbilityAction.Create, 'CommentThread'); can(AbilityAction.Create, 'Activity');
can(AbilityAction.Update, 'CommentThread', { workspaceId: workspace.id }); can(AbilityAction.Update, 'Activity', { workspaceId: workspace.id });
can(AbilityAction.Delete, 'CommentThread', { workspaceId: workspace.id }); can(AbilityAction.Delete, 'Activity', { workspaceId: workspace.id });
// Comment // Comment
can(AbilityAction.Read, 'Comment', { workspaceId: workspace.id }); can(AbilityAction.Read, 'Comment', { workspaceId: workspace.id });
@ -104,9 +104,9 @@ export class AbilityFactory {
authorId: user.id, authorId: user.id,
}); });
// CommentThreadTarget // ActivityTarget
can(AbilityAction.Read, 'CommentThreadTarget'); can(AbilityAction.Read, 'ActivityTarget');
can(AbilityAction.Create, 'CommentThreadTarget'); can(AbilityAction.Create, 'ActivityTarget');
// Attachment // Attachment
can(AbilityAction.Read, 'Attachment', { workspaceId: workspace.id }); can(AbilityAction.Read, 'Attachment', { workspaceId: workspace.id });

View File

@ -46,12 +46,12 @@ import {
DeleteRefreshTokenAbilityHandler, DeleteRefreshTokenAbilityHandler,
} from './handlers/refresh-token.ability-handler'; } from './handlers/refresh-token.ability-handler';
import { import {
ManageCommentThreadAbilityHandler, ManageActivityAbilityHandler,
ReadCommentThreadAbilityHandler, ReadActivityAbilityHandler,
CreateCommentThreadAbilityHandler, CreateActivityAbilityHandler,
UpdateCommentThreadAbilityHandler, UpdateActivityAbilityHandler,
DeleteCommentThreadAbilityHandler, DeleteActivityAbilityHandler,
} from './handlers/comment-thread.ability-handler'; } from './handlers/activity.ability-handler';
import { import {
ManageCommentAbilityHandler, ManageCommentAbilityHandler,
ReadCommentAbilityHandler, ReadCommentAbilityHandler,
@ -60,12 +60,12 @@ import {
DeleteCommentAbilityHandler, DeleteCommentAbilityHandler,
} from './handlers/comment.ability-handler'; } from './handlers/comment.ability-handler';
import { import {
ManageCommentThreadTargetAbilityHandler, ManageActivityTargetAbilityHandler,
ReadCommentThreadTargetAbilityHandler, ReadActivityTargetAbilityHandler,
CreateCommentThreadTargetAbilityHandler, CreateActivityTargetAbilityHandler,
UpdateCommentThreadTargetAbilityHandler, UpdateActivityTargetAbilityHandler,
DeleteCommentThreadTargetAbilityHandler, DeleteActivityTargetAbilityHandler,
} from './handlers/comment-thread-target.ability-handler'; } from './handlers/activity-target.ability-handler';
import { import {
ManagePipelineAbilityHandler, ManagePipelineAbilityHandler,
ReadPipelineAbilityHandler, ReadPipelineAbilityHandler,
@ -140,24 +140,24 @@ import {
CreateRefreshTokenAbilityHandler, CreateRefreshTokenAbilityHandler,
UpdateRefreshTokenAbilityHandler, UpdateRefreshTokenAbilityHandler,
DeleteRefreshTokenAbilityHandler, DeleteRefreshTokenAbilityHandler,
// CommentThread // Activity
ManageCommentThreadAbilityHandler, ManageActivityAbilityHandler,
ReadCommentThreadAbilityHandler, ReadActivityAbilityHandler,
CreateCommentThreadAbilityHandler, CreateActivityAbilityHandler,
UpdateCommentThreadAbilityHandler, UpdateActivityAbilityHandler,
DeleteCommentThreadAbilityHandler, DeleteActivityAbilityHandler,
// Comment // Comment
ManageCommentAbilityHandler, ManageCommentAbilityHandler,
ReadCommentAbilityHandler, ReadCommentAbilityHandler,
CreateCommentAbilityHandler, CreateCommentAbilityHandler,
UpdateCommentAbilityHandler, UpdateCommentAbilityHandler,
DeleteCommentAbilityHandler, DeleteCommentAbilityHandler,
// CommentThreadTarget // ActivityTarget
ManageCommentThreadTargetAbilityHandler, ManageActivityTargetAbilityHandler,
ReadCommentThreadTargetAbilityHandler, ReadActivityTargetAbilityHandler,
CreateCommentThreadTargetAbilityHandler, CreateActivityTargetAbilityHandler,
UpdateCommentThreadTargetAbilityHandler, UpdateActivityTargetAbilityHandler,
DeleteCommentThreadTargetAbilityHandler, DeleteActivityTargetAbilityHandler,
//Attachment //Attachment
ManageAttachmentAbilityHandler, ManageAttachmentAbilityHandler,
ReadAttachmentAbilityHandler, ReadAttachmentAbilityHandler,
@ -224,24 +224,24 @@ import {
CreateRefreshTokenAbilityHandler, CreateRefreshTokenAbilityHandler,
UpdateRefreshTokenAbilityHandler, UpdateRefreshTokenAbilityHandler,
DeleteRefreshTokenAbilityHandler, DeleteRefreshTokenAbilityHandler,
// CommentThread // Activity
ManageCommentThreadAbilityHandler, ManageActivityAbilityHandler,
ReadCommentThreadAbilityHandler, ReadActivityAbilityHandler,
CreateCommentThreadAbilityHandler, CreateActivityAbilityHandler,
UpdateCommentThreadAbilityHandler, UpdateActivityAbilityHandler,
DeleteCommentThreadAbilityHandler, DeleteActivityAbilityHandler,
// Comment // Comment
ManageCommentAbilityHandler, ManageCommentAbilityHandler,
ReadCommentAbilityHandler, ReadCommentAbilityHandler,
CreateCommentAbilityHandler, CreateCommentAbilityHandler,
UpdateCommentAbilityHandler, UpdateCommentAbilityHandler,
DeleteCommentAbilityHandler, DeleteCommentAbilityHandler,
// CommentThreadTarget // ActivityTarget
ManageCommentThreadTargetAbilityHandler, ManageActivityTargetAbilityHandler,
ReadCommentThreadTargetAbilityHandler, ReadActivityTargetAbilityHandler,
CreateCommentThreadTargetAbilityHandler, CreateActivityTargetAbilityHandler,
UpdateCommentThreadTargetAbilityHandler, UpdateActivityTargetAbilityHandler,
DeleteCommentThreadTargetAbilityHandler, DeleteActivityTargetAbilityHandler,
//Attachment //Attachment
ManageAttachmentAbilityHandler, ManageAttachmentAbilityHandler,
ReadAttachmentAbilityHandler, ReadAttachmentAbilityHandler,

View File

@ -0,0 +1,79 @@
import {
ExecutionContext,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { subject } from '@casl/ability';
import { IAbilityHandler } from 'src/ability/interfaces/ability-handler.interface';
import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory';
import { assert } from 'src/utils/assert';
import { ActivityTargetWhereInput } from 'src/core/@generated/activity-target/activity-target-where.input';
class ActivityTargetArgs {
where?: ActivityTargetWhereInput;
}
@Injectable()
export class ManageActivityTargetAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility) {
return ability.can(AbilityAction.Manage, 'ActivityTarget');
}
}
@Injectable()
export class ReadActivityTargetAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) {
return ability.can(AbilityAction.Read, 'ActivityTarget');
}
}
@Injectable()
export class CreateActivityTargetAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) {
return ability.can(AbilityAction.Create, 'ActivityTarget');
}
}
@Injectable()
export class UpdateActivityTargetAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<ActivityTargetArgs>();
const ActivityTarget = await this.prismaService.client.activityTarget.findFirst({
where: args.where,
});
assert(ActivityTarget, '', NotFoundException);
return ability.can(
AbilityAction.Update,
subject('ActivityTarget', ActivityTarget),
);
}
}
@Injectable()
export class DeleteActivityTargetAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<ActivityTargetArgs>();
const ActivityTarget = await this.prismaService.client.activityTarget.findFirst({
where: args.where,
});
assert(ActivityTarget, '', NotFoundException);
return ability.can(
AbilityAction.Delete,
subject('ActivityTarget', ActivityTarget),
);
}
}

View File

@ -0,0 +1,73 @@
import {
ExecutionContext,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { subject } from '@casl/ability';
import { IAbilityHandler } from 'src/ability/interfaces/ability-handler.interface';
import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory';
import { assert } from 'src/utils/assert';
import { ActivityWhereInput } from 'src/core/@generated/activity/activity-where.input';
class ActivityArgs {
where?: ActivityWhereInput;
}
@Injectable()
export class ManageActivityAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility) {
return ability.can(AbilityAction.Manage, 'Activity');
}
}
@Injectable()
export class ReadActivityAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) {
return ability.can(AbilityAction.Read, 'Activity');
}
}
@Injectable()
export class CreateActivityAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) {
return ability.can(AbilityAction.Create, 'Activity');
}
}
@Injectable()
export class UpdateActivityAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<ActivityArgs>();
const Activity = await this.prismaService.client.activity.findFirst({
where: args.where,
});
assert(Activity, '', NotFoundException);
return ability.can(AbilityAction.Update, subject('Activity', Activity));
}
}
@Injectable()
export class DeleteActivityAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<ActivityArgs>();
const Activity = await this.prismaService.client.activity.findFirst({
where: args.where,
});
assert(Activity, '', NotFoundException);
return ability.can(AbilityAction.Delete, subject('Activity', Activity));
}
}

View File

@ -42,16 +42,12 @@ export class CreateAttachmentAbilityHandler implements IAbilityHandler {
const args = gqlContext.getArgs<AttachmentArgs>(); const args = gqlContext.getArgs<AttachmentArgs>();
assert(args.activityId, '', ForbiddenException); assert(args.activityId, '', ForbiddenException);
const activity = await this.prismaService.client.commentThread.findUnique({ const activity = await this.prismaService.client.activity.findUnique({
where: { id: args.activityId }, where: { id: args.activityId },
include: { workspace: true },
}); });
assert(activity, '', NotFoundException); assert(activity, '', NotFoundException);
return ability.can( return ability.can(AbilityAction.Update, subject('Activity', activity));
AbilityAction.Update,
subject('Workspace', activity.workspace),
);
} }
} }

View File

@ -1,118 +0,0 @@
import {
ExecutionContext,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { subject } from '@casl/ability';
import { IAbilityHandler } from 'src/ability/interfaces/ability-handler.interface';
import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory';
import { CommentThreadTargetWhereInput } from 'src/core/@generated/comment-thread-target/comment-thread-target-where.input';
import { relationAbilityChecker } from 'src/ability/ability.util';
import { assert } from 'src/utils/assert';
class CommentThreadTargetArgs {
where?: CommentThreadTargetWhereInput;
[key: string]: any;
}
@Injectable()
export class ManageCommentThreadTargetAbilityHandler
implements IAbilityHandler
{
async handle(ability: AppAbility) {
return ability.can(AbilityAction.Manage, 'CommentThreadTarget');
}
}
@Injectable()
export class ReadCommentThreadTargetAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) {
return ability.can(AbilityAction.Read, 'CommentThreadTarget');
}
}
@Injectable()
export class CreateCommentThreadTargetAbilityHandler
implements IAbilityHandler
{
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'CommentThreadTarget',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'CommentThreadTarget');
}
}
@Injectable()
export class UpdateCommentThreadTargetAbilityHandler
implements IAbilityHandler
{
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<CommentThreadTargetArgs>();
const commentThreadTarget =
await this.prismaService.client.commentThreadTarget.findFirst({
where: args.where,
});
assert(commentThreadTarget, '', NotFoundException);
const allowed = await relationAbilityChecker(
'CommentThreadTarget',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(
AbilityAction.Update,
subject('CommentThreadTarget', commentThreadTarget),
);
}
}
@Injectable()
export class DeleteCommentThreadTargetAbilityHandler
implements IAbilityHandler
{
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<CommentThreadTargetArgs>();
const commentThreadTarget =
await this.prismaService.client.commentThreadTarget.findFirst({
where: args.where,
});
assert(commentThreadTarget, '', NotFoundException);
return ability.can(
AbilityAction.Delete,
subject('CommentThreadTarget', commentThreadTarget),
);
}
}

View File

@ -1,110 +0,0 @@
import {
ExecutionContext,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { subject } from '@casl/ability';
import { IAbilityHandler } from 'src/ability/interfaces/ability-handler.interface';
import { PrismaService } from 'src/database/prisma.service';
import { AbilityAction } from 'src/ability/ability.action';
import { AppAbility } from 'src/ability/ability.factory';
import { CommentThreadWhereInput } from 'src/core/@generated/comment-thread/comment-thread-where.input';
import { relationAbilityChecker } from 'src/ability/ability.util';
import { assert } from 'src/utils/assert';
class CommentThreadArgs {
where?: CommentThreadWhereInput;
[key: string]: any;
}
@Injectable()
export class ManageCommentThreadAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility) {
return ability.can(AbilityAction.Manage, 'CommentThread');
}
}
@Injectable()
export class ReadCommentThreadAbilityHandler implements IAbilityHandler {
handle(ability: AppAbility) {
return ability.can(AbilityAction.Read, 'CommentThread');
}
}
@Injectable()
export class CreateCommentThreadAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'CommentThread',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'CommentThread');
}
}
@Injectable()
export class UpdateCommentThreadAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<CommentThreadArgs>();
const commentThread =
await this.prismaService.client.commentThread.findFirst({
where: args.where,
});
assert(commentThread, '', NotFoundException);
const allowed = await relationAbilityChecker(
'CommentThread',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(
AbilityAction.Update,
subject('CommentThread', commentThread),
);
}
}
@Injectable()
export class DeleteCommentThreadAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<CommentThreadArgs>();
const commentThread =
await this.prismaService.client.commentThread.findFirst({
where: args.where,
});
assert(commentThread, '', NotFoundException);
return ability.can(
AbilityAction.Delete,
subject('CommentThread', commentThread),
);
}
}

View File

@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { ActivityResolver } from './resolvers/activity.resolver';
import { ActivityService } from './services/activity.service';
import { ActivityTargetService } from './services/activity-target.service';
@Module({
providers: [ActivityResolver, ActivityService, ActivityTargetService],
exports: [ActivityService, ActivityTargetService],
})
export class ActivityModule {}

View File

@ -1,19 +1,19 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { CommentThreadService } from 'src/core/comment/services/comment-thread.service'; import { ActivityService } from 'src/core/activity/services/activity.service';
import { AbilityFactory } from 'src/ability/ability.factory'; import { AbilityFactory } from 'src/ability/ability.factory';
import { CommentThreadResolver } from './comment-thread.resolver'; import { ActivityResolver } from './activity.resolver';
describe('CommentThreadResolver', () => { describe('ActivityResolver', () => {
let resolver: CommentThreadResolver; let resolver: ActivityResolver;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
CommentThreadResolver, ActivityResolver,
{ {
provide: CommentThreadService, provide: ActivityService,
useValue: {}, useValue: {},
}, },
{ {
@ -23,7 +23,7 @@ describe('CommentThreadResolver', () => {
], ],
}).compile(); }).compile();
resolver = module.get<CommentThreadResolver>(CommentThreadResolver); resolver = module.get<ActivityResolver>(ActivityResolver);
}); });
it('should be defined', () => { it('should be defined', () => {

View File

@ -0,0 +1,156 @@
import { Resolver, Args, Mutation, Query } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { accessibleBy } from '@casl/prisma';
import { Prisma } from '@prisma/client';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import {
PrismaSelector,
PrismaSelect,
} from 'src/decorators/prisma-select.decorator';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
CreateActivityAbilityHandler,
DeleteActivityAbilityHandler,
ReadActivityAbilityHandler,
UpdateActivityAbilityHandler,
} from 'src/ability/handlers/activity.ability-handler';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output';
import { Activity } from 'src/core/@generated/activity/activity.model';
import { ActivityService } from 'src/core/activity/services/activity.service';
import { CreateOneActivityArgs } from 'src/core/@generated/activity/create-one-activity.args';
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { UpdateOneActivityArgs } from 'src/core/@generated/activity/update-one-activity.args';
import { FindManyActivityArgs } from 'src/core/@generated/activity/find-many-activity.args';
import { DeleteManyActivityArgs } from 'src/core/@generated/activity/delete-many-activity.args';
@UseGuards(JwtAuthGuard)
@Resolver(() => Activity)
export class ActivityResolver {
constructor(private readonly activityService: ActivityService) {}
@Mutation(() => Activity, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreateActivityAbilityHandler)
async createOneActivity(
@Args() args: CreateOneActivityArgs,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'Activity' })
prismaSelect: PrismaSelect<'Activity'>,
): Promise<Partial<Activity>> {
const createdActivity = await this.activityService.create({
data: {
...args.data,
...{ workspace: { connect: { id: workspace.id } } },
activityTargets: args.data?.activityTargets?.createMany
? {
createMany: {
data: args.data.activityTargets.createMany.data.map(
(target) => ({ ...target, workspaceId: workspace.id }),
),
},
}
: undefined,
},
select: prismaSelect.value,
} as Prisma.ActivityCreateArgs);
return createdActivity;
}
@Mutation(() => Activity, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(UpdateActivityAbilityHandler)
async updateOneActivity(
@Args() args: UpdateOneActivityArgs,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'Activity' })
prismaSelect: PrismaSelect<'Activity'>,
): Promise<Partial<Activity>> {
// TODO: Do a proper check with recursion testing on args in a more generic place
for (const key in args.data) {
if (args.data[key]) {
for (const subKey in args.data[key]) {
if (JSON.stringify(args.data[key][subKey]) === '{}') {
delete args.data[key][subKey];
}
}
}
if (JSON.stringify(args.data[key]) === '{}') {
delete args.data[key];
}
}
const updatedActivity = await this.activityService.update({
where: args.where,
data: {
...args.data,
activityTargets: args.data?.activityTargets
? {
createMany: args.data.activityTargets.createMany
? {
data: args.data.activityTargets.createMany.data.map(
(target) => ({
...target,
workspaceId: workspace.id,
}),
),
}
: undefined,
deleteMany: args.data.activityTargets.deleteMany ?? undefined,
}
: undefined,
},
select: prismaSelect.value,
} as Prisma.ActivityUpdateArgs);
return updatedActivity;
}
@Query(() => [Activity])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadActivityAbilityHandler)
async findManyActivities(
@Args() args: FindManyActivityArgs,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'Activity' })
prismaSelect: PrismaSelect<'Activity'>,
): Promise<Partial<Activity>[]> {
const result = await this.activityService.findMany({
where: {
...args.where,
AND: [accessibleBy(ability).Activity],
},
orderBy: args.orderBy,
cursor: args.cursor,
take: args.take,
skip: args.skip,
distinct: args.distinct,
select: prismaSelect.value,
});
return result;
}
@Mutation(() => AffectedRows, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(DeleteActivityAbilityHandler)
async deleteManyActivities(
@Args() args: DeleteManyActivityArgs,
): Promise<AffectedRows> {
return this.activityService.deleteMany({
where: args.where,
});
}
}

View File

@ -3,15 +3,15 @@ import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from 'src/database/prisma.service'; import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton'; import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { CommentThreadTargetService } from './comment-thread-target.service'; import { ActivityTargetService } from './activity-target.service';
describe('CommentThreadTargetService', () => { describe('ActivityTargetService', () => {
let service: CommentThreadTargetService; let service: ActivityTargetService;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
CommentThreadTargetService, ActivityTargetService,
{ {
provide: PrismaService, provide: PrismaService,
useValue: prismaMock, useValue: prismaMock,
@ -19,9 +19,7 @@ describe('CommentThreadTargetService', () => {
], ],
}).compile(); }).compile();
service = module.get<CommentThreadTargetService>( service = module.get<ActivityTargetService>(ActivityTargetService);
CommentThreadTargetService,
);
}); });
it('should be defined', () => { it('should be defined', () => {

View File

@ -0,0 +1,40 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class ActivityTargetService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.activityTarget.findFirst;
findFirstOrThrow = this.prismaService.client.activityTarget.findFirstOrThrow;
findUnique = this.prismaService.client.activityTarget.findUnique;
findUniqueOrThrow =
this.prismaService.client.activityTarget.findUniqueOrThrow;
findMany = this.prismaService.client.activityTarget.findMany;
// Create
create = this.prismaService.client.activityTarget.create;
createMany = this.prismaService.client.activityTarget.createMany;
// Update
update = this.prismaService.client.activityTarget.update;
upsert = this.prismaService.client.activityTarget.upsert;
updateMany = this.prismaService.client.activityTarget.updateMany;
// Delete
delete = this.prismaService.client.activityTarget.delete;
deleteMany = this.prismaService.client.activityTarget.deleteMany;
// Aggregate
aggregate = this.prismaService.client.activityTarget.aggregate;
// Count
count = this.prismaService.client.activityTarget.count;
// GroupBy
groupBy = this.prismaService.client.activityTarget.groupBy;
}

View File

@ -3,15 +3,15 @@ import { Test, TestingModule } from '@nestjs/testing';
import { PrismaService } from 'src/database/prisma.service'; import { PrismaService } from 'src/database/prisma.service';
import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton'; import { prismaMock } from 'src/database/client-mock/jest-prisma-singleton';
import { CommentThreadService } from './comment-thread.service'; import { ActivityService } from './activity.service';
describe('CommentThreadService', () => { describe('ActivityService', () => {
let service: CommentThreadService; let service: ActivityService;
beforeEach(async () => { beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({ const module: TestingModule = await Test.createTestingModule({
providers: [ providers: [
CommentThreadService, ActivityService,
{ {
provide: PrismaService, provide: PrismaService,
useValue: prismaMock, useValue: prismaMock,
@ -19,7 +19,7 @@ describe('CommentThreadService', () => {
], ],
}).compile(); }).compile();
service = module.get<CommentThreadService>(CommentThreadService); service = module.get<ActivityService>(ActivityService);
}); });
it('should be defined', () => { it('should be defined', () => {

View File

@ -0,0 +1,39 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class ActivityService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.activity.findFirst;
findFirstOrThrow = this.prismaService.client.activity.findFirstOrThrow;
findUnique = this.prismaService.client.activity.findUnique;
findUniqueOrThrow = this.prismaService.client.activity.findUniqueOrThrow;
findMany = this.prismaService.client.activity.findMany;
// Create
create = this.prismaService.client.activity.create;
createMany = this.prismaService.client.activity.createMany;
// Update
update = this.prismaService.client.activity.update;
upsert = this.prismaService.client.activity.upsert;
updateMany = this.prismaService.client.activity.updateMany;
// Delete
delete = this.prismaService.client.activity.delete;
deleteMany = this.prismaService.client.activity.deleteMany;
// Aggregate
aggregate = this.prismaService.client.activity.aggregate;
// Count
count = this.prismaService.client.activity.count;
// GroupBy
groupBy = this.prismaService.client.activity.groupBy;
}

View File

@ -1,19 +1,10 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CommentService } from './services/comment.service'; import { CommentService } from './comment.service';
import { CommentResolver } from './resolvers/comment.resolver'; import { CommentResolver } from './comment.resolver';
import { CommentThreadTargetService } from './services/comment-thread-target.service';
import { CommentThreadResolver } from './resolvers/comment-thread.resolver';
import { CommentThreadService } from './services/comment-thread.service';
@Module({ @Module({
providers: [ providers: [CommentService, CommentResolver],
CommentService, exports: [CommentService],
CommentThreadService,
CommentThreadTargetService,
CommentResolver,
CommentThreadResolver,
],
exports: [CommentService, CommentThreadService, CommentThreadTargetService],
}) })
export class CommentModule {} export class CommentModule {}

View File

@ -1,6 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { CommentService } from 'src/core/comment/services/comment.service'; import { CommentService } from 'src/core/comment/comment.service';
import { AbilityFactory } from 'src/ability/ability.factory'; import { AbilityFactory } from 'src/ability/ability.factory';
import { CommentResolver } from './comment.resolver'; import { CommentResolver } from './comment.resolver';

View File

@ -8,7 +8,7 @@ import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator'; import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { CreateOneCommentArgs } from 'src/core/@generated/comment/create-one-comment.args'; import { CreateOneCommentArgs } from 'src/core/@generated/comment/create-one-comment.args';
import { Comment } from 'src/core/@generated/comment/comment.model'; import { Comment } from 'src/core/@generated/comment/comment.model';
import { CommentService } from 'src/core/comment/services/comment.service'; import { CommentService } from 'src/core/comment/comment.service';
import { import {
PrismaSelector, PrismaSelector,
PrismaSelect, PrismaSelect,

View File

@ -1,150 +0,0 @@
import { Resolver, Args, Mutation, Query } from '@nestjs/graphql';
import { UseGuards } from '@nestjs/common';
import { accessibleBy } from '@casl/prisma';
import { Prisma } from '@prisma/client';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { CommentThread } from 'src/core/@generated/comment-thread/comment-thread.model';
import { CreateOneCommentThreadArgs } from 'src/core/@generated/comment-thread/create-one-comment-thread.args';
import { FindManyCommentThreadArgs } from 'src/core/@generated/comment-thread/find-many-comment-thread.args';
import { CommentThreadService } from 'src/core/comment/services/comment-thread.service';
import { UpdateOneCommentThreadArgs } from 'src/core/@generated/comment-thread/update-one-comment-thread.args';
import {
PrismaSelector,
PrismaSelect,
} from 'src/decorators/prisma-select.decorator';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
CreateCommentThreadAbilityHandler,
DeleteCommentThreadAbilityHandler,
ReadCommentThreadAbilityHandler,
UpdateCommentThreadAbilityHandler,
} from 'src/ability/handlers/comment-thread.ability-handler';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output';
import { DeleteManyCommentThreadArgs } from 'src/core/@generated/comment-thread/delete-many-comment-thread.args';
@UseGuards(JwtAuthGuard)
@Resolver(() => CommentThread)
export class CommentThreadResolver {
constructor(private readonly commentThreadService: CommentThreadService) {}
@Mutation(() => CommentThread, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(CreateCommentThreadAbilityHandler)
async createOneCommentThread(
@Args() args: CreateOneCommentThreadArgs,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'CommentThread' })
prismaSelect: PrismaSelect<'CommentThread'>,
): Promise<Partial<CommentThread>> {
const createdCommentThread = await this.commentThreadService.create({
data: {
...args.data,
...{ workspace: { connect: { id: workspace.id } } },
commentThreadTargets: args.data?.commentThreadTargets?.createMany
? {
createMany: {
data: args.data.commentThreadTargets.createMany.data.map(
(target) => ({ ...target, workspaceId: workspace.id }),
),
},
}
: undefined,
},
select: prismaSelect.value,
} as Prisma.CommentThreadCreateArgs);
return createdCommentThread;
}
@Mutation(() => CommentThread, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(UpdateCommentThreadAbilityHandler)
async updateOneCommentThread(
@Args() args: UpdateOneCommentThreadArgs,
@AuthWorkspace() workspace: Workspace,
@PrismaSelector({ modelName: 'CommentThread' })
prismaSelect: PrismaSelect<'CommentThread'>,
): Promise<Partial<CommentThread>> {
// TODO: Do a proper check with recursion testing on args in a more generic place
for (const key in args.data) {
if (args.data[key]) {
for (const subKey in args.data[key]) {
if (JSON.stringify(args.data[key][subKey]) === '{}') {
delete args.data[key][subKey];
}
}
}
if (JSON.stringify(args.data[key]) === '{}') {
delete args.data[key];
}
}
const updatedCommentThread = await this.commentThreadService.update({
where: args.where,
data: {
...args.data,
commentThreadTargets: args.data?.commentThreadTargets?.createMany
? {
createMany: {
data: args.data.commentThreadTargets.createMany.data.map(
(target) => ({ ...target, workspaceId: workspace.id }),
),
},
}
: undefined,
},
select: prismaSelect.value,
} as Prisma.CommentThreadUpdateArgs);
return updatedCommentThread;
}
@Query(() => [CommentThread])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadCommentThreadAbilityHandler)
async findManyCommentThreads(
@Args() args: FindManyCommentThreadArgs,
@UserAbility() ability: AppAbility,
@PrismaSelector({ modelName: 'CommentThread' })
prismaSelect: PrismaSelect<'CommentThread'>,
): Promise<Partial<CommentThread>[]> {
const result = await this.commentThreadService.findMany({
where: {
...args.where,
AND: [accessibleBy(ability).CommentThread],
},
orderBy: args.orderBy,
cursor: args.cursor,
take: args.take,
skip: args.skip,
distinct: args.distinct,
select: prismaSelect.value,
});
return result;
}
@Mutation(() => AffectedRows, {
nullable: false,
})
@UseGuards(AbilityGuard)
@CheckAbilities(DeleteCommentThreadAbilityHandler)
async deleteManyCommentThreads(
@Args() args: DeleteManyCommentThreadArgs,
): Promise<AffectedRows> {
return this.commentThreadService.deleteMany({
where: args.where,
});
}
}

View File

@ -1,41 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class CommentThreadTargetService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.commentThreadTarget.findFirst;
findFirstOrThrow =
this.prismaService.client.commentThreadTarget.findFirstOrThrow;
findUnique = this.prismaService.client.commentThreadTarget.findUnique;
findUniqueOrThrow =
this.prismaService.client.commentThreadTarget.findUniqueOrThrow;
findMany = this.prismaService.client.commentThreadTarget.findMany;
// Create
create = this.prismaService.client.commentThreadTarget.create;
createMany = this.prismaService.client.commentThreadTarget.createMany;
// Update
update = this.prismaService.client.commentThreadTarget.update;
upsert = this.prismaService.client.commentThreadTarget.upsert;
updateMany = this.prismaService.client.commentThreadTarget.updateMany;
// Delete
delete = this.prismaService.client.commentThreadTarget.delete;
deleteMany = this.prismaService.client.commentThreadTarget.deleteMany;
// Aggregate
aggregate = this.prismaService.client.commentThreadTarget.aggregate;
// Count
count = this.prismaService.client.commentThreadTarget.count;
// GroupBy
groupBy = this.prismaService.client.commentThreadTarget.groupBy;
}

View File

@ -1,39 +0,0 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/database/prisma.service';
@Injectable()
export class CommentThreadService {
constructor(private readonly prismaService: PrismaService) {}
// Find
findFirst = this.prismaService.client.commentThread.findFirst;
findFirstOrThrow = this.prismaService.client.commentThread.findFirstOrThrow;
findUnique = this.prismaService.client.commentThread.findUnique;
findUniqueOrThrow = this.prismaService.client.commentThread.findUniqueOrThrow;
findMany = this.prismaService.client.commentThread.findMany;
// Create
create = this.prismaService.client.commentThread.create;
createMany = this.prismaService.client.commentThread.createMany;
// Update
update = this.prismaService.client.commentThread.update;
upsert = this.prismaService.client.commentThread.upsert;
updateMany = this.prismaService.client.commentThread.updateMany;
// Delete
delete = this.prismaService.client.commentThread.delete;
deleteMany = this.prismaService.client.commentThread.deleteMany;
// Aggregate
aggregate = this.prismaService.client.commentThread.aggregate;
// Count
count = this.prismaService.client.commentThread.count;
// GroupBy
groupBy = this.prismaService.client.commentThread.groupBy;
}

View File

@ -1,7 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { CommentThreadService } from 'src/core/comment/services/comment-thread.service'; import { CommentService } from 'src/core/comment/comment.service';
import { CommentService } from 'src/core/comment/services/comment.service'; import { ActivityService } from 'src/core/activity/services/activity.service';
import { CompanyRelationsResolver } from './company-relations.resolver'; import { CompanyRelationsResolver } from './company-relations.resolver';
import { CompanyService } from './company.service'; import { CompanyService } from './company.service';
@ -18,7 +18,7 @@ describe('CompanyRelationsResolver', () => {
useValue: {}, useValue: {},
}, },
{ {
provide: CommentThreadService, provide: ActivityService,
useValue: {}, useValue: {},
}, },
{ {

View File

@ -1,36 +1,36 @@
import { Resolver, ResolveField, Root, Int } from '@nestjs/graphql'; import { Resolver, ResolveField, Root, Int } from '@nestjs/graphql';
import { CommentThread } from 'src/core/@generated/comment-thread/comment-thread.model';
import { Comment } from 'src/core/@generated/comment/comment.model'; import { Comment } from 'src/core/@generated/comment/comment.model';
import { Company } from 'src/core/@generated/company/company.model'; import { Company } from 'src/core/@generated/company/company.model';
import { CommentThreadService } from 'src/core/comment/services/comment-thread.service'; import { CommentService } from 'src/core/comment/comment.service';
import { CommentService } from 'src/core/comment/services/comment.service';
import { import {
PrismaSelect, PrismaSelect,
PrismaSelector, PrismaSelector,
} from 'src/decorators/prisma-select.decorator'; } from 'src/decorators/prisma-select.decorator';
import { ActivityService } from 'src/core/activity/services/activity.service';
import { Activity } from 'src/core/@generated/activity/activity.model';
@Resolver(() => Company) @Resolver(() => Company)
export class CompanyRelationsResolver { export class CompanyRelationsResolver {
constructor( constructor(
private readonly commentThreadService: CommentThreadService, private readonly activityService: ActivityService,
private readonly commentService: CommentService, private readonly commentService: CommentService,
) {} ) {}
@ResolveField(() => [CommentThread], { @ResolveField(() => [Activity], {
nullable: false, nullable: false,
}) })
async commentThreads( async activities(
@Root() company: Company, @Root() company: Company,
@PrismaSelector({ modelName: 'CommentThread' }) @PrismaSelector({ modelName: 'Activity' })
prismaSelect: PrismaSelect<'CommentThread'>, prismaSelect: PrismaSelect<'Activity'>,
): Promise<Partial<CommentThread>[]> { ): Promise<Partial<Activity>[]> {
return this.commentThreadService.findMany({ return this.activityService.findMany({
where: { where: {
commentThreadTargets: { activityTargets: {
some: { some: {
commentableId: company.id,
commentableType: 'Company', commentableType: 'Company',
commentableId: company.id,
}, },
}, },
}, },
@ -48,11 +48,11 @@ export class CompanyRelationsResolver {
): Promise<Partial<Comment>[]> { ): Promise<Partial<Comment>[]> {
return this.commentService.findMany({ return this.commentService.findMany({
where: { where: {
commentThread: { activity: {
commentThreadTargets: { activityTargets: {
some: { some: {
commentableId: company.id,
commentableType: 'Company', commentableType: 'Company',
commentableId: company.id,
}, },
}, },
}, },
@ -64,13 +64,13 @@ export class CompanyRelationsResolver {
@ResolveField(() => Int, { @ResolveField(() => Int, {
nullable: false, nullable: false,
}) })
async _commentThreadCount(@Root() company: Company): Promise<number> { async _activityCount(@Root() company: Company): Promise<number> {
return this.commentThreadService.count({ return this.activityService.count({
where: { where: {
commentThreadTargets: { activityTargets: {
some: { some: {
commentableId: company.id,
commentableType: 'Company', commentableType: 'Company',
commentableId: company.id,
}, },
}, },
}, },

View File

@ -1,13 +1,14 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CommentModule } from 'src/core/comment/comment.module'; import { CommentModule } from 'src/core/comment/comment.module';
import { ActivityModule } from 'src/core/activity/activity.module';
import { CompanyService } from './company.service'; import { CompanyService } from './company.service';
import { CompanyResolver } from './company.resolver'; import { CompanyResolver } from './company.resolver';
import { CompanyRelationsResolver } from './company-relations.resolver'; import { CompanyRelationsResolver } from './company-relations.resolver';
@Module({ @Module({
imports: [CommentModule], imports: [CommentModule, ActivityModule],
providers: [CompanyService, CompanyResolver, CompanyRelationsResolver], providers: [CompanyService, CompanyResolver, CompanyRelationsResolver],
exports: [CompanyService], exports: [CompanyService],
}) })

View File

@ -11,6 +11,7 @@ import { AnalyticsModule } from './analytics/analytics.module';
import { FileModule } from './file/file.module'; import { FileModule } from './file/file.module';
import { ClientConfigModule } from './client-config/client-config.module'; import { ClientConfigModule } from './client-config/client-config.module';
import { AttachmentModule } from './attachment/attachment.module'; import { AttachmentModule } from './attachment/attachment.module';
import { ActivityModule } from './activity/activity.module';
import { ViewModule } from './view/view.module'; import { ViewModule } from './view/view.module';
@Module({ @Module({
@ -26,6 +27,7 @@ import { ViewModule } from './view/view.module';
FileModule, FileModule,
ClientConfigModule, ClientConfigModule,
AttachmentModule, AttachmentModule,
ActivityModule,
ViewModule, ViewModule,
], ],
exports: [ exports: [

View File

@ -1,7 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { CommentThreadService } from 'src/core/comment/services/comment-thread.service'; import { CommentService } from 'src/core/comment/comment.service';
import { CommentService } from 'src/core/comment/services/comment.service'; import { ActivityService } from 'src/core/activity/services/activity.service';
import { PersonRelationsResolver } from './person-relations.resolver'; import { PersonRelationsResolver } from './person-relations.resolver';
import { PersonService } from './person.service'; import { PersonService } from './person.service';
@ -18,7 +18,7 @@ describe('PersonRelationsResolver', () => {
useValue: {}, useValue: {},
}, },
{ {
provide: CommentThreadService, provide: ActivityService,
useValue: {}, useValue: {},
}, },
{ {

View File

@ -1,36 +1,36 @@
import { Resolver, Root, ResolveField, Int } from '@nestjs/graphql'; import { Resolver, Root, ResolveField, Int } from '@nestjs/graphql';
import { CommentThread } from 'src/core/@generated/comment-thread/comment-thread.model';
import { Comment } from 'src/core/@generated/comment/comment.model'; import { Comment } from 'src/core/@generated/comment/comment.model';
import { Person } from 'src/core/@generated/person/person.model'; import { Person } from 'src/core/@generated/person/person.model';
import { CommentThreadService } from 'src/core/comment/services/comment-thread.service'; import { CommentService } from 'src/core/comment/comment.service';
import { CommentService } from 'src/core/comment/services/comment.service';
import { import {
PrismaSelect, PrismaSelect,
PrismaSelector, PrismaSelector,
} from 'src/decorators/prisma-select.decorator'; } from 'src/decorators/prisma-select.decorator';
import { Activity } from 'src/core/@generated/activity/activity.model';
import { ActivityService } from 'src/core/activity/services/activity.service';
@Resolver(() => Person) @Resolver(() => Person)
export class PersonRelationsResolver { export class PersonRelationsResolver {
constructor( constructor(
private readonly commentThreadService: CommentThreadService, private readonly activityService: ActivityService,
private readonly commentService: CommentService, private readonly commentService: CommentService,
) {} ) {}
@ResolveField(() => [CommentThread], { @ResolveField(() => [Activity], {
nullable: false, nullable: false,
}) })
async commentThreads( async activities(
@Root() person: Person, @Root() person: Person,
@PrismaSelector({ modelName: 'CommentThread' }) @PrismaSelector({ modelName: 'Activity' })
prismaSelect: PrismaSelect<'CommentThread'>, prismaSelect: PrismaSelect<'Activity'>,
): Promise<Partial<CommentThread>[]> { ): Promise<Partial<Activity>[]> {
return await this.commentThreadService.findMany({ return await this.activityService.findMany({
where: { where: {
commentThreadTargets: { activityTargets: {
some: { some: {
commentableId: person.id,
commentableType: 'Person', commentableType: 'Person',
commentableId: person.id,
}, },
}, },
}, },
@ -48,11 +48,11 @@ export class PersonRelationsResolver {
): Promise<Partial<Comment>[]> { ): Promise<Partial<Comment>[]> {
return this.commentService.findMany({ return this.commentService.findMany({
where: { where: {
commentThread: { activity: {
commentThreadTargets: { activityTargets: {
some: { some: {
commentableId: person.id,
commentableType: 'Person', commentableType: 'Person',
commentableId: person.id,
}, },
}, },
}, },
@ -64,13 +64,13 @@ export class PersonRelationsResolver {
@ResolveField(() => Int, { @ResolveField(() => Int, {
nullable: false, nullable: false,
}) })
async _commentThreadCount(@Root() person: Person): Promise<number> { async _activityCount(@Root() person: Person): Promise<number> {
return this.commentThreadService.count({ return this.activityService.count({
where: { where: {
commentThreadTargets: { activityTargets: {
some: { some: {
commentableId: person.id,
commentableType: 'Person', commentableType: 'Person',
commentableId: person.id,
}, },
}, },
}, },

View File

@ -1,13 +1,14 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { CommentModule } from 'src/core/comment/comment.module'; import { CommentModule } from 'src/core/comment/comment.module';
import { ActivityModule } from 'src/core/activity/activity.module';
import { PersonService } from './person.service'; import { PersonService } from './person.service';
import { PersonResolver } from './person.resolver'; import { PersonResolver } from './person.resolver';
import { PersonRelationsResolver } from './person-relations.resolver'; import { PersonRelationsResolver } from './person-relations.resolver';
@Module({ @Module({
imports: [CommentModule, CommentModule], imports: [CommentModule, ActivityModule],
providers: [PersonService, PersonResolver, PersonRelationsResolver], providers: [PersonService, PersonResolver, PersonRelationsResolver],
exports: [PersonService], exports: [PersonService],
}) })

View File

@ -18,7 +18,6 @@ import { UpdateOnePersonArgs } from 'src/core/@generated/person/update-one-perso
import { CreateOnePersonArgs } from 'src/core/@generated/person/create-one-person.args'; import { CreateOnePersonArgs } from 'src/core/@generated/person/create-one-person.args';
import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output'; import { AffectedRows } from 'src/core/@generated/prisma/affected-rows.output';
import { DeleteManyPersonArgs } from 'src/core/@generated/person/delete-many-person.args'; import { DeleteManyPersonArgs } from 'src/core/@generated/person/delete-many-person.args';
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator'; import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { import {
PrismaSelect, PrismaSelect,
@ -34,6 +33,7 @@ import {
} from 'src/ability/handlers/person.ability-handler'; } from 'src/ability/handlers/person.ability-handler';
import { UserAbility } from 'src/decorators/user-ability.decorator'; import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory'; import { AppAbility } from 'src/ability/ability.factory';
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { PersonService } from './person.service'; import { PersonService } from './person.service';

View File

@ -109,11 +109,11 @@ export class WorkspaceService {
refreshToken, refreshToken,
attachment, attachment,
comment, comment,
commentThreadTarget, activityTarget,
commentThread, activity,
} = this.prismaService.client; } = this.prismaService.client;
const commentThreads = await commentThread.findMany({ const activitys = await activity.findMany({
where: { authorId: userId }, where: { authorId: userId },
}); });
@ -142,12 +142,12 @@ export class WorkspaceService {
comment.deleteMany({ comment.deleteMany({
where, where,
}), }),
...commentThreads.map(({ id: commentThreadId }) => ...activitys.map(({ id: activityId }) =>
commentThreadTarget.deleteMany({ activityTarget.deleteMany({
where: { commentThreadId }, where: { activityId },
}), }),
), ),
commentThread.deleteMany({ activity.deleteMany({
where, where,
}), }),
refreshToken.deleteMany({ refreshToken.deleteMany({

View File

@ -1,4 +1,3 @@
-- Create the new tables first, without any foreign key constraints
-- Activities Table -- Activities Table
CREATE TABLE "activities" ( CREATE TABLE "activities" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,

View File

@ -0,0 +1,69 @@
/*
Warnings:
- You are about to drop the `comment_thread_targets` table. If the table is not empty, all the data it contains will be lost.
- You are about to drop the `comment_threads` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "attachments" DROP CONSTRAINT "attachments_activityId_fkey";
-- DropForeignKey
ALTER TABLE "comment_thread_targets" DROP CONSTRAINT "comment_thread_targets_commentThreadId_fkey";
-- DropForeignKey
ALTER TABLE "comment_thread_targets" DROP CONSTRAINT "comment_thread_targets_workspaceId_fkey";
-- DropForeignKey
ALTER TABLE "comment_threads" DROP CONSTRAINT "comment_threads_assigneeId_fkey";
-- DropForeignKey
ALTER TABLE "comment_threads" DROP CONSTRAINT "comment_threads_authorId_fkey";
-- DropForeignKey
ALTER TABLE "comment_threads" DROP CONSTRAINT "comment_threads_workspaceId_fkey";
-- DropForeignKey
ALTER TABLE "comments" DROP CONSTRAINT "comments_commentThreadId_fkey";
-- AlterTable
ALTER TABLE "activity_targets" ADD COLUMN "commentableId" TEXT,
ADD COLUMN "commentableType" "CommentableType",
ALTER COLUMN "personId" DROP NOT NULL,
ALTER COLUMN "companyId" DROP NOT NULL;
-- AlterTable
ALTER TABLE "comments" ADD COLUMN "activityId" TEXT;
-- DropTable
DROP TABLE "comment_thread_targets";
-- DropTable
DROP TABLE "comment_threads";
-- AddForeignKey
ALTER TABLE "activities" ADD CONSTRAINT "activities_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "activities" ADD CONSTRAINT "activities_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "activities" ADD CONSTRAINT "activities_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "comments" ADD CONSTRAINT "comments_activityId_fkey" FOREIGN KEY ("activityId") REFERENCES "activities"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "activity_targets" ADD CONSTRAINT "activity_targets_activityId_fkey" FOREIGN KEY ("activityId") REFERENCES "activities"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "activity_targets" ADD CONSTRAINT "activity_targets_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "activity_targets" ADD CONSTRAINT "activity_targets_personId_fkey" FOREIGN KEY ("personId") REFERENCES "people"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "activity_targets" ADD CONSTRAINT "activity_targets_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "companies"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "attachments" ADD CONSTRAINT "attachments_activityId_fkey" FOREIGN KEY ("activityId") REFERENCES "activities"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -20,20 +20,20 @@ generator nestgraphql {
fields_Validator_from = "class-validator" fields_Validator_from = "class-validator"
// All relations, only allow connect // All relations, only allow connect
decorate_all_type = "!(CommentThreadTarget*Input|UserSettingsUpdateOneRequiredWithoutUserNestedInput)" decorate_all_type = "!(ActivityTarget*Input|UserSettingsUpdateOneRequiredWithoutUserNestedInput)"
decorate_all_field = "*(create|connectOrCreate|update|upsert|delete|createMany|updateMany|deleteMany)" decorate_all_field = "*(create|connectOrCreate|update|upsert|delete|createMany|updateMany|deleteMany)"
decorate_all_name = "HideField" decorate_all_name = "HideField"
decorate_all_from = "@nestjs/graphql" decorate_all_from = "@nestjs/graphql"
decorate_all_arguments = "[]" decorate_all_arguments = "[]"
// CommentThread: Only Allow targets createOrConnect / createMany // Activity: Only Allow targets createOrConnect / createMany
decorate_commentThreadTargets_type = "*CommentThreadTarget*Input" decorate_activityTargets_type = "*ActivityTarget*Input"
decorate_commentThreadTargets_field = "*(update|upsert|updateMany)" decorate_activityTargets_field = "*(update|upsert|updateMany)"
decorate_commentThreadTargets_name = "HideField" decorate_activityTargets_name = "HideField"
decorate_commentThreadTargets_from = "@nestjs/graphql" decorate_activityTargets_from = "@nestjs/graphql"
decorate_commentThreadTargets_arguments = "[]" decorate_activityTargets_arguments = "[]"
// CommentThread: Only Allow targets createOrConnect / createMany // User Settings: Only Allow targets createOrConnect / createMany
decorate_userSettings_type = "*UserSettingsUpdateOneRequiredWithoutUserNestedInput" decorate_userSettings_type = "*UserSettingsUpdateOneRequiredWithoutUserNestedInput"
decorate_userSettings_field = "!(update)" decorate_userSettings_field = "!(update)"
decorate_userSettings_name = "HideField" decorate_userSettings_name = "HideField"
@ -106,10 +106,10 @@ model User {
refreshTokens RefreshToken[] refreshTokens RefreshToken[]
comments Comment[] comments Comment[]
authoredCommentThreads CommentThread[] @relation(name: "authoredCommentThreads") authoredActivities Activity[] @relation(name: "authoredActivities")
assignedCommentThreads CommentThread[] @relation(name: "assignedCommentThreads") assignedActivities Activity[] @relation(name: "assignedActivities")
settings UserSettings @relation(fields: [settingsId], references: [id]) settings UserSettings @relation(fields: [settingsId], references: [id])
settingsId String @unique settingsId String @unique
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
deletedAt DateTime? deletedAt DateTime?
@ -164,20 +164,20 @@ model Workspace {
workspaceMember WorkspaceMember[] workspaceMember WorkspaceMember[]
companies Company[] companies Company[]
people Person[] people Person[]
commentThreads CommentThread[] activities Activity[]
comments Comment[] comments Comment[]
pipelines Pipeline[] pipelines Pipeline[]
pipelineStages PipelineStage[] pipelineStages PipelineStage[]
pipelineProgresses PipelineProgress[] pipelineProgresses PipelineProgress[]
activityTargets ActivityTarget[]
viewFields ViewField[] viewFields ViewField[]
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
deletedAt DateTime? deletedAt DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
Attachment Attachment[] Attachment Attachment[]
CommentThreadTarget CommentThreadTarget[]
@@map("workspaces") @@map("workspaces")
} }
@ -234,8 +234,9 @@ model Company {
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
deletedAt DateTime? deletedAt DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
ActivityTarget ActivityTarget[]
@@map("companies") @@map("companies")
} }
@ -277,8 +278,9 @@ model Person {
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
deletedAt DateTime? deletedAt DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
ActivityTarget ActivityTarget[]
@@map("people") @@map("people")
} }
@ -311,30 +313,28 @@ enum ActivityType {
Task Task
} }
model CommentThread { model Activity {
/// @Validator.IsString() /// @Validator.IsString()
/// @Validator.IsOptional() /// @Validator.IsOptional()
id String @id @default(uuid()) id String @id @default(uuid())
body String?
commentThreadTargets CommentThreadTarget[] title String?
comments Comment[] type ActivityType @default(Note)
/// @TypeGraphQL.omit(input: true, output: true)
workspace Workspace @relation(fields: [workspaceId], references: [id])
/// @TypeGraphQL.omit(input: true, output: true)
workspaceId String
authorId String
author User @relation(fields: [authorId], references: [id], name: "authoredCommentThreads")
body String?
title String?
type ActivityType @default(Note)
reminderAt DateTime? reminderAt DateTime?
dueAt DateTime? dueAt DateTime?
completedAt DateTime? completedAt DateTime?
assignee User? @relation(fields: [assigneeId], references: [id], name: "assignedCommentThreads")
assigneeId String? activityTargets ActivityTarget[]
comments Comment[]
attachments Attachment[]
author User @relation(fields: [authorId], references: [id], name: "authoredActivities")
authorId String
assignee User? @relation(fields: [assigneeId], references: [id], name: "assignedActivities")
assigneeId String?
/// @TypeGraphQL.omit(input: true, output: true)
workspace Workspace @relation(fields: [workspaceId], references: [id])
/// @TypeGraphQL.omit(input: true, output: true)
workspaceId String
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
deletedAt DateTime? deletedAt DateTime?
@ -342,9 +342,12 @@ model CommentThread {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
attachments Attachment[] @@map("activities")
}
@@map("comment_threads") enum CommentableType {
Person
Company
} }
model Comment { model Comment {
@ -354,12 +357,13 @@ model Comment {
/// @Validator.IsString() /// @Validator.IsString()
body String body String
author User @relation(fields: [authorId], references: [id]) author User @relation(fields: [authorId], references: [id])
authorId String authorId String
commentThread CommentThread @relation(fields: [commentThreadId], references: [id], onDelete: Cascade) activity Activity? @relation(fields: [activityId], references: [id], onDelete: Cascade)
activityId String?
commentThreadId String commentThreadId String
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
workspace Workspace @relation(fields: [workspaceId], references: [id]) workspace Workspace @relation(fields: [workspaceId], references: [id])
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
workspaceId String workspaceId String
@ -372,24 +376,25 @@ model Comment {
@@map("comments") @@map("comments")
} }
enum CommentableType { model ActivityTarget {
Person
Company
}
model CommentThreadTarget {
/// @Validator.IsString() /// @Validator.IsString()
/// @Validator.IsOptional() /// @Validator.IsOptional()
id String @id @default(uuid()) id String @id @default(uuid())
commentThread CommentThread @relation(fields: [commentThreadId], references: [id], onDelete: Cascade) activity Activity @relation(fields: [activityId], references: [id], onDelete: Cascade)
commentThreadId String activityId String
/// @TypeGraphQL.omit(input: true, output: false) commentableType CommentableType?
workspace Workspace? @relation(fields: [workspaceId], references: [id]) commentableId String?
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
workspaceId String? workspace Workspace @relation(fields: [workspaceId], references: [id])
commentableType CommentableType /// @TypeGraphQL.omit(input: true, output: true)
commentableId String workspaceId String
personId String?
person Person? @relation(fields: [personId], references: [id])
companyId String?
company Company? @relation(fields: [companyId], references: [id])
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
deletedAt DateTime? deletedAt DateTime?
@ -397,7 +402,7 @@ model CommentThreadTarget {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@map("comment_thread_targets") @@map("activity_targets")
} }
model Pipeline { model Pipeline {
@ -515,17 +520,16 @@ model Attachment {
type AttachmentType type AttachmentType
name String name String
authorId String
author User @relation(fields: [authorId], references: [id], name: "authoredAttachments")
activityId String
activity CommentThread @relation(fields: [activityId], references: [id])
/// @TypeGraphQL.omit(input: true, output: false)
workspace Workspace @relation(fields: [workspaceId], references: [id])
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
workspaceId String workspaceId String
author User @relation(fields: [authorId], references: [id], name: "authoredAttachments")
authorId String
activity Activity @relation(fields: [activityId], references: [id])
activityId String
/// @TypeGraphQL.omit(input: true, output: false)
workspace Workspace @relation(fields: [workspaceId], references: [id])
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
deletedAt DateTime? deletedAt DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())

View File

@ -1,6 +1,6 @@
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from '@prisma/client';
export const seedComments = async (prisma: PrismaClient) => { export const seedComments = async (prisma: PrismaClient) => {
await prisma.commentThread.upsert({ await prisma.activity.upsert({
where: { id: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb400' }, where: { id: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb400' },
update: {}, update: {},
create: { create: {
@ -13,15 +13,15 @@ export const seedComments = async (prisma: PrismaClient) => {
}, },
}); });
await prisma.commentThreadTarget.upsert({ await prisma.activityTarget.upsert({
where: { id: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb600' }, where: { id: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb600' },
update: {}, update: {},
create: { create: {
id: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb600', id: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb600',
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
commentableType: 'Company', commentableType: 'Company',
commentableId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfa408', commentableId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfa408',
commentThreadId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb400', activityId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb400',
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
}, },
}); });
@ -32,6 +32,7 @@ export const seedComments = async (prisma: PrismaClient) => {
id: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb200', id: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb200',
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
body: 'Hi Félix ! How do you like your Twenty workspace?', body: 'Hi Félix ! How do you like your Twenty workspace?',
activityId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb400',
commentThreadId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb400', commentThreadId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb400',
authorId: 'twenty-ge256b39-3ec3-4fe3-8997-b76aa0bfa408', authorId: 'twenty-ge256b39-3ec3-4fe3-8997-b76aa0bfa408',
}, },
@ -44,12 +45,13 @@ export const seedComments = async (prisma: PrismaClient) => {
id: 'twenty-fe256b40-3ec3-4fe3-8997-b76aa0bfb200', id: 'twenty-fe256b40-3ec3-4fe3-8997-b76aa0bfb200',
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
body: 'I love it!', body: 'I love it!',
activityId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb400',
commentThreadId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb400', commentThreadId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb400',
authorId: 'twenty-gk256b39-3ec3-4fe3-8997-b76aa0bfa408', authorId: 'twenty-gk256b39-3ec3-4fe3-8997-b76aa0bfa408',
}, },
}); });
await prisma.commentThread.upsert({ await prisma.activity.upsert({
where: { id: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfc408' }, where: { id: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfc408' },
update: {}, update: {},
create: { create: {
@ -64,15 +66,15 @@ export const seedComments = async (prisma: PrismaClient) => {
}, },
}); });
await prisma.commentThreadTarget.upsert({ await prisma.activityTarget.upsert({
where: { id: 'twenty-fe256b39-3ec3-4fe3-8997-a76aa0bfb600' }, where: { id: 'twenty-fe256b39-3ec3-4fe3-8997-a76aa0bfb600' },
update: {}, update: {},
create: { create: {
id: 'twenty-fe256b39-3ec3-4fe3-8997-a76aa0bfb600', id: 'twenty-fe256b39-3ec3-4fe3-8997-a76aa0bfb600',
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
commentableType: 'Person', commentableType: 'Person',
commentableId: 'twenty-755035db-623d-41fe-92e7-dd45b7c568e1', commentableId: 'twenty-755035db-623d-41fe-92e7-dd45b7c568e1',
commentThreadId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfc408', activityId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfc408',
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
}, },
}); });
@ -83,12 +85,13 @@ export const seedComments = async (prisma: PrismaClient) => {
id: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb100', id: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfb100',
workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419', workspaceId: 'twenty-7ed9d212-1c25-4d02-bf25-6aeccf7ea419',
body: 'I really like this comment thread feature!', body: 'I really like this comment thread feature!',
activityId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfc408',
commentThreadId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfc408', commentThreadId: 'twenty-fe256b39-3ec3-4fe3-8997-b76aa0bfc408',
authorId: 'twenty-ge256b39-3ec3-4fe3-8997-b76aa0bfa408', authorId: 'twenty-ge256b39-3ec3-4fe3-8997-b76aa0bfa408',
}, },
}); });
await prisma.commentThread.upsert({ await prisma.activity.upsert({
where: { id: 'twenty-dev-fe256b39-3ec3-4fe3-8997-b76aaabfb408' }, where: { id: 'twenty-dev-fe256b39-3ec3-4fe3-8997-b76aaabfb408' },
update: {}, update: {},
create: { create: {
@ -100,15 +103,15 @@ export const seedComments = async (prisma: PrismaClient) => {
}, },
}); });
await prisma.commentThreadTarget.upsert({ await prisma.activityTarget.upsert({
where: { id: 'twenty-dev-fe256b39-3ec3-4fe3-8997-a76aa0bfba00' }, where: { id: 'twenty-dev-fe256b39-3ec3-4fe3-8997-a76aa0bfba00' },
update: {}, update: {},
create: { create: {
id: 'twenty-dev-fe256b39-3ec3-4fe3-8997-a76aa0bfba00', id: 'twenty-dev-fe256b39-3ec3-4fe3-8997-a76aa0bfba00',
workspaceId: 'twenty-dev-7ed9d212-1c25-4d02-bf25-6aeccf7ea420',
commentableType: 'Company', commentableType: 'Company',
commentableId: 'twenty-dev-a674fa6c-1455-4c57-afaf-dd5dc086361e', commentableId: 'twenty-dev-a674fa6c-1455-4c57-afaf-dd5dc086361e',
commentThreadId: 'twenty-dev-fe256b39-3ec3-4fe3-8997-b76aaabfb408', activityId: 'twenty-dev-fe256b39-3ec3-4fe3-8997-b76aaabfb408',
workspaceId: 'twenty-dev-7ed9d212-1c25-4d02-bf25-6aeccf7ea420',
}, },
}); });
@ -119,6 +122,7 @@ export const seedComments = async (prisma: PrismaClient) => {
id: 'twenty-dev-fe256b39-3ec3-4fe3-8997-b76aa0bfb000', id: 'twenty-dev-fe256b39-3ec3-4fe3-8997-b76aa0bfb000',
workspaceId: 'twenty-dev-7ed9d212-1c25-4d02-bf25-6aeccf7ea420', workspaceId: 'twenty-dev-7ed9d212-1c25-4d02-bf25-6aeccf7ea420',
body: 'I really like this comment thread feature!', body: 'I really like this comment thread feature!',
activityId: 'twenty-dev-fe256b39-3ec3-4fe3-8997-b76aaabfb408',
commentThreadId: 'twenty-dev-fe256b39-3ec3-4fe3-8997-b76aaabfb408', commentThreadId: 'twenty-dev-fe256b39-3ec3-4fe3-8997-b76aaabfb408',
authorId: 'twenty-dev-gk256b39-3ec3-4fe3-8997-b76aa0boa408', authorId: 'twenty-dev-gk256b39-3ec3-4fe3-8997-b76aa0boa408',
}, },

View File

@ -9,9 +9,9 @@ export type ModelSelectMap = {
Company: Prisma.CompanySelect; Company: Prisma.CompanySelect;
Person: Prisma.PersonSelect; Person: Prisma.PersonSelect;
RefreshToken: Prisma.RefreshTokenSelect; RefreshToken: Prisma.RefreshTokenSelect;
CommentThread: Prisma.CommentThreadSelect; Activity: Prisma.ActivitySelect;
Comment: Prisma.CommentSelect; Comment: Prisma.CommentSelect;
CommentThreadTarget: Prisma.CommentThreadTargetSelect; ActivityTarget: Prisma.ActivityTargetSelect;
Pipeline: Prisma.PipelineSelect; Pipeline: Prisma.PipelineSelect;
PipelineStage: Prisma.PipelineStageSelect; PipelineStage: Prisma.PipelineStageSelect;
PipelineProgress: Prisma.PipelineProgressSelect; PipelineProgress: Prisma.PipelineProgressSelect;