Activity as standard object (#6219)
In this PR I layout the first steps to migrate Activity to a traditional Standard objects Since this is a big transition, I'd rather split it into several deployments / PRs <img width="1512" alt="image" src="https://github.com/user-attachments/assets/012e2bbf-9d1b-4723-aaf6-269ef588b050"> --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: bosiraphael <71827178+bosiraphael@users.noreply.github.com> Co-authored-by: Weiko <corentin@twenty.com> Co-authored-by: Faisal-imtiyaz123 <142205282+Faisal-imtiyaz123@users.noreply.github.com> Co-authored-by: Prateek Jain <prateekj1171998@gmail.com>
This commit is contained in:
@ -6,6 +6,7 @@ module.exports = {
|
||||
'mockServiceWorker.js',
|
||||
'**/generated*/*',
|
||||
'**/generated/standard-metadata-query-result.ts',
|
||||
'**/getObjectMetadataItemsMock.ts',
|
||||
'tsup.config.ts',
|
||||
'build',
|
||||
'coverage',
|
||||
|
||||
@ -84,7 +84,6 @@ import { SettingsBilling } from '~/pages/settings/SettingsBilling';
|
||||
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
|
||||
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
|
||||
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
|
||||
import { Tasks } from '~/pages/tasks/Tasks';
|
||||
import { getPageTitleFromPath } from '~/utils/title-utils';
|
||||
|
||||
const ProvidersThatNeedRouterContext = () => {
|
||||
@ -158,7 +157,6 @@ const createRouter = (
|
||||
element={<PaymentSuccess />}
|
||||
/>
|
||||
<Route path={indexAppPath.getIndexAppPath()} element={<></>} />
|
||||
<Route path={AppPath.TasksPage} element={<Tasks />} />
|
||||
<Route path={AppPath.Impersonate} element={<ImpersonateEffect />} />
|
||||
<Route path={AppPath.RecordIndexPage} element={<RecordIndexPage />} />
|
||||
<Route path={AppPath.RecordShowPage} element={<RecordShowPage />} />
|
||||
|
||||
@ -9,6 +9,7 @@ import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCapt
|
||||
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
|
||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||
import { CommandType } from '@/command-menu/types/Command';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||
import { AppBasePath } from '@/types/AppBasePath';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
@ -38,7 +39,9 @@ export const PageChangeEffect = () => {
|
||||
|
||||
const { addToCommandMenu, setToInitialCommandMenu } = useCommandMenu();
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
const openCreateActivity = useOpenCreateActivityDrawer({
|
||||
activityObjectNameSingular: CoreObjectNameSingular.Task,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!previousLocation || previousLocation !== location.pathname) {
|
||||
@ -143,8 +146,7 @@ export const PageChangeEffect = () => {
|
||||
label: 'Create Task',
|
||||
type: CommandType.Create,
|
||||
Icon: IconCheckbox,
|
||||
onCommandClick: () =>
|
||||
openCreateActivity({ type: 'Task', targetableObjects: [] }),
|
||||
onCommandClick: () => openCreateActivity({ targetableObjects: [] }),
|
||||
},
|
||||
]);
|
||||
}, [addToCommandMenu, setToInitialCommandMenu, openCreateActivity]);
|
||||
|
||||
@ -361,6 +361,7 @@ export enum FieldMetadataType {
|
||||
Rating = 'RATING',
|
||||
RawJson = 'RAW_JSON',
|
||||
Relation = 'RELATION',
|
||||
RichText = 'RICH_TEXT',
|
||||
Select = 'SELECT',
|
||||
Text = 'TEXT',
|
||||
Uuid = 'UUID'
|
||||
|
||||
@ -250,6 +250,7 @@ export enum FieldMetadataType {
|
||||
Rating = 'RATING',
|
||||
RawJson = 'RAW_JSON',
|
||||
Relation = 'RELATION',
|
||||
RichText = 'RICH_TEXT',
|
||||
Select = 'SELECT',
|
||||
Text = 'TEXT',
|
||||
Uuid = 'UUID'
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Comment as CommentType } from '@/activities/types/Comment';
|
||||
|
||||
import { CommentHeader } from './CommentHeader';
|
||||
|
||||
type CommentProps = {
|
||||
comment: CommentType;
|
||||
actionBar?: React.ReactNode;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
justify-content: flex-start;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledCommentBody = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.md};
|
||||
overflow-wrap: anywhere;
|
||||
|
||||
padding-left: 24px;
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
export const Comment = ({ comment, actionBar }: CommentProps) => (
|
||||
<StyledContainer>
|
||||
<CommentHeader comment={comment} actionBar={actionBar} />
|
||||
<StyledCommentBody>{comment.body}</StyledCommentBody>
|
||||
</StyledContainer>
|
||||
);
|
||||
@ -1,27 +0,0 @@
|
||||
import React from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconComment } from 'twenty-ui';
|
||||
|
||||
const StyledCommentIcon = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const CommentCounter = ({ commentCount }: { commentCount: number }) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<div>
|
||||
{commentCount > 0 && (
|
||||
<StyledCommentIcon>
|
||||
<IconComment size={theme.icon.size.md} />
|
||||
{commentCount}
|
||||
</StyledCommentIcon>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CommentCounter;
|
||||
@ -1,85 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { AppTooltip, Avatar } from 'twenty-ui';
|
||||
|
||||
import { Comment } from '@/activities/types/Comment';
|
||||
import {
|
||||
beautifyExactDateTime,
|
||||
beautifyPastDateRelativeToNow,
|
||||
} from '~/utils/date-utils';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
justify-content: space-between;
|
||||
|
||||
padding: ${({ theme }) => theme.spacing(1)};
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(1)});
|
||||
`;
|
||||
|
||||
const StyledLeftContainer = styled.div`
|
||||
align-items: end;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledName = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
max-width: 160px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledDate = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
margin-left: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
type CommentHeaderProps = {
|
||||
comment: Pick<Comment, 'id' | 'author' | 'createdAt'>;
|
||||
actionBar?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const CommentHeader = ({ comment, actionBar }: CommentHeaderProps) => {
|
||||
const beautifiedCreatedAt = beautifyPastDateRelativeToNow(comment.createdAt);
|
||||
const exactCreatedAt = beautifyExactDateTime(comment.createdAt);
|
||||
const showDate = beautifiedCreatedAt !== '';
|
||||
|
||||
const author = comment.author;
|
||||
const authorName = author?.name?.firstName + ' ' + author?.name?.lastName;
|
||||
const avatarUrl = author?.avatarUrl;
|
||||
const commentId = comment.id;
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledLeftContainer>
|
||||
<Avatar
|
||||
avatarUrl={avatarUrl}
|
||||
size="md"
|
||||
placeholderColorSeed={author?.id}
|
||||
placeholder={authorName}
|
||||
/>
|
||||
<StyledName>{authorName}</StyledName>
|
||||
{showDate && (
|
||||
<>
|
||||
<StyledDate id={`id-${commentId}`}>
|
||||
{beautifiedCreatedAt}
|
||||
</StyledDate>
|
||||
<AppTooltip
|
||||
anchorSelect={`#id-${commentId}`}
|
||||
content={exactCreatedAt}
|
||||
clickable
|
||||
noArrow
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</StyledLeftContainer>
|
||||
<div>{actionBar}</div>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -1,54 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
|
||||
import { ActivityActionBar } from '../../right-drawer/components/ActivityActionBar';
|
||||
import { Comment } from '../Comment';
|
||||
|
||||
import { mockComment, mockCommentWithLongValues } from './mock-comment';
|
||||
|
||||
const CommentSetterEffect = () => {
|
||||
const setViewableRecord = useSetRecoilState(viewableRecordIdState);
|
||||
|
||||
useEffect(() => {
|
||||
setViewableRecord('test-id');
|
||||
}, [setViewableRecord]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const meta: Meta<typeof Comment> = {
|
||||
title: 'Modules/Activity/Comment/Comment',
|
||||
component: Comment,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<>
|
||||
<CommentSetterEffect />
|
||||
<Story />
|
||||
</>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
argTypes: {
|
||||
actionBar: {
|
||||
type: 'boolean',
|
||||
mapping: {
|
||||
true: <ActivityActionBar />,
|
||||
false: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
args: { comment: mockComment },
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Comment>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const WithLongValues: Story = {
|
||||
args: { comment: mockCommentWithLongValues },
|
||||
};
|
||||
@ -1,108 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { ActivityActionBar } from '@/activities/right-drawer/components/ActivityActionBar';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
||||
import { avatarUrl } from '~/testing/mock-data/users';
|
||||
|
||||
import { CommentHeader } from '../CommentHeader';
|
||||
|
||||
import { mockComment, mockCommentWithLongValues } from './mock-comment';
|
||||
|
||||
const CommentHeaderSetterEffect = () => {
|
||||
const setViewableRecord = useSetRecoilState(viewableRecordIdState);
|
||||
|
||||
useEffect(() => {
|
||||
setViewableRecord('test-id');
|
||||
}, [setViewableRecord]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const meta: Meta<typeof CommentHeader> = {
|
||||
title: 'Modules/Activity/Comment/CommentHeader',
|
||||
component: CommentHeader,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<>
|
||||
<CommentHeaderSetterEffect />
|
||||
<Story />
|
||||
</>
|
||||
),
|
||||
ComponentWithRouterDecorator,
|
||||
],
|
||||
argTypes: {
|
||||
actionBar: {
|
||||
type: 'boolean',
|
||||
mapping: {
|
||||
true: <ActivityActionBar />,
|
||||
false: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
args: { comment: mockComment },
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof CommentHeader>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const FewHoursAgo: Story = {
|
||||
args: {
|
||||
comment: {
|
||||
...mockComment,
|
||||
createdAt: DateTime.now().minus({ hours: 2 }).toISO() ?? '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FewDaysAgo: Story = {
|
||||
args: {
|
||||
comment: {
|
||||
...mockComment,
|
||||
createdAt: DateTime.now().minus({ days: 2 }).toISO() ?? '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FewMonthsAgo: Story = {
|
||||
args: {
|
||||
comment: {
|
||||
...mockComment,
|
||||
createdAt: DateTime.now().minus({ months: 2 }).toISO() ?? '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const FewYearsAgo: Story = {
|
||||
args: {
|
||||
comment: {
|
||||
...mockComment,
|
||||
createdAt: DateTime.now().minus({ years: 2 }).toISO() ?? '',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithAvatar: Story = {
|
||||
args: {
|
||||
comment: {
|
||||
...mockComment,
|
||||
author: {
|
||||
...mockComment.author,
|
||||
avatarUrl,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLongUserName: Story = {
|
||||
args: { comment: mockCommentWithLongValues },
|
||||
};
|
||||
|
||||
export const WithActionBar: Story = {
|
||||
args: { actionBar: true },
|
||||
};
|
||||
@ -1,55 +0,0 @@
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { Comment } from '@/activities/types/Comment';
|
||||
|
||||
export const mockComment: Pick<
|
||||
Comment,
|
||||
| 'id'
|
||||
| 'author'
|
||||
| 'createdAt'
|
||||
| 'body'
|
||||
| 'updatedAt'
|
||||
| 'activityId'
|
||||
| '__typename'
|
||||
> = {
|
||||
id: 'fake_comment_1_uuid',
|
||||
body: 'Hello, this is a comment.',
|
||||
author: {
|
||||
id: 'fake_comment_1_author_uuid',
|
||||
name: {
|
||||
firstName: 'Jony' ?? '',
|
||||
lastName: 'Ive' ?? '',
|
||||
},
|
||||
avatarUrl: null,
|
||||
},
|
||||
createdAt: DateTime.fromFormat('2021-03-12', 'yyyy-MM-dd').toISO() ?? '',
|
||||
updatedAt: DateTime.fromFormat('2021-03-13', 'yyyy-MM-dd').toISO() ?? '',
|
||||
activityId: 'fake_activity_1_uuid',
|
||||
__typename: 'Comment',
|
||||
};
|
||||
|
||||
export const mockCommentWithLongValues: Pick<
|
||||
Comment,
|
||||
| 'id'
|
||||
| 'author'
|
||||
| 'createdAt'
|
||||
| 'body'
|
||||
| 'updatedAt'
|
||||
| 'activityId'
|
||||
| '__typename'
|
||||
> = {
|
||||
id: 'fake_comment_2_uuid',
|
||||
body: 'Hello, this is a comment. Hello, this is a comment. Hello, this is a comment. Hello, this is a comment. Hello, this is a comment. Hello, this is a comment.',
|
||||
author: {
|
||||
id: 'fake_comment_1_author_uuid',
|
||||
name: {
|
||||
firstName: 'Jony' ?? '',
|
||||
lastName: 'Ive' ?? '',
|
||||
},
|
||||
avatarUrl: null,
|
||||
},
|
||||
createdAt: DateTime.fromFormat('2021-03-12', 'yyyy-MM-dd').toISO() ?? '',
|
||||
updatedAt: DateTime.fromFormat('2021-03-13', 'yyyy-MM-dd').toISO() ?? '',
|
||||
activityId: 'fake_activity_1_uuid',
|
||||
__typename: 'Comment',
|
||||
};
|
||||
@ -1,129 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { Comment } from '@/activities/comment/Comment';
|
||||
import { Comment as CommentType } from '@/activities/types/Comment';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import {
|
||||
AutosizeTextInput,
|
||||
AutosizeTextInputVariant,
|
||||
} from '@/ui/input/components/AutosizeTextInput';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
const StyledThreadItemListContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
|
||||
justify-content: flex-start;
|
||||
padding: ${({ theme }) => theme.spacing(8)};
|
||||
padding-left: ${({ theme }) => theme.spacing(12)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledCommentActionBar = styled.div`
|
||||
background: ${({ theme }) => theme.background.primary};
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
display: flex;
|
||||
padding: 16px 24px 16px 48px;
|
||||
width: calc(
|
||||
${({ theme }) => (useIsMobile() ? '100%' : theme.rightDrawerWidth)} - 72px
|
||||
);
|
||||
`;
|
||||
|
||||
const StyledThreadCommentTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.xs};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
text-transform: uppercase;
|
||||
`;
|
||||
|
||||
type ActivityCommentsProps = {
|
||||
activityId: string;
|
||||
scrollableContainerRef: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export const ActivityComments = ({
|
||||
activityId,
|
||||
scrollableContainerRef,
|
||||
}: ActivityCommentsProps) => {
|
||||
const { createOneRecord: createOneComment } = useCreateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Comment,
|
||||
});
|
||||
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const { records: comments } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Comment,
|
||||
skip: !isNonEmptyString(activityId),
|
||||
filter: {
|
||||
activityId: {
|
||||
eq: activityId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!currentWorkspaceMember) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const handleSendComment = (commentText: string) => {
|
||||
if (!isNonEmptyString(commentText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
createOneComment?.({
|
||||
id: v4(),
|
||||
authorId: currentWorkspaceMember?.id ?? '',
|
||||
author: currentWorkspaceMember,
|
||||
activityId: activityId,
|
||||
body: commentText,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
const scrollableContainer = scrollableContainerRef.current;
|
||||
|
||||
scrollableContainer?.scrollTo({
|
||||
top: scrollableContainer.scrollHeight,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{comments.length > 0 && (
|
||||
<>
|
||||
<StyledThreadItemListContainer>
|
||||
<StyledThreadCommentTitle>Comments</StyledThreadCommentTitle>
|
||||
{comments?.map((comment) => (
|
||||
<Comment key={comment.id} comment={comment as CommentType} />
|
||||
))}
|
||||
</StyledThreadItemListContainer>
|
||||
</>
|
||||
)}
|
||||
|
||||
<StyledCommentActionBar>
|
||||
{currentWorkspaceMember && (
|
||||
<AutosizeTextInput
|
||||
onValidate={handleSendComment}
|
||||
onFocus={handleFocus}
|
||||
variant={AutosizeTextInputVariant.Button}
|
||||
placeholder={comments.length > 0 ? 'Reply...' : undefined}
|
||||
/>
|
||||
)}
|
||||
</StyledCommentActionBar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,43 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||
|
||||
const StyledCreationDisplay = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type ActivityCreationDateProps = {
|
||||
activityId: string;
|
||||
};
|
||||
|
||||
export const ActivityCreationDate = ({
|
||||
activityId,
|
||||
}: ActivityCreationDateProps) => {
|
||||
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
|
||||
const activity = activityInStore as Activity;
|
||||
|
||||
const beautifiedDate = activity.createdAt
|
||||
? beautifyPastDateRelativeToNow(activity.createdAt)
|
||||
: null;
|
||||
|
||||
const authorName = activity.author?.name
|
||||
? `${activity.author.name.firstName} ${activity.author.name.lastName}`
|
||||
: null;
|
||||
|
||||
if (!activity.createdAt || !authorName) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledCreationDisplay>
|
||||
Created {beautifiedDate} by {authorName}
|
||||
</StyledCreationDisplay>
|
||||
);
|
||||
};
|
||||
@ -1,89 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRef } from 'react';
|
||||
|
||||
import { ActivityBodyEditor } from '@/activities/components/ActivityBodyEditor';
|
||||
import { ActivityBodyEffect } from '@/activities/components/ActivityBodyEffect';
|
||||
import { ActivityComments } from '@/activities/components/ActivityComments';
|
||||
import { ActivityCreationDate } from '@/activities/components/ActivityCreationDate';
|
||||
import { ActivityEditorFields } from '@/activities/components/ActivityEditorFields';
|
||||
import { ActivityTitleEffect } from '@/activities/components/ActivityTitleEffect';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
import { ActivityTitle } from './ActivityTitle';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledUpperPartContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
justify-content: flex-start;
|
||||
`;
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
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: ${({ theme }) => theme.spacing(6)};
|
||||
`;
|
||||
|
||||
type ActivityEditorProps = {
|
||||
activityId: string;
|
||||
showComment?: boolean;
|
||||
fillTitleFromBody?: boolean;
|
||||
};
|
||||
|
||||
export const ActivityEditor = ({
|
||||
activityId,
|
||||
showComment = true,
|
||||
fillTitleFromBody = false,
|
||||
}: ActivityEditorProps) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<StyledContainer ref={containerRef}>
|
||||
<StyledUpperPartContainer>
|
||||
<StyledTopContainer>
|
||||
<ActivityTitleEffect activityId={activityId} />
|
||||
<StyledTitleContainer>
|
||||
<ActivityTitle activityId={activityId} />
|
||||
<ActivityCreationDate activityId={activityId} />
|
||||
</StyledTitleContainer>
|
||||
<ActivityEditorFields activityId={activityId} />
|
||||
</StyledTopContainer>
|
||||
</StyledUpperPartContainer>
|
||||
<ActivityBodyEffect activityId={activityId} />
|
||||
<ActivityBodyEditor
|
||||
activityId={activityId}
|
||||
fillTitleFromBody={fillTitleFromBody}
|
||||
/>
|
||||
{showComment && (
|
||||
<ActivityComments
|
||||
activityId={activityId}
|
||||
scrollableContainerRef={containerRef}
|
||||
/>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -1,102 +0,0 @@
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState';
|
||||
import { activityTitleFamilyState } from '@/activities/states/activityTitleFamilyState';
|
||||
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useDeleteRecordFromCache } from '@/object-record/cache/hooks/useDeleteRecordFromCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
|
||||
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const ActivityEditorEffect = ({
|
||||
activityId,
|
||||
}: {
|
||||
activityId: string;
|
||||
}) => {
|
||||
const { useRegisterClickOutsideListenerCallback } = useClickOutsideListener(
|
||||
RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID,
|
||||
);
|
||||
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
const deleteRecordFromCache = useDeleteRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const upsertActivityCallback = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
() => {
|
||||
const isUpsertingActivityInDB = snapshot
|
||||
.getLoadable(isUpsertingActivityInDBState)
|
||||
.getValue();
|
||||
|
||||
const canCreateActivity = snapshot
|
||||
.getLoadable(canCreateActivityState)
|
||||
.getValue();
|
||||
|
||||
const isActivityInCreateMode = snapshot
|
||||
.getLoadable(isActivityInCreateModeState)
|
||||
.getValue();
|
||||
|
||||
const activityFromStore = snapshot
|
||||
.getLoadable(recordStoreFamilyState(activityId))
|
||||
.getValue();
|
||||
|
||||
const activity = activityFromStore as Activity | null;
|
||||
|
||||
const activityTitle = snapshot
|
||||
.getLoadable(activityTitleFamilyState({ activityId }))
|
||||
.getValue();
|
||||
|
||||
const activityBody = snapshot
|
||||
.getLoadable(activityBodyFamilyState({ activityId }))
|
||||
.getValue();
|
||||
|
||||
if (isUpsertingActivityInDB || !activityFromStore) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isActivityInCreateMode && isDefined(activity)) {
|
||||
if (canCreateActivity) {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
title: activityFromStore.title,
|
||||
body: activityFromStore.body,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
deleteRecordFromCache(activity);
|
||||
}
|
||||
|
||||
set(isActivityInCreateModeState, false);
|
||||
} else if (isDefined(activity)) {
|
||||
if (
|
||||
activity.title !== activityTitle ||
|
||||
activity.body !== activityBody
|
||||
) {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
title: activityTitle,
|
||||
body: activityBody,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[activityId, deleteRecordFromCache, upsertActivity],
|
||||
);
|
||||
|
||||
useRegisterClickOutsideListenerCallback({
|
||||
callbackId: 'activity-editor',
|
||||
callbackFunction: upsertActivityCallback,
|
||||
});
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -1,115 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||
import {
|
||||
RecordUpdateHook,
|
||||
RecordUpdateHookParams,
|
||||
} from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
|
||||
import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { isRightDrawerAnimationCompletedState } from '@/ui/layout/right-drawer/states/isRightDrawerAnimationCompletedState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledPropertyBox = styled(PropertyBox)`
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
export const ActivityEditorFields = ({
|
||||
activityId,
|
||||
}: {
|
||||
activityId: string;
|
||||
}) => {
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
|
||||
const isRightDrawerAnimationCompleted = useRecoilValue(
|
||||
isRightDrawerAnimationCompletedState,
|
||||
);
|
||||
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const activityFromCache = getRecordFromCache<Activity>(activityId);
|
||||
|
||||
const activityFromStore = useRecoilValue(recordStoreFamilyState(activityId));
|
||||
|
||||
const activity = activityFromStore as Activity;
|
||||
|
||||
const useUpsertOneActivityMutation: RecordUpdateHook = () => {
|
||||
const upsertActivityMutation = async ({
|
||||
variables,
|
||||
}: RecordUpdateHookParams) => {
|
||||
if (isDefined(activityFromStore)) {
|
||||
await upsertActivity({
|
||||
activity: activityFromStore as Activity,
|
||||
input: variables.updateOneRecordInput,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return [upsertActivityMutation, { loading: false }];
|
||||
};
|
||||
|
||||
const { FieldContextProvider: ReminderAtFieldContextProvider } =
|
||||
useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectRecordId: activityId,
|
||||
fieldMetadataName: 'reminderAt',
|
||||
fieldPosition: 0,
|
||||
clearable: true,
|
||||
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
|
||||
});
|
||||
|
||||
const { FieldContextProvider: DueAtFieldContextProvider } = useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectRecordId: activityId,
|
||||
fieldMetadataName: 'dueAt',
|
||||
fieldPosition: 1,
|
||||
clearable: true,
|
||||
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
|
||||
});
|
||||
|
||||
const { FieldContextProvider: AssigneeFieldContextProvider } =
|
||||
useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectRecordId: activityId,
|
||||
fieldMetadataName: 'assignee',
|
||||
fieldPosition: 2,
|
||||
clearable: true,
|
||||
customUseUpdateOneObjectHook: useUpsertOneActivityMutation,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledPropertyBox>
|
||||
{activity.type === 'Task' &&
|
||||
ReminderAtFieldContextProvider &&
|
||||
DueAtFieldContextProvider &&
|
||||
AssigneeFieldContextProvider && (
|
||||
<>
|
||||
<ReminderAtFieldContextProvider>
|
||||
<RecordInlineCell />
|
||||
</ReminderAtFieldContextProvider>
|
||||
<DueAtFieldContextProvider>
|
||||
<RecordInlineCell />
|
||||
</DueAtFieldContextProvider>
|
||||
<AssigneeFieldContextProvider>
|
||||
<RecordInlineCell />
|
||||
</AssigneeFieldContextProvider>
|
||||
</>
|
||||
)}
|
||||
{isDefined(activityFromCache) && isRightDrawerAnimationCompleted && (
|
||||
<ActivityTargetsInlineCell
|
||||
activity={activityFromCache}
|
||||
maxWidth={340}
|
||||
/>
|
||||
)}
|
||||
</StyledPropertyBox>
|
||||
);
|
||||
};
|
||||
@ -1,199 +0,0 @@
|
||||
import { useRef } from 'react';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { activityTitleFamilyState } from '@/activities/states/activityTitleFamilyState';
|
||||
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
|
||||
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import {
|
||||
Checkbox,
|
||||
CheckboxShape,
|
||||
CheckboxSize,
|
||||
} from '@/ui/input/components/Checkbox';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledEditableTitleInput = styled.input<{
|
||||
completed: boolean;
|
||||
value: string;
|
||||
}>`
|
||||
background: transparent;
|
||||
|
||||
border: none;
|
||||
color: ${({ theme, value }) =>
|
||||
value ? theme.font.color.primary : theme.font.color.light};
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
font-size: ${({ theme }) => theme.font.size.xl};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.md};
|
||||
outline: none;
|
||||
text-decoration: ${({ completed }) => (completed ? 'line-through' : 'none')};
|
||||
&::placeholder {
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
}
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(2)});
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type ActivityTitleProps = {
|
||||
activityId: string;
|
||||
};
|
||||
|
||||
export const ActivityTitle = ({ activityId }: ActivityTitleProps) => {
|
||||
const [activityInStore, setActivityInStore] = useRecoilState(
|
||||
recordStoreFamilyState(activityId),
|
||||
);
|
||||
|
||||
const cache = useApolloClient().cache;
|
||||
|
||||
const [activityTitle, setActivityTitle] = useRecoilState(
|
||||
activityTitleFamilyState({ activityId }),
|
||||
);
|
||||
|
||||
const activity = activityInStore as Activity;
|
||||
|
||||
const [canCreateActivity, setCanCreateActivity] = useRecoilState(
|
||||
canCreateActivityState,
|
||||
);
|
||||
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
const titleInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
() => {
|
||||
handleBlur();
|
||||
},
|
||||
ActivityEditorHotkeyScope.ActivityTitle,
|
||||
);
|
||||
|
||||
const handleBlur = () => {
|
||||
goBackToPreviousHotkeyScope();
|
||||
titleInputRef.current?.blur();
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
setHotkeyScopeAndMemorizePreviousScope(
|
||||
ActivityEditorHotkeyScope.ActivityTitle,
|
||||
);
|
||||
};
|
||||
|
||||
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
|
||||
activityTitleHasBeenSetFamilyState({
|
||||
activityId: activityId,
|
||||
}),
|
||||
);
|
||||
|
||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const persistTitleDebounced = useDebouncedCallback((newTitle: string) => {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
title: newTitle,
|
||||
},
|
||||
});
|
||||
|
||||
if (!activityTitleHasBeenSet) {
|
||||
setActivityTitleHasBeenSet(true);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
const setTitleDebounced = useDebouncedCallback((newTitle: string) => {
|
||||
setActivityInStore((currentActivity) => {
|
||||
return {
|
||||
...currentActivity,
|
||||
id: activity.id,
|
||||
title: newTitle,
|
||||
__typename: activity.__typename,
|
||||
};
|
||||
});
|
||||
|
||||
if (isNonEmptyString(newTitle) && !canCreateActivity) {
|
||||
setCanCreateActivity(true);
|
||||
}
|
||||
|
||||
modifyRecordFromCache({
|
||||
recordId: activity.id,
|
||||
fieldModifiers: {
|
||||
title: () => {
|
||||
return newTitle;
|
||||
},
|
||||
},
|
||||
cache: cache,
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
}, 500);
|
||||
|
||||
const handleTitleChange = (newTitle: string) => {
|
||||
setActivityTitle(newTitle);
|
||||
|
||||
setTitleDebounced(newTitle);
|
||||
|
||||
persistTitleDebounced(newTitle);
|
||||
};
|
||||
|
||||
const handleActivityCompletionChange = (value: boolean) => {
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
completedAt: value ? new Date().toISOString() : null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const completed = isDefined(activity.completedAt);
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{activity.type === 'Task' && (
|
||||
<Checkbox
|
||||
size={CheckboxSize.Large}
|
||||
shape={CheckboxShape.Rounded}
|
||||
checked={completed}
|
||||
onCheckedChange={(value) => handleActivityCompletionChange(value)}
|
||||
/>
|
||||
)}
|
||||
<StyledEditableTitleInput
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
ref={titleInputRef}
|
||||
placeholder={`${activity.type} title`}
|
||||
onChange={(event) => handleTitleChange(event.target.value)}
|
||||
value={activityTitle}
|
||||
completed={completed}
|
||||
onBlur={handleBlur}
|
||||
onFocus={handleFocus}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -1,28 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { activityTitleFamilyState } from '@/activities/states/activityTitleFamilyState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const ActivityTitleEffect = ({ activityId }: { activityId: string }) => {
|
||||
const [activityFromStore] = useRecoilState(
|
||||
recordStoreFamilyState(activityId),
|
||||
);
|
||||
|
||||
const [activityTitle, setActivityTitle] = useRecoilState(
|
||||
activityTitleFamilyState({ activityId }),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
activityTitle === '' &&
|
||||
isDefined(activityFromStore) &&
|
||||
activityTitle !== activityFromStore.title
|
||||
) {
|
||||
setActivityTitle(activityFromStore.title);
|
||||
}
|
||||
}, [activityFromStore, activityTitle, setActivityTitle]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -1,40 +0,0 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import {
|
||||
Chip,
|
||||
ChipAccent,
|
||||
ChipSize,
|
||||
ChipVariant,
|
||||
IconCheckbox,
|
||||
IconNotes,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
|
||||
type ActivityTypeDropdownProps = {
|
||||
activityId: string;
|
||||
};
|
||||
|
||||
export const ActivityTypeDropdown = ({
|
||||
activityId,
|
||||
}: ActivityTypeDropdownProps) => {
|
||||
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Chip
|
||||
label={activityInStore?.type}
|
||||
leftComponent={
|
||||
activityInStore?.type === 'Note' ? (
|
||||
<IconNotes size={theme.icon.size.md} />
|
||||
) : (
|
||||
<IconCheckbox size={theme.icon.size.md} />
|
||||
)
|
||||
}
|
||||
size={ChipSize.Large}
|
||||
accent={ChipAccent.TextSecondary}
|
||||
variant={ChipVariant.Highlighted}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -12,10 +12,8 @@ import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState';
|
||||
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
|
||||
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
||||
@ -29,22 +27,31 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
import { getFileType } from '../files/utils/getFileType';
|
||||
|
||||
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import '@blocknote/core/fonts/inter.css';
|
||||
import '@blocknote/mantine/style.css';
|
||||
import '@blocknote/react/style.css';
|
||||
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
||||
|
||||
type ActivityBodyEditorProps = {
|
||||
type RichTextEditorProps = {
|
||||
activityId: string;
|
||||
fillTitleFromBody: boolean;
|
||||
activityObjectNameSingular:
|
||||
| CoreObjectNameSingular.Task
|
||||
| CoreObjectNameSingular.Note;
|
||||
};
|
||||
|
||||
export const ActivityBodyEditor = ({
|
||||
export const RichTextEditor = ({
|
||||
activityId,
|
||||
fillTitleFromBody,
|
||||
}: ActivityBodyEditorProps) => {
|
||||
activityObjectNameSingular,
|
||||
}: RichTextEditorProps) => {
|
||||
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
|
||||
|
||||
const cache = useApolloClient().cache;
|
||||
const activity = activityInStore as Activity | null;
|
||||
const activity = activityInStore as Task | Note | null;
|
||||
|
||||
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
|
||||
activityTitleHasBeenSetFamilyState({
|
||||
@ -60,7 +67,7 @@ export const ActivityBodyEditor = ({
|
||||
|
||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const {
|
||||
@ -68,7 +75,9 @@ export const ActivityBodyEditor = ({
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
const { upsertActivity } = useUpsertActivity({
|
||||
activityObjectNameSingular: activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const persistBodyDebounced = useDebouncedCallback((newBody: string) => {
|
||||
if (isDefined(activity)) {
|
||||
@ -1,34 +0,0 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature';
|
||||
|
||||
export const CREATE_ONE_ACTIVITY_OPERATION_SIGNATURE: RecordGqlOperationSignature =
|
||||
{
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
variables: {},
|
||||
fields: {
|
||||
id: true,
|
||||
__typename: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
author: {
|
||||
id: true,
|
||||
name: true,
|
||||
__typename: true,
|
||||
},
|
||||
authorId: true,
|
||||
assigneeId: true,
|
||||
assignee: {
|
||||
id: true,
|
||||
name: true,
|
||||
__typename: true,
|
||||
},
|
||||
comments: true,
|
||||
attachments: true,
|
||||
body: true,
|
||||
title: true,
|
||||
completedAt: true,
|
||||
dueAt: true,
|
||||
reminderAt: true,
|
||||
type: true,
|
||||
},
|
||||
};
|
||||
@ -0,0 +1,40 @@
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory';
|
||||
|
||||
export const createOneActivityOperationSignatureFactory: RecordGqlOperationSignatureFactory =
|
||||
({ objectNameSingular }: { objectNameSingular: CoreObjectNameSingular }) =>
|
||||
objectNameSingular === CoreObjectNameSingular.Note
|
||||
? {
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
variables: {},
|
||||
fields: {
|
||||
id: true,
|
||||
__typename: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
attachments: true,
|
||||
body: true,
|
||||
title: true,
|
||||
},
|
||||
}
|
||||
: {
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
variables: {},
|
||||
fields: {
|
||||
id: true,
|
||||
__typename: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
assigneeId: true,
|
||||
assignee: {
|
||||
id: true,
|
||||
name: true,
|
||||
__typename: true,
|
||||
},
|
||||
attachments: true,
|
||||
body: true,
|
||||
title: true,
|
||||
status: true,
|
||||
dueAt: true,
|
||||
},
|
||||
};
|
||||
@ -4,8 +4,14 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory';
|
||||
|
||||
export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatureFactory =
|
||||
({ objectMetadataItems }: { objectMetadataItems: ObjectMetadataItem[] }) => ({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
({
|
||||
objectMetadataItems,
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
objectNameSingular: CoreObjectNameSingular;
|
||||
}) => ({
|
||||
objectNameSingular: objectNameSingular,
|
||||
variables: {},
|
||||
fields: {
|
||||
id: true,
|
||||
@ -28,18 +34,32 @@ export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatur
|
||||
attachments: true,
|
||||
body: true,
|
||||
title: true,
|
||||
completedAt: true,
|
||||
status: true,
|
||||
dueAt: true,
|
||||
reminderAt: true,
|
||||
type: true,
|
||||
activityTargets: {
|
||||
...(objectNameSingular === CoreObjectNameSingular.Note
|
||||
? {
|
||||
noteTargets: {
|
||||
id: true,
|
||||
__typename: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
activity: true,
|
||||
activityId: true,
|
||||
note: true,
|
||||
noteId: true,
|
||||
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
|
||||
},
|
||||
}
|
||||
: {
|
||||
taskTargets: {
|
||||
id: true,
|
||||
__typename: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
task: true,
|
||||
taskId: true,
|
||||
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
@ -1,11 +1,18 @@
|
||||
import { generateActivityTargetMorphFieldKeys } from '@/activities/utils/generateActivityTargetMorphFieldKeys';
|
||||
import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory';
|
||||
|
||||
export const findActivityTargetsOperationSignatureFactory: RecordGqlOperationSignatureFactory =
|
||||
({ objectMetadataItems }: { objectMetadataItems: ObjectMetadataItem[] }) => ({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
({
|
||||
objectNameSingular,
|
||||
objectMetadataItems,
|
||||
}: {
|
||||
objectNameSingular: CoreObjectNameSingular;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
}) => ({
|
||||
objectNameSingular: getJoinObjectNameSingular(objectNameSingular),
|
||||
variables: {},
|
||||
fields: {
|
||||
id: true,
|
||||
|
||||
@ -6,6 +6,7 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useActivities } from '@/activities/hooks/useActivities';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||
|
||||
@ -20,16 +21,14 @@ const mockActivityTarget = {
|
||||
};
|
||||
|
||||
const mockActivity = {
|
||||
__typename: 'Activity',
|
||||
__typename: 'Note',
|
||||
updatedAt: '2021-08-03T19:20:06.000Z',
|
||||
createdAt: '2021-08-03T19:20:06.000Z',
|
||||
completedAt: '2021-08-03T19:20:06.000Z',
|
||||
status: 'DONE',
|
||||
reminderAt: '2021-08-03T19:20:06.000Z',
|
||||
title: 'title',
|
||||
authorId: '1',
|
||||
body: 'body',
|
||||
dueAt: '2021-08-03T19:20:06.000Z',
|
||||
type: 'type',
|
||||
assigneeId: '1',
|
||||
id: '234',
|
||||
};
|
||||
@ -102,13 +101,13 @@ const mocks: MockedResponse[] = [
|
||||
{
|
||||
request: {
|
||||
query: gql`
|
||||
query FindManyActivities(
|
||||
$filter: ActivityFilterInput
|
||||
$orderBy: [ActivityOrderByInput]
|
||||
query FindManyTasks(
|
||||
$filter: TaskFilterInput
|
||||
$orderBy: [TaskOrderByInput]
|
||||
$lastCursor: String
|
||||
$limit: Int
|
||||
) {
|
||||
activities(
|
||||
tasks(
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
first: $limit
|
||||
@ -117,17 +116,13 @@ const mocks: MockedResponse[] = [
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
createdAt
|
||||
reminderAt
|
||||
authorId
|
||||
title
|
||||
completedAt
|
||||
updatedAt
|
||||
body
|
||||
dueAt
|
||||
type
|
||||
id
|
||||
assigneeId
|
||||
updatedAt
|
||||
createdAt
|
||||
body
|
||||
status
|
||||
dueAt
|
||||
}
|
||||
cursor
|
||||
}
|
||||
@ -178,6 +173,7 @@ describe('useActivities', () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useActivities({
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
targetableObjects: [],
|
||||
activitiesFilters: {},
|
||||
activitiesOrderByVariables: [{}],
|
||||
@ -200,6 +196,7 @@ describe('useActivities', () => {
|
||||
);
|
||||
|
||||
const activities = useActivities({
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
targetableObjects: [
|
||||
{ targetObjectNameSingular: 'company', id: '123' },
|
||||
],
|
||||
|
||||
@ -7,8 +7,8 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
import { getRecordFromRecordNode } from '@/object-record/cache/utils/getRecordFromRecordNode';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
||||
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||
@ -16,108 +16,95 @@ const mockObjectMetadataItems = getObjectMetadataItemsMock();
|
||||
|
||||
const cache = new InMemoryCache();
|
||||
|
||||
const activityNode = {
|
||||
id: '3ecaa1be-aac7-463a-a38e-64078dd451d5',
|
||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
reminderAt: null,
|
||||
title: 'My very first note',
|
||||
type: 'Note',
|
||||
body: '',
|
||||
dueAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
completedAt: null,
|
||||
author: null,
|
||||
assignee: null,
|
||||
assigneeId: null,
|
||||
authorId: null,
|
||||
comments: {
|
||||
edges: [],
|
||||
},
|
||||
activityTargets: {
|
||||
edges: [
|
||||
{
|
||||
node: {
|
||||
const taskTarget = {
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
|
||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
personId: null,
|
||||
companyId: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
|
||||
company: {
|
||||
companyId: null,
|
||||
company: null,
|
||||
personId: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
|
||||
person: {
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
|
||||
name: 'Airbnb',
|
||||
domainName: {
|
||||
primaryLinkUrl: 'https://www.airbnb.com',
|
||||
primaryLinkLabel: '',
|
||||
secondaryLinks: null,
|
||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
city: 'City',
|
||||
name: {
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
},
|
||||
__typename: 'Person',
|
||||
},
|
||||
person: null,
|
||||
activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
|
||||
activity: {
|
||||
taskId: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
|
||||
task: {
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
|
||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
dueAt: null,
|
||||
body: '{}',
|
||||
title: 'Task title',
|
||||
assigneeId: null,
|
||||
__typename: 'Task',
|
||||
},
|
||||
__typename: 'ActivityTarget',
|
||||
},
|
||||
__typename: 'ActivityTargetEdge',
|
||||
},
|
||||
],
|
||||
__typename: 'ActivityTargetConnection',
|
||||
},
|
||||
__typename: 'Activity' as const,
|
||||
__typename: 'TaskTarget',
|
||||
};
|
||||
|
||||
cache.writeFragment({
|
||||
fragment: gql`
|
||||
fragment CreateOneActivityInCache on Activity {
|
||||
id
|
||||
createdAt
|
||||
fragment TaskTargetFragment on TaskTarget {
|
||||
__typename
|
||||
updatedAt
|
||||
reminderAt
|
||||
createdAt
|
||||
personId
|
||||
taskId
|
||||
companyId
|
||||
id
|
||||
task {
|
||||
__typename
|
||||
createdAt
|
||||
title
|
||||
updatedAt
|
||||
body
|
||||
dueAt
|
||||
completedAt
|
||||
author
|
||||
assignee
|
||||
id
|
||||
assigneeId
|
||||
authorId
|
||||
activityTargets {
|
||||
edges {
|
||||
node {
|
||||
}
|
||||
person {
|
||||
__typename
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
personId
|
||||
companyId
|
||||
city
|
||||
name {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
}
|
||||
company {
|
||||
id
|
||||
name
|
||||
domainName {
|
||||
primaryLinkUrl
|
||||
primaryLinkLabel
|
||||
secondaryLinks
|
||||
}
|
||||
}
|
||||
person
|
||||
activityId
|
||||
activity {
|
||||
__typename
|
||||
id
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
__typename
|
||||
}
|
||||
}
|
||||
}
|
||||
__typename
|
||||
}
|
||||
`,
|
||||
id: activityNode.id,
|
||||
data: activityNode,
|
||||
id: `TaskTarget:${taskTarget.id}`,
|
||||
data: taskTarget,
|
||||
});
|
||||
|
||||
const task = {
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
|
||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
title: 'Task title',
|
||||
body: null,
|
||||
assigneeId: null,
|
||||
status: null,
|
||||
dueAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
assignee: null,
|
||||
__typename: 'Task' as any,
|
||||
taskTargets: [taskTarget],
|
||||
};
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider cache={cache}>
|
||||
@ -142,7 +129,8 @@ describe('useActivityTargetObjectRecords', () => {
|
||||
);
|
||||
|
||||
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
|
||||
getRecordFromRecordNode({ recordNode: activityNode as any }),
|
||||
task,
|
||||
CoreObjectNameSingular.Task,
|
||||
);
|
||||
|
||||
return {
|
||||
@ -158,18 +146,17 @@ describe('useActivityTargetObjectRecords', () => {
|
||||
result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
|
||||
result.current.setObjectMetadataItems(mockObjectMetadataItems);
|
||||
});
|
||||
|
||||
const activityTargetObjectRecords =
|
||||
result.current.activityTargetObjectRecords;
|
||||
|
||||
expect(activityTargetObjectRecords).toHaveLength(1);
|
||||
expect(activityTargetObjectRecords[0].activityTarget).toEqual(
|
||||
activityNode.activityTargets.edges[0].node,
|
||||
);
|
||||
expect(activityTargetObjectRecords[0].activityTarget).toEqual(taskTarget);
|
||||
expect(activityTargetObjectRecords[0].targetObject).toEqual(
|
||||
activityNode.activityTargets.edges[0].node.company,
|
||||
taskTarget.person,
|
||||
);
|
||||
expect(
|
||||
activityTargetObjectRecords[0].targetObjectMetadataItem.nameSingular,
|
||||
).toEqual('company');
|
||||
).toEqual('person');
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,131 +0,0 @@
|
||||
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import gql from 'graphql-tag';
|
||||
import { ReactNode } from 'react';
|
||||
import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||
|
||||
const mockActivityTarget = {
|
||||
__typename: 'ActivityTarget',
|
||||
updatedAt: '2021-08-03T19:20:06.000Z',
|
||||
createdAt: '2021-08-03T19:20:06.000Z',
|
||||
personId: '1',
|
||||
activityId: '234',
|
||||
companyId: '1',
|
||||
id: '123',
|
||||
};
|
||||
|
||||
const defaultResponseData = {
|
||||
pageInfo: {
|
||||
hasNextPage: false,
|
||||
startCursor: '',
|
||||
endCursor: '',
|
||||
},
|
||||
totalCount: 1,
|
||||
};
|
||||
|
||||
const mocks: MockedResponse[] = [
|
||||
{
|
||||
request: {
|
||||
query: gql`
|
||||
query FindManyActivityTargets(
|
||||
$filter: ActivityTargetFilterInput
|
||||
$orderBy: [ActivityTargetOrderByInput]
|
||||
$lastCursor: String
|
||||
$limit: Int
|
||||
) {
|
||||
activityTargets(
|
||||
filter: $filter
|
||||
orderBy: $orderBy
|
||||
first: $limit
|
||||
after: $lastCursor
|
||||
) {
|
||||
edges {
|
||||
node {
|
||||
__typename
|
||||
updatedAt
|
||||
createdAt
|
||||
personId
|
||||
activityId
|
||||
companyId
|
||||
id
|
||||
}
|
||||
cursor
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
startCursor
|
||||
endCursor
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
filter: { personId: { eq: '1234' } },
|
||||
orderBy: undefined,
|
||||
lastCursor: undefined,
|
||||
limit: undefined,
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
activityTargets: {
|
||||
...defaultResponseData,
|
||||
edges: [
|
||||
{
|
||||
node: mockActivityTarget,
|
||||
cursor: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('useActivityTargetsForTargetableObject', () => {
|
||||
it('works as expected', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
|
||||
const res = useActivityTargetsForTargetableObject({
|
||||
targetableObject: {
|
||||
id: '1234',
|
||||
targetObjectNameSingular: 'person',
|
||||
},
|
||||
});
|
||||
return { ...res, setCurrentWorkspaceMember };
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
|
||||
});
|
||||
|
||||
expect(result.current.loadingActivityTargets).toBe(true);
|
||||
|
||||
await waitFor(() => !result.current.loadingActivityTargets);
|
||||
|
||||
expect(result.current.activityTargets).toEqual([mockActivityTarget]);
|
||||
});
|
||||
});
|
||||
@ -1,106 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import gql from 'graphql-tag';
|
||||
import { RecoilRoot, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useCreateActivityInCache } from '@/activities/hooks/useCreateActivityInCache';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||
|
||||
const mocks: MockedResponse[] = [
|
||||
{
|
||||
request: {
|
||||
query: gql`
|
||||
query FindOneWorkspaceMember($objectRecordId: ID!) {
|
||||
workspaceMember(filter: { id: { eq: $objectRecordId } }) {
|
||||
__typename
|
||||
colorScheme
|
||||
name {
|
||||
firstName
|
||||
lastName
|
||||
}
|
||||
locale
|
||||
userId
|
||||
avatarUrl
|
||||
createdAt
|
||||
updatedAt
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { objectRecordId: '20202020-1553-45c6-a028-5a9064cce07f' },
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
workspaceMember: mockWorkspaceMembers[0],
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
const mockObjectMetadataItems = getObjectMetadataItemsMock();
|
||||
|
||||
describe('useCreateActivityInCache', () => {
|
||||
it('Should create activity in cache', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
const setObjectMetadataItems = useSetRecoilState(
|
||||
objectMetadataItemsState,
|
||||
);
|
||||
const setRecordStore = useSetRecoilState(
|
||||
recordStoreFamilyState('1234'),
|
||||
);
|
||||
|
||||
const res = useCreateActivityInCache();
|
||||
return {
|
||||
...res,
|
||||
setCurrentWorkspaceMember,
|
||||
setObjectMetadataItems,
|
||||
setRecordStore,
|
||||
};
|
||||
},
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.setRecordStore({
|
||||
id: '1234',
|
||||
__typename: 'Person',
|
||||
});
|
||||
result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
|
||||
result.current.setObjectMetadataItems(mockObjectMetadataItems);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
const res = result.current.createActivityInCache({
|
||||
type: 'Note',
|
||||
targetObject: {
|
||||
targetObjectNameSingular: 'person',
|
||||
id: '1234',
|
||||
},
|
||||
});
|
||||
|
||||
expect(res.createdActivityInCache).toHaveProperty('id');
|
||||
expect(res.createdActivityInCache).toHaveProperty('__typename');
|
||||
expect(res.createdActivityInCache).toHaveProperty('activityTargets');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -6,22 +6,16 @@ import { ReactNode } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { mockedActivities } from '~/testing/mock-data/activities';
|
||||
import { mockedTasks } from '~/testing/mock-data/tasks';
|
||||
|
||||
const mockedDate = '2024-03-15T12:00:00.000Z';
|
||||
const toISOStringMock = jest.fn(() => mockedDate);
|
||||
global.Date.prototype.toISOString = toISOStringMock;
|
||||
|
||||
const mockedActivity = {
|
||||
...pick(mockedActivities[0], [
|
||||
'id',
|
||||
'title',
|
||||
'body',
|
||||
'type',
|
||||
'completedAt',
|
||||
'dueAt',
|
||||
]),
|
||||
...pick(mockedTasks[0], ['id', 'title', 'body', 'type', 'status', 'dueAt']),
|
||||
updatedAt: mockedDate,
|
||||
};
|
||||
|
||||
@ -36,7 +30,7 @@ const mocks: MockedResponse[] = [
|
||||
reminderAt
|
||||
authorId
|
||||
title
|
||||
completedAt
|
||||
status
|
||||
updatedAt
|
||||
body
|
||||
dueAt
|
||||
@ -77,14 +71,19 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
|
||||
describe('useCreateActivityInDB', () => {
|
||||
it('Should create activity in DB', async () => {
|
||||
const { result } = renderHook(() => useCreateActivityInDB(), {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useCreateActivityInDB({
|
||||
activityObjectNameSingular: CoreObjectNameSingular.Task,
|
||||
}),
|
||||
{
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.createActivityInDB({
|
||||
...mockedActivity,
|
||||
__typename: 'Activity',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
@ -17,12 +17,12 @@ describe('useOpenActivityRightDrawer', () => {
|
||||
it('works as expected', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer({
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
});
|
||||
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
||||
const activityIdInDrawer = useRecoilValue(activityIdInDrawerState);
|
||||
return {
|
||||
openActivityRightDrawer,
|
||||
activityIdInDrawer,
|
||||
viewableRecordId,
|
||||
};
|
||||
},
|
||||
@ -31,12 +31,10 @@ describe('useOpenActivityRightDrawer', () => {
|
||||
},
|
||||
);
|
||||
|
||||
expect(result.current.activityIdInDrawer).toBeNull();
|
||||
expect(result.current.viewableRecordId).toBeNull();
|
||||
act(() => {
|
||||
result.current.openActivityRightDrawer('123');
|
||||
});
|
||||
expect(result.current.activityIdInDrawer).toBe('123');
|
||||
expect(result.current.viewableRecordId).toBe('123');
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,23 +1,71 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import gql from 'graphql-tag';
|
||||
import pick from 'lodash.pick';
|
||||
import { mockedTasks } from '~/testing/mock-data/tasks';
|
||||
|
||||
const mockUUID = '37873e04-2f83-4468-9ab7-3f87da6cafad';
|
||||
const mockedDate = '2024-03-15T12:00:00.000Z';
|
||||
const toISOStringMock = jest.fn(() => mockedDate);
|
||||
global.Date.prototype.toISOString = toISOStringMock;
|
||||
|
||||
jest.mock('uuid', () => ({
|
||||
v4: () => mockUUID,
|
||||
}));
|
||||
const mockedActivity = {
|
||||
...pick(mockedTasks[0], ['id', 'title', 'body', 'type', 'status', 'dueAt']),
|
||||
updatedAt: mockedDate,
|
||||
};
|
||||
|
||||
const mocks: MockedResponse[] = [
|
||||
{
|
||||
request: {
|
||||
query: gql`
|
||||
mutation CreateOneActivity($input: ActivityCreateInput!) {
|
||||
createActivity(data: $input) {
|
||||
__typename
|
||||
createdAt
|
||||
reminderAt
|
||||
authorId
|
||||
title
|
||||
status
|
||||
updatedAt
|
||||
body
|
||||
dueAt
|
||||
type
|
||||
id
|
||||
assigneeId
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: mockedActivity,
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
createActivity: {
|
||||
...mockedActivity,
|
||||
__typename: 'Activity',
|
||||
assigneeId: '',
|
||||
authorId: '1',
|
||||
reminderAt: null,
|
||||
createdAt: mockedDate,
|
||||
},
|
||||
},
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider addTypename={false}>{children}</MockedProvider>
|
||||
<MockedProvider addTypename={false} mocks={mocks}>
|
||||
{children}
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
@ -27,15 +75,15 @@ describe('useOpenCreateActivityDrawer', () => {
|
||||
it('works as expected', async () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const openActivityRightDrawer = useOpenCreateActivityDrawer();
|
||||
const openActivityRightDrawer = useOpenCreateActivityDrawer({
|
||||
activityObjectNameSingular: CoreObjectNameSingular.Note,
|
||||
});
|
||||
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
||||
const activityIdInDrawer = useRecoilValue(activityIdInDrawerState);
|
||||
const setObjectMetadataItems = useSetRecoilState(
|
||||
objectMetadataItemsState,
|
||||
);
|
||||
return {
|
||||
openActivityRightDrawer,
|
||||
activityIdInDrawer,
|
||||
viewableRecordId,
|
||||
setObjectMetadataItems,
|
||||
};
|
||||
@ -49,15 +97,11 @@ describe('useOpenCreateActivityDrawer', () => {
|
||||
result.current.setObjectMetadataItems(mockObjectMetadataItems);
|
||||
});
|
||||
|
||||
expect(result.current.activityIdInDrawer).toBeNull();
|
||||
expect(result.current.viewableRecordId).toBeNull();
|
||||
await act(async () => {
|
||||
result.current.openActivityRightDrawer({
|
||||
type: 'Note',
|
||||
targetableObjects: [],
|
||||
});
|
||||
});
|
||||
expect(result.current.activityIdInDrawer).toBe(mockUUID);
|
||||
expect(result.current.viewableRecordId).toBe(mockUUID);
|
||||
});
|
||||
});
|
||||
|
||||
@ -3,21 +3,25 @@ import { useRecoilCallback } from 'recoil';
|
||||
|
||||
import { findActivitiesOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory';
|
||||
import { useActivityTargetsForTargetableObjects } from '@/activities/hooks/useActivityTargetsForTargetableObjects';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { sortByAscString } from '~/utils/array/sortByAscString';
|
||||
|
||||
export const useActivities = ({
|
||||
export const useActivities = <T extends Task | Note>({
|
||||
objectNameSingular,
|
||||
targetableObjects,
|
||||
activitiesFilters,
|
||||
activitiesOrderByVariables,
|
||||
skip,
|
||||
}: {
|
||||
objectNameSingular: CoreObjectNameSingular;
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
activitiesFilters: RecordGqlOperationFilter;
|
||||
activitiesOrderByVariables: RecordGqlOperationOrderBy;
|
||||
@ -27,6 +31,7 @@ export const useActivities = ({
|
||||
|
||||
const { activityTargets, loadingActivityTargets } =
|
||||
useActivityTargetsForTargetableObjects({
|
||||
objectNameSingular,
|
||||
targetableObjects,
|
||||
skip: skip,
|
||||
});
|
||||
@ -36,7 +41,10 @@ export const useActivities = ({
|
||||
activityTargets
|
||||
? [
|
||||
...activityTargets
|
||||
.map((activityTarget) => activityTarget.activityId)
|
||||
.map(
|
||||
(activityTarget) =>
|
||||
activityTarget.taskId ?? activityTarget.noteId,
|
||||
)
|
||||
.filter(isNonEmptyString),
|
||||
].sort(sortByAscString)
|
||||
: [],
|
||||
@ -54,10 +62,13 @@ export const useActivities = ({
|
||||
};
|
||||
|
||||
const FIND_ACTIVITIES_OPERATION_SIGNATURE =
|
||||
findActivitiesOperationSignatureFactory({ objectMetadataItems });
|
||||
findActivitiesOperationSignatureFactory({
|
||||
objectMetadataItems,
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
const { records: activities, loading: loadingActivities } =
|
||||
useFindManyRecords<Activity>({
|
||||
useFindManyRecords<Task | Note>({
|
||||
skip: skip || loadingActivityTargets,
|
||||
objectNameSingular:
|
||||
FIND_ACTIVITIES_OPERATION_SIGNATURE.objectNameSingular,
|
||||
@ -76,7 +87,7 @@ export const useActivities = ({
|
||||
});
|
||||
|
||||
return {
|
||||
activities,
|
||||
activities: activities as T[],
|
||||
loading: loadingActivities || loadingActivityTargets,
|
||||
};
|
||||
};
|
||||
|
||||
@ -2,31 +2,41 @@ import { useApolloClient } from '@apollo/client';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { Nullable } from 'twenty-ui';
|
||||
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { NoteTarget } from '@/activities/types/NoteTarget';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useActivityTargetObjectRecords = (activity: Activity) => {
|
||||
export const useActivityTargetObjectRecords = (
|
||||
activity: Task | Note,
|
||||
objectNameSingular: CoreObjectNameSingular,
|
||||
) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const activityTargets = activity.activityTargets ?? [];
|
||||
const activityTargets =
|
||||
'noteTargets' in activity && activity.noteTargets
|
||||
? activity.noteTargets
|
||||
: 'taskTargets' in activity && activity.taskTargets
|
||||
? activity.taskTargets
|
||||
: [];
|
||||
|
||||
const getRecordFromCache = useGetRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
objectNameSingular: getJoinObjectNameSingular(objectNameSingular),
|
||||
});
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const activityTargetObjectRecords = activityTargets
|
||||
.map<Nullable<ActivityTargetWithTargetRecord>>((activityTarget) => {
|
||||
const activityTargetFromCache = getRecordFromCache<ActivityTarget>(
|
||||
activityTarget.id,
|
||||
apolloClient.cache,
|
||||
);
|
||||
const activityTargetFromCache = getRecordFromCache<
|
||||
NoteTarget | TaskTarget
|
||||
>(activityTarget.id, apolloClient.cache);
|
||||
|
||||
if (!isDefined(activityTargetFromCache)) {
|
||||
throw new Error(
|
||||
@ -37,7 +47,9 @@ export const useActivityTargetObjectRecords = (activity: Activity) => {
|
||||
const correspondingObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
isDefined(activityTargetFromCache[objectMetadataItem.nameSingular]) &&
|
||||
!objectMetadataItem.isSystem,
|
||||
![CoreObjectNameSingular.Note, CoreObjectNameSingular.Task].includes(
|
||||
objectMetadataItem.nameSingular as CoreObjectNameSingular,
|
||||
),
|
||||
);
|
||||
|
||||
if (!correspondingObjectMetadataItem) {
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
|
||||
export const useActivityTargetsForTargetableObject = ({
|
||||
targetableObject,
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const targetObjectFieldName = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const targetableObjectId = targetableObject.id;
|
||||
|
||||
const skipRequest = !isNonEmptyString(targetableObjectId);
|
||||
|
||||
// TODO: We want to optimistically remove from this request
|
||||
// If we are on a show page and we remove the current show page object corresponding activity target
|
||||
// See also if we need to update useTimelineActivities
|
||||
const { records: activityTargets, loading: loadingActivityTargets } =
|
||||
useFindManyRecords<ActivityTarget>({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
skip: skipRequest,
|
||||
filter: {
|
||||
[targetObjectFieldName]: {
|
||||
eq: targetableObject.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
activityTargets,
|
||||
loadingActivityTargets,
|
||||
};
|
||||
};
|
||||
@ -1,23 +1,27 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { findActivityTargetsOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/findActivityTargetsOperationSignatureFactory';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { NoteTarget } from '@/activities/types/NoteTarget';
|
||||
import { TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
|
||||
export const useActivityTargetsForTargetableObjects = ({
|
||||
objectNameSingular,
|
||||
targetableObjects,
|
||||
skip,
|
||||
onCompleted,
|
||||
}: {
|
||||
objectNameSingular: CoreObjectNameSingular;
|
||||
targetableObjects: Pick<
|
||||
ActivityTargetableObject,
|
||||
'id' | 'targetObjectNameSingular'
|
||||
>[];
|
||||
skip?: boolean;
|
||||
onCompleted?: (activityTargets: ActivityTarget[]) => void;
|
||||
onCompleted?: (activityTargets: (TaskTarget | NoteTarget)[]) => void;
|
||||
}) => {
|
||||
const activityTargetsFilter = getActivityTargetsFilter({
|
||||
targetableObjects: targetableObjects,
|
||||
@ -26,13 +30,16 @@ export const useActivityTargetsForTargetableObjects = ({
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
const FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE =
|
||||
findActivityTargetsOperationSignatureFactory({ objectMetadataItems });
|
||||
findActivityTargetsOperationSignatureFactory({
|
||||
objectNameSingular,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
// TODO: We want to optimistically remove from this request
|
||||
// If we are on a show page and we remove the current show page object corresponding activity target
|
||||
// See also if we need to update useTimelineActivities
|
||||
const { records: activityTargets, loading: loadingActivityTargets } =
|
||||
useFindManyRecords<ActivityTarget>({
|
||||
useFindManyRecords<TaskTarget | NoteTarget>({
|
||||
skip,
|
||||
objectNameSingular:
|
||||
FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE.objectNameSingular,
|
||||
|
||||
@ -1,188 +0,0 @@
|
||||
import { Reference, useApolloClient } from '@apollo/client';
|
||||
import { useRecoilCallback, useRecoilValue } from 'recoil';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { Activity, ActivityType } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { makeActivityTargetsToCreateFromTargetableObjects } from '@/activities/utils/getActivityTargetsToCreateFromTargetableObjects';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateManyRecordsInCache } from '@/object-record/cache/hooks/useCreateManyRecordsInCache';
|
||||
import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache';
|
||||
import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords';
|
||||
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
export const useCreateActivityInCache = () => {
|
||||
const { createManyRecordsInCache: createManyActivityTargetsInCache } =
|
||||
useCreateManyRecordsInCache<ActivityTarget>({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const cache = useApolloClient().cache;
|
||||
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const { record: currentWorkspaceMemberRecord } = useFindOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
|
||||
objectRecordId: currentWorkspaceMember?.id,
|
||||
});
|
||||
|
||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const { objectMetadataItem: objectMetadataItemActivityTarget } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const createOneActivityInCache = useCreateOneRecordInCache<Activity>({
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
|
||||
const createActivityInCache = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
({
|
||||
type,
|
||||
targetObject,
|
||||
customAssignee,
|
||||
}: {
|
||||
type: ActivityType;
|
||||
targetObject?: ActivityTargetableObject;
|
||||
customAssignee?: WorkspaceMember;
|
||||
}) => {
|
||||
const activityId = v4();
|
||||
|
||||
const createdActivityInCache = createOneActivityInCache({
|
||||
id: activityId,
|
||||
__typename: 'Activity',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
author: currentWorkspaceMemberRecord,
|
||||
authorId: currentWorkspaceMemberRecord?.id,
|
||||
assignee: customAssignee ?? currentWorkspaceMemberRecord,
|
||||
assigneeId: customAssignee?.id ?? currentWorkspaceMemberRecord?.id,
|
||||
type,
|
||||
});
|
||||
|
||||
if (isUndefinedOrNull(createdActivityInCache)) {
|
||||
throw new Error('Failed to create activity in cache');
|
||||
}
|
||||
|
||||
if (isUndefinedOrNull(targetObject)) {
|
||||
set(recordStoreFamilyState(activityId), {
|
||||
...createdActivityInCache,
|
||||
activityTargets: [],
|
||||
comments: [],
|
||||
});
|
||||
|
||||
return {
|
||||
createdActivityInCache: {
|
||||
...createdActivityInCache,
|
||||
activityTargets: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const targetObjectRecord = snapshot
|
||||
.getLoadable(recordStoreFamilyState(targetObject.id))
|
||||
.getValue();
|
||||
|
||||
if (isUndefinedOrNull(targetObjectRecord)) {
|
||||
throw new Error('Failed to find target object record');
|
||||
}
|
||||
|
||||
const activityTargetsToCreate =
|
||||
makeActivityTargetsToCreateFromTargetableObjects({
|
||||
activity: createdActivityInCache,
|
||||
targetableObjects: [targetObject],
|
||||
targetObjectRecords: [targetObjectRecord],
|
||||
});
|
||||
|
||||
const createdActivityTargetsInCache = createManyActivityTargetsInCache(
|
||||
activityTargetsToCreate,
|
||||
);
|
||||
|
||||
const activityTargetsConnection = getRecordConnectionFromRecords({
|
||||
objectMetadataItems: objectMetadataItems,
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
records: createdActivityTargetsInCache,
|
||||
withPageInfo: false,
|
||||
computeReferences: true,
|
||||
isRootLevel: false,
|
||||
});
|
||||
|
||||
modifyRecordFromCache({
|
||||
recordId: createdActivityInCache.id,
|
||||
cache,
|
||||
fieldModifiers: {
|
||||
activityTargets: () => activityTargetsConnection,
|
||||
},
|
||||
objectMetadataItem: objectMetadataItemActivity,
|
||||
});
|
||||
|
||||
const targetObjectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.nameSingular === targetObject.targetObjectNameSingular,
|
||||
);
|
||||
|
||||
if (isDefined(targetObjectMetadataItem)) {
|
||||
modifyRecordFromCache({
|
||||
cache,
|
||||
objectMetadataItem: targetObjectMetadataItem,
|
||||
recordId: targetObject.id,
|
||||
fieldModifiers: {
|
||||
activityTargets: (activityTargetsRef, { readField }) => {
|
||||
const edges = readField<{ node: Reference }[]>(
|
||||
'edges',
|
||||
activityTargetsRef,
|
||||
);
|
||||
|
||||
if (!edges) return activityTargetsRef;
|
||||
|
||||
return {
|
||||
...activityTargetsRef,
|
||||
edges: [...edges, ...activityTargetsConnection.edges],
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
set(recordStoreFamilyState(activityId), {
|
||||
...createdActivityInCache,
|
||||
activityTargets: createdActivityTargetsInCache,
|
||||
comments: [],
|
||||
});
|
||||
|
||||
return {
|
||||
createdActivityInCache: {
|
||||
...createdActivityInCache,
|
||||
activityTargets: createdActivityTargetsInCache,
|
||||
},
|
||||
};
|
||||
},
|
||||
[
|
||||
createOneActivityInCache,
|
||||
currentWorkspaceMemberRecord,
|
||||
createManyActivityTargetsInCache,
|
||||
objectMetadataItems,
|
||||
objectMetadataItemActivityTarget,
|
||||
cache,
|
||||
objectMetadataItemActivity,
|
||||
],
|
||||
);
|
||||
|
||||
return {
|
||||
createActivityInCache,
|
||||
};
|
||||
};
|
||||
@ -1,8 +1,6 @@
|
||||
import { isNonEmptyArray } from '@sniptt/guards';
|
||||
|
||||
import { CREATE_ONE_ACTIVITY_OPERATION_SIGNATURE } from '@/activities/graphql/operation-signatures/CreateOneActivityOperationSignature';
|
||||
import { ActivityForEditor } from '@/activities/types/ActivityForEditor';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
@ -13,20 +11,35 @@ import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { createOneActivityOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/createOneActivityOperationSignatureFactory';
|
||||
import { NoteTarget } from '@/activities/types/NoteTarget';
|
||||
import { TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular';
|
||||
import { useRecoilCallback } from 'recoil';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useCreateActivityInDB = () => {
|
||||
export const useCreateActivityInDB = ({
|
||||
activityObjectNameSingular,
|
||||
}: {
|
||||
activityObjectNameSingular:
|
||||
| CoreObjectNameSingular.Task
|
||||
| CoreObjectNameSingular.Note;
|
||||
}) => {
|
||||
const createOneActivityOperationSignature =
|
||||
createOneActivityOperationSignatureFactory({
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const { createOneRecord: createOneActivity } = useCreateOneRecord({
|
||||
objectNameSingular:
|
||||
CREATE_ONE_ACTIVITY_OPERATION_SIGNATURE.objectNameSingular,
|
||||
recordGqlFields: CREATE_ONE_ACTIVITY_OPERATION_SIGNATURE.fields,
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
recordGqlFields: createOneActivityOperationSignature.fields,
|
||||
shouldMatchRootQueryFilter: true,
|
||||
});
|
||||
|
||||
const { createManyRecords: createManyActivityTargets } =
|
||||
useCreateManyRecords<ActivityTarget>({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
const { createManyRecords: createManyActivityTargets } = useCreateManyRecords<
|
||||
TaskTarget | NoteTarget
|
||||
>({
|
||||
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||
shouldMatchRootQueryFilter: true,
|
||||
});
|
||||
|
||||
@ -34,12 +47,12 @@ export const useCreateActivityInDB = () => {
|
||||
|
||||
const { objectMetadataItem: objectMetadataItemActivityTarget } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||
});
|
||||
|
||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const cache = useApolloClient().cache;
|
||||
@ -52,7 +65,8 @@ export const useCreateActivityInDB = () => {
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
const activityTargetsToCreate = activityToCreate.activityTargets ?? [];
|
||||
const activityTargetsToCreate =
|
||||
activityToCreate.noteTargets ?? activityToCreate.taskTargets ?? [];
|
||||
|
||||
if (isNonEmptyArray(activityTargetsToCreate)) {
|
||||
await createManyActivityTargets(activityTargetsToCreate);
|
||||
|
||||
@ -1,25 +1,34 @@
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
|
||||
export const useOpenActivityRightDrawer = () => {
|
||||
export const useOpenActivityRightDrawer = ({
|
||||
objectNameSingular,
|
||||
}: {
|
||||
objectNameSingular: CoreObjectNameSingular;
|
||||
}) => {
|
||||
const { openRightDrawer, isRightDrawerOpen, rightDrawerPage } =
|
||||
useRightDrawer();
|
||||
const [viewableRecordId, setViewableRecordId] = useRecoilState(
|
||||
viewableRecordIdState,
|
||||
);
|
||||
const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState);
|
||||
|
||||
const setViewableRecordNameSingular = useSetRecoilState(
|
||||
viewableRecordNameSingularState,
|
||||
);
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
return (activityId: string) => {
|
||||
if (
|
||||
isRightDrawerOpen &&
|
||||
rightDrawerPage === RightDrawerPages.EditActivity &&
|
||||
rightDrawerPage === RightDrawerPages.ViewRecord &&
|
||||
viewableRecordId === activityId
|
||||
) {
|
||||
return;
|
||||
@ -27,7 +36,7 @@ export const useOpenActivityRightDrawer = () => {
|
||||
|
||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||
setViewableRecordId(activityId);
|
||||
setActivityIdInDrawer(activityId);
|
||||
openRightDrawer(RightDrawerPages.EditActivity);
|
||||
setViewableRecordNameSingular(objectNameSingular);
|
||||
openRightDrawer(RightDrawerPages.ViewRecord);
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useCreateActivityInCache } from '@/activities/hooks/useCreateActivityInCache';
|
||||
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
|
||||
import { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
|
||||
import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState';
|
||||
import { ActivityType } from '@/activities/types/Activity';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||
@ -14,54 +9,84 @@ import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPage
|
||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { NoteTarget } from '@/activities/types/NoteTarget';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState';
|
||||
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
|
||||
|
||||
export const useOpenCreateActivityDrawer = () => {
|
||||
export const useOpenCreateActivityDrawer = ({
|
||||
activityObjectNameSingular,
|
||||
}: {
|
||||
activityObjectNameSingular:
|
||||
| CoreObjectNameSingular.Note
|
||||
| CoreObjectNameSingular.Task;
|
||||
}) => {
|
||||
const { openRightDrawer } = useRightDrawer();
|
||||
|
||||
const setHotkeyScope = useSetHotkeyScope();
|
||||
|
||||
const { createActivityInCache } = useCreateActivityInCache();
|
||||
const { createOneRecord: createOneActivity } = useCreateOneRecord<
|
||||
Task | Note
|
||||
>({
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const { createOneRecord: createOneActivityTarget } = useCreateOneRecord<
|
||||
TaskTarget | NoteTarget
|
||||
>({
|
||||
objectNameSingular:
|
||||
activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||
? CoreObjectNameSingular.TaskTarget
|
||||
: CoreObjectNameSingular.NoteTarget,
|
||||
});
|
||||
|
||||
const setActivityTargetableEntityArray = useSetRecoilState(
|
||||
activityTargetableEntityArrayState,
|
||||
);
|
||||
const setViewableRecordId = useSetRecoilState(viewableRecordIdState);
|
||||
|
||||
const setIsCreatingActivity = useSetRecoilState(isActivityInCreateModeState);
|
||||
|
||||
const setTemporaryActivityForEditor = useSetRecoilState(
|
||||
temporaryActivityForEditorState,
|
||||
const setViewableRecordNameSingular = useSetRecoilState(
|
||||
viewableRecordNameSingularState,
|
||||
);
|
||||
|
||||
const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState);
|
||||
|
||||
const [, setIsUpsertingActivityInDB] = useRecoilState(
|
||||
const setIsUpsertingActivityInDB = useSetRecoilState(
|
||||
isUpsertingActivityInDBState,
|
||||
);
|
||||
|
||||
const openCreateActivityDrawer = async ({
|
||||
type,
|
||||
targetableObjects,
|
||||
customAssignee,
|
||||
}: {
|
||||
type: ActivityType;
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
customAssignee?: WorkspaceMember;
|
||||
}) => {
|
||||
const { createdActivityInCache } = createActivityInCache({
|
||||
type,
|
||||
targetObject: targetableObjects[0],
|
||||
customAssignee,
|
||||
const activity = await createOneActivity({
|
||||
assigneeId: customAssignee?.id,
|
||||
});
|
||||
|
||||
const targetableObjectRelationIdName = `${targetableObjects[0].targetObjectNameSingular}Id`;
|
||||
|
||||
await createOneActivityTarget({
|
||||
taskId:
|
||||
activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||
? activity.id
|
||||
: undefined,
|
||||
noteId:
|
||||
activityObjectNameSingular === CoreObjectNameSingular.Note
|
||||
? activity.id
|
||||
: undefined,
|
||||
[targetableObjectRelationIdName]: targetableObjects[0].id,
|
||||
});
|
||||
|
||||
setActivityIdInDrawer(createdActivityInCache.id);
|
||||
setTemporaryActivityForEditor(createdActivityInCache);
|
||||
setIsCreatingActivity(true);
|
||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||
setViewableRecordId(createdActivityInCache.id);
|
||||
setViewableRecordId(activity.id);
|
||||
setViewableRecordNameSingular(activityObjectNameSingular);
|
||||
setActivityTargetableEntityArray(targetableObjects ?? []);
|
||||
openRightDrawer(RightDrawerPages.CreateActivity);
|
||||
|
||||
openRightDrawer(RightDrawerPages.ViewRecord);
|
||||
setIsUpsertingActivityInDB(false);
|
||||
};
|
||||
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
|
||||
import { findActivitiesOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { NoteTarget } from '@/activities/types/NoteTarget';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
@ -14,14 +16,18 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { sortByAscString } from '~/utils/array/sortByAscString';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const usePrepareFindManyActivitiesQuery = () => {
|
||||
export const usePrepareFindManyActivitiesQuery = ({
|
||||
activityObjectNameSingular,
|
||||
}: {
|
||||
activityObjectNameSingular: CoreObjectNameSingular;
|
||||
}) => {
|
||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const getActivityFromCache = useGetRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const cache = useApolloClient().cache;
|
||||
@ -39,7 +45,7 @@ export const usePrepareFindManyActivitiesQuery = () => {
|
||||
}: {
|
||||
additionalFilter?: Record<string, unknown>;
|
||||
targetableObject: ActivityTargetableObject;
|
||||
shouldActivityBeExcluded?: (activityTarget: Activity) => boolean;
|
||||
shouldActivityBeExcluded?: (activityTarget: Task | Note) => boolean;
|
||||
}) => {
|
||||
const targetableObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
@ -60,8 +66,10 @@ export const usePrepareFindManyActivitiesQuery = () => {
|
||||
cache,
|
||||
});
|
||||
|
||||
const activityTargets: ActivityTarget[] =
|
||||
targetableObjectRecord?.activityTargets ?? [];
|
||||
const activityTargets: (TaskTarget | NoteTarget)[] =
|
||||
targetableObjectRecord?.taskTargets ??
|
||||
targetableObjectRecord?.noteTargets ??
|
||||
[];
|
||||
|
||||
const activityTargetIds = [
|
||||
...new Set(
|
||||
@ -71,7 +79,7 @@ export const usePrepareFindManyActivitiesQuery = () => {
|
||||
),
|
||||
];
|
||||
|
||||
const activities: Activity[] = activityTargetIds
|
||||
const activities: (Task | Note)[] = activityTargetIds
|
||||
.map((activityTargetId) => {
|
||||
const activityTarget = activityTargets.find(
|
||||
(activityTarget) => activityTarget.id === activityTargetId,
|
||||
@ -81,7 +89,7 @@ export const usePrepareFindManyActivitiesQuery = () => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return getActivityFromCache<Activity>(activityTarget.activityId);
|
||||
return getActivityFromCache<Task | Note>(activityTarget.activityId);
|
||||
})
|
||||
.filter(isDefined);
|
||||
|
||||
@ -103,7 +111,10 @@ export const usePrepareFindManyActivitiesQuery = () => {
|
||||
});
|
||||
|
||||
const FIND_ACTIVITIES_OPERATION_SIGNATURE =
|
||||
findActivitiesOperationSignatureFactory({ objectMetadataItems });
|
||||
findActivitiesOperationSignatureFactory({
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
objectMetadataItems,
|
||||
});
|
||||
|
||||
upsertFindManyActivitiesInCache({
|
||||
objectRecordsToOverwrite: filteredActivities,
|
||||
|
||||
@ -1,20 +1,25 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { usePrepareFindManyActivitiesQuery } from '@/activities/hooks/usePrepareFindManyActivitiesQuery';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timelineActivities/states/objectShowPageTargetableObjectIdState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
// This hook should only be executed if the normalized cache is up-to-date
|
||||
// It will take a targetableObject and prepare the queries for the activities
|
||||
// based on the activityTargets of the targetableObject
|
||||
export const useRefreshShowPageFindManyActivitiesQueries = () => {
|
||||
export const useRefreshShowPageFindManyActivitiesQueries = ({
|
||||
activityObjectNameSingular,
|
||||
}: {
|
||||
activityObjectNameSingular: CoreObjectNameSingular;
|
||||
}) => {
|
||||
const objectShowPageTargetableObject = useRecoilValue(
|
||||
objectShowPageTargetableObjectState,
|
||||
);
|
||||
|
||||
const { prepareFindManyActivitiesQuery } =
|
||||
usePrepareFindManyActivitiesQuery();
|
||||
const { prepareFindManyActivitiesQuery } = usePrepareFindManyActivitiesQuery({
|
||||
activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const refreshShowPageFindManyActivitiesQueries = () => {
|
||||
if (isDefined(objectShowPageTargetableObject)) {
|
||||
@ -24,21 +29,12 @@ export const useRefreshShowPageFindManyActivitiesQueries = () => {
|
||||
prepareFindManyActivitiesQuery({
|
||||
targetableObject: objectShowPageTargetableObject,
|
||||
additionalFilter: {
|
||||
completedAt: { is: 'NULL' },
|
||||
type: { eq: 'Task' },
|
||||
},
|
||||
shouldActivityBeExcluded: (activity: Activity) => {
|
||||
return activity.type !== 'Task';
|
||||
status: { eq: 'TODO' },
|
||||
},
|
||||
});
|
||||
prepareFindManyActivitiesQuery({
|
||||
targetableObject: objectShowPageTargetableObject,
|
||||
additionalFilter: {
|
||||
type: { eq: 'Note' },
|
||||
},
|
||||
shouldActivityBeExcluded: (activity: Activity) => {
|
||||
return activity.type !== 'Note';
|
||||
},
|
||||
additionalFilter: {},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,50 +1,58 @@
|
||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB';
|
||||
import { useRefreshShowPageFindManyActivitiesQueries } from '@/activities/hooks/useRefreshShowPageFindManyActivitiesQueries';
|
||||
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timelineActivities/states/objectShowPageTargetableObjectIdState';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useUpsertActivity = () => {
|
||||
const [isActivityInCreateMode, setIsActivityInCreateMode] = useRecoilState(
|
||||
isActivityInCreateModeState,
|
||||
);
|
||||
export const useUpsertActivity = ({
|
||||
activityObjectNameSingular,
|
||||
}: {
|
||||
activityObjectNameSingular:
|
||||
| CoreObjectNameSingular.Task
|
||||
| CoreObjectNameSingular.Note;
|
||||
}) => {
|
||||
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
|
||||
|
||||
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Activity>({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<
|
||||
Task | Note
|
||||
>({
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const { createActivityInDB } = useCreateActivityInDB();
|
||||
const { createActivityInDB } = useCreateActivityInDB({
|
||||
activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const [, setIsUpsertingActivityInDB] = useRecoilState(
|
||||
isUpsertingActivityInDBState,
|
||||
);
|
||||
|
||||
const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState);
|
||||
|
||||
const objectShowPageTargetableObject = useRecoilValue(
|
||||
objectShowPageTargetableObjectState,
|
||||
);
|
||||
|
||||
const { refreshShowPageFindManyActivitiesQueries } =
|
||||
useRefreshShowPageFindManyActivitiesQueries();
|
||||
useRefreshShowPageFindManyActivitiesQueries({
|
||||
activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const upsertActivity = async ({
|
||||
activity,
|
||||
input,
|
||||
}: {
|
||||
activity: Activity;
|
||||
input: Partial<Activity>;
|
||||
activity: Task | Note;
|
||||
input: Partial<Task | Note>;
|
||||
}) => {
|
||||
setIsUpsertingActivityInDB(true);
|
||||
if (isActivityInCreateMode) {
|
||||
const activityToCreate: Activity = {
|
||||
const activityToCreate: Partial<Task | Note> = {
|
||||
...activity,
|
||||
...input,
|
||||
};
|
||||
@ -54,9 +62,6 @@ export const useUpsertActivity = () => {
|
||||
}
|
||||
|
||||
await createActivityInDB(activityToCreate);
|
||||
|
||||
setActivityIdInDrawer(activityToCreate.id);
|
||||
setIsActivityInCreateMode(false);
|
||||
} else {
|
||||
await updateOneActivity?.({
|
||||
idToUpdate: activity.id,
|
||||
|
||||
@ -6,10 +6,13 @@ import { v4 } from 'uuid';
|
||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||
import { ActivityTargetObjectRecordEffect } from '@/activities/inline-cell/components/ActivityTargetObjectRecordEffect';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { NoteTarget } from '@/activities/types/NoteTarget';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateManyRecordsInCache } from '@/object-record/cache/hooks/useCreateManyRecordsInCache';
|
||||
@ -35,13 +38,17 @@ const StyledSelectContainer = styled.div`
|
||||
`;
|
||||
|
||||
type ActivityTargetInlineCellEditModeProps = {
|
||||
activity: Activity;
|
||||
activity: Task | Note;
|
||||
activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[];
|
||||
activityObjectNameSingular:
|
||||
| CoreObjectNameSingular.Note
|
||||
| CoreObjectNameSingular.Task;
|
||||
};
|
||||
|
||||
export const ActivityTargetInlineCellEditMode = ({
|
||||
activity,
|
||||
activityTargetWithTargetRecords,
|
||||
activityObjectNameSingular,
|
||||
}: ActivityTargetInlineCellEditModeProps) => {
|
||||
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
|
||||
const relationPickerScopeId = `relation-picker-${activity.id}`;
|
||||
@ -53,24 +60,27 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
}),
|
||||
);
|
||||
|
||||
const { createManyRecords: createManyActivityTargets } =
|
||||
useCreateManyRecords<ActivityTarget>({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
const { createManyRecords: createManyActivityTargets } = useCreateManyRecords<
|
||||
NoteTarget | TaskTarget
|
||||
>({
|
||||
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||
});
|
||||
|
||||
const { deleteManyRecords: deleteManyActivityTargets } = useDeleteManyRecords(
|
||||
{
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||
},
|
||||
);
|
||||
|
||||
const { closeInlineCell: closeEditableField } = useInlineCell();
|
||||
|
||||
const { upsertActivity } = useUpsertActivity();
|
||||
const { upsertActivity } = useUpsertActivity({
|
||||
activityObjectNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItem: objectMetadataItemActivityTarget } =
|
||||
useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||
});
|
||||
|
||||
const setActivityFromStore = useSetRecoilState(
|
||||
@ -78,8 +88,8 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
);
|
||||
|
||||
const { createManyRecordsInCache: createManyActivityTargetsInCache } =
|
||||
useCreateManyRecordsInCache<ActivityTarget>({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
useCreateManyRecordsInCache<NoteTarget | TaskTarget>({
|
||||
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||
});
|
||||
|
||||
const handleSubmit = useRecoilCallback(
|
||||
@ -166,12 +176,19 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
const fieldNameWithIdSuffix = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: record.objectMetadataItem.nameSingular,
|
||||
});
|
||||
const newActivityTarget = prefillRecord<ActivityTarget>({
|
||||
|
||||
const newActivityTarget = prefillRecord<NoteTarget | TaskTarget>({
|
||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||
input: {
|
||||
id: newActivityTargetId,
|
||||
activityId: activity.id,
|
||||
activity,
|
||||
taskId:
|
||||
activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||
? activity.id
|
||||
: null,
|
||||
noteId:
|
||||
activityObjectNameSingular === CoreObjectNameSingular.Note
|
||||
? activity.id
|
||||
: null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
[fieldName]: record.record,
|
||||
@ -186,7 +203,11 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
activityTargets: activityTargetsAfterUpdate,
|
||||
[activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||
? 'taskTargets'
|
||||
: activityObjectNameSingular === CoreObjectNameSingular.Note
|
||||
? 'noteTargets'
|
||||
: '']: activityTargetsAfterUpdate,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@ -219,7 +240,11 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
upsertActivity({
|
||||
activity,
|
||||
input: {
|
||||
activityTargets: activityTargetsAfterUpdate,
|
||||
[activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||
? 'taskTargets'
|
||||
: activityObjectNameSingular === CoreObjectNameSingular.Note
|
||||
? 'noteTargets'
|
||||
: '']: activityTargetsAfterUpdate,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
@ -241,6 +266,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
objectMetadataItemActivityTarget,
|
||||
relationPickerScopeId,
|
||||
upsertActivity,
|
||||
activityObjectNameSingular,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@ -5,8 +5,9 @@ import { IconArrowUpRight, IconPencil } from 'twenty-ui';
|
||||
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
||||
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
||||
import { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||
@ -17,10 +18,13 @@ import { useInlineCell } from '@/object-record/record-inline-cell/hooks/useInlin
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
|
||||
type ActivityTargetsInlineCellProps = {
|
||||
activity: Activity;
|
||||
activity: Task | Note;
|
||||
showLabel?: boolean;
|
||||
maxWidth?: number;
|
||||
readonly?: boolean;
|
||||
activityObjectNameSingular:
|
||||
| CoreObjectNameSingular.Note
|
||||
| CoreObjectNameSingular.Task;
|
||||
};
|
||||
|
||||
export const ActivityTargetsInlineCell = ({
|
||||
@ -28,9 +32,13 @@ export const ActivityTargetsInlineCell = ({
|
||||
showLabel = true,
|
||||
maxWidth,
|
||||
readonly,
|
||||
activityObjectNameSingular,
|
||||
}: ActivityTargetsInlineCellProps) => {
|
||||
const { activityTargetObjectRecords } =
|
||||
useActivityTargetObjectRecords(activity);
|
||||
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
|
||||
activity,
|
||||
activityObjectNameSingular,
|
||||
);
|
||||
|
||||
const { closeInlineCell } = useInlineCell();
|
||||
|
||||
const { fieldDefinition } = useContext(FieldContext);
|
||||
@ -45,9 +53,9 @@ export const ActivityTargetsInlineCell = ({
|
||||
|
||||
const { FieldContextProvider: ActivityTargetsContextProvider } =
|
||||
useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
objectRecordId: activity.id,
|
||||
fieldMetadataName: 'activityTargets',
|
||||
fieldMetadataName: fieldDefinition.metadata.fieldName,
|
||||
fieldPosition: 3,
|
||||
overridenIsFieldEmpty: activityTargetObjectRecords.length === 0,
|
||||
});
|
||||
@ -70,6 +78,7 @@ export const ActivityTargetsInlineCell = ({
|
||||
<ActivityTargetInlineCellEditMode
|
||||
activity={activity}
|
||||
activityTargetWithTargetRecords={activityTargetObjectRecords}
|
||||
activityObjectNameSingular={activityObjectNameSingular}
|
||||
/>
|
||||
}
|
||||
label="Relations"
|
||||
|
||||
@ -1,16 +1,11 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconComment } from 'twenty-ui';
|
||||
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { getActivityPreview } from '@/activities/utils/getActivityPreview';
|
||||
import {
|
||||
FieldContext,
|
||||
GenericFieldContextType,
|
||||
} from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||
|
||||
const StyledCard = styled.div<{ isSingleNote: boolean }>`
|
||||
align-items: flex-start;
|
||||
@ -66,14 +61,6 @@ const StyledFooter = styled.div`
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(4)});
|
||||
`;
|
||||
|
||||
const StyledCommentIcon = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
margin-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const NoteCard = ({
|
||||
note,
|
||||
isSingleNote,
|
||||
@ -81,17 +68,19 @@ export const NoteCard = ({
|
||||
note: Note;
|
||||
isSingleNote: boolean;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer({
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
});
|
||||
const body = getActivityPreview(note.body);
|
||||
|
||||
const fieldContext = useMemo(
|
||||
() => ({ recoilScopeId: note?.id ?? '' }),
|
||||
[note?.id],
|
||||
);
|
||||
const { FieldContextProvider: NoteTargetsContextProvider } = useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
objectRecordId: note.id,
|
||||
fieldMetadataName: 'noteTargets',
|
||||
fieldPosition: 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<FieldContext.Provider value={fieldContext as GenericFieldContextType}>
|
||||
<StyledCard isSingleNote={isSingleNote}>
|
||||
<StyledCardDetailsContainer
|
||||
onClick={() => openActivityRightDrawer(note.id)}
|
||||
@ -100,15 +89,16 @@ export const NoteCard = ({
|
||||
<StyledCardContent>{body}</StyledCardContent>
|
||||
</StyledCardDetailsContainer>
|
||||
<StyledFooter>
|
||||
<ActivityTargetsInlineCell activity={note} readonly />
|
||||
{note.comments && note.comments.length > 0 && (
|
||||
<StyledCommentIcon>
|
||||
<IconComment size={theme.icon.size.md} />
|
||||
{note.comments.length}
|
||||
</StyledCommentIcon>
|
||||
{NoteTargetsContextProvider && (
|
||||
<NoteTargetsContextProvider>
|
||||
<ActivityTargetsInlineCell
|
||||
activity={note}
|
||||
activityObjectNameSingular={CoreObjectNameSingular.Note}
|
||||
readonly
|
||||
/>
|
||||
</NoteTargetsContextProvider>
|
||||
)}
|
||||
</StyledFooter>
|
||||
</StyledCard>
|
||||
</FieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -6,6 +6,7 @@ import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateAct
|
||||
import { NoteList } from '@/activities/notes/components/NoteList';
|
||||
import { useNotes } from '@/activities/notes/hooks/useNotes';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
|
||||
import {
|
||||
@ -31,7 +32,9 @@ export const Notes = ({
|
||||
}) => {
|
||||
const { notes, loading } = useNotes(targetableObject);
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
const openCreateActivity = useOpenCreateActivityDrawer({
|
||||
activityObjectNameSingular: CoreObjectNameSingular.Note,
|
||||
});
|
||||
|
||||
const isNotesEmpty = !notes || notes.length === 0;
|
||||
|
||||
@ -60,7 +63,6 @@ export const Notes = ({
|
||||
variant="secondary"
|
||||
onClick={() =>
|
||||
openCreateActivity({
|
||||
type: 'Note',
|
||||
targetableObjects: [targetableObject],
|
||||
})
|
||||
}
|
||||
@ -82,7 +84,6 @@ export const Notes = ({
|
||||
title="Add note"
|
||||
onClick={() =>
|
||||
openCreateActivity({
|
||||
type: 'Note',
|
||||
targetableObjects: [targetableObject],
|
||||
})
|
||||
}
|
||||
|
||||
@ -16,9 +16,7 @@ jest.mock('recoil', () => {
|
||||
...actualRecoil,
|
||||
useRecoilState: jest.fn(() => {
|
||||
const mockCurrentNotesQueryVariables = {
|
||||
filter: {
|
||||
type: { eq: 'Note' },
|
||||
},
|
||||
filter: {},
|
||||
orderBy: 'mockOrderBy',
|
||||
};
|
||||
return [mockCurrentNotesQueryVariables, jest.fn()];
|
||||
|
||||
@ -3,26 +3,26 @@ import { useRecoilState } from 'recoil';
|
||||
|
||||
import { useActivities } from '@/activities/hooks/useActivities';
|
||||
import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState';
|
||||
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FindManyTimelineActivitiesOrderBy';
|
||||
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timelineActivities/constants/FindManyTimelineActivitiesOrderBy';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { ActivityTargetableObject } from '../../types/ActivityTargetableEntity';
|
||||
|
||||
export const useNotes = (targetableObject: ActivityTargetableObject) => {
|
||||
const notesQueryVariables = useMemo(
|
||||
() =>
|
||||
({
|
||||
filter: {
|
||||
type: { eq: 'Note' },
|
||||
},
|
||||
filter: {},
|
||||
orderBy: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||
}) as RecordGqlOperationVariables,
|
||||
[],
|
||||
);
|
||||
|
||||
const { activities, loading } = useActivities({
|
||||
const { activities, loading } = useActivities<Note>({
|
||||
objectNameSingular: CoreObjectNameSingular.Note,
|
||||
activitiesFilters: notesQueryVariables.filter ?? {},
|
||||
activitiesOrderByVariables: notesQueryVariables.orderBy ?? [{}],
|
||||
targetableObjects: [targetableObject],
|
||||
|
||||
@ -1,133 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { IconTrash } from 'twenty-ui';
|
||||
|
||||
import { useRefreshShowPageFindManyActivitiesQueries } from '@/activities/hooks/useRefreshShowPageFindManyActivitiesQueries';
|
||||
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
|
||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
|
||||
import { temporaryActivityForEditorState } from '@/activities/states/temporaryActivityForEditorState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useDeleteRecordFromCache } from '@/object-record/cache/hooks/useDeleteRecordFromCache';
|
||||
import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { mapToRecordId } from '@/object-record/utils/mapToObjectId';
|
||||
import { IconButton } from '@/ui/input/button/components/IconButton';
|
||||
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
display: inline-flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const ActivityActionBar = () => {
|
||||
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
||||
const activityIdInDrawer = useRecoilValue(activityIdInDrawerState);
|
||||
|
||||
const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
|
||||
const { deleteOneRecord: deleteOneActivity } = useDeleteOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
|
||||
const { deleteManyRecords: deleteManyActivityTargets } = useDeleteManyRecords(
|
||||
{
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
},
|
||||
);
|
||||
|
||||
const [temporaryActivityForEditor, setTemporaryActivityForEditor] =
|
||||
useRecoilState(temporaryActivityForEditorState);
|
||||
|
||||
const deleteActivityFromCache = useDeleteRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
});
|
||||
const deleteActivityTargetFromCache = useDeleteRecordFromCache({
|
||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
||||
});
|
||||
|
||||
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
|
||||
const [isUpsertingActivityInDB] = useRecoilState(
|
||||
isUpsertingActivityInDBState,
|
||||
);
|
||||
|
||||
const { refreshShowPageFindManyActivitiesQueries } =
|
||||
useRefreshShowPageFindManyActivitiesQueries();
|
||||
|
||||
const deleteActivity = useRecoilCallback(
|
||||
({ snapshot }) =>
|
||||
async () => {
|
||||
if (!activityIdInDrawer) {
|
||||
throw new Error(
|
||||
'activityIdInDrawer is not defined, this should not happen',
|
||||
);
|
||||
}
|
||||
|
||||
const activity = snapshot
|
||||
.getLoadable(recordStoreFamilyState(activityIdInDrawer))
|
||||
.getValue() as Activity;
|
||||
|
||||
setIsRightDrawerOpen(false);
|
||||
|
||||
if (!isNonEmptyString(viewableRecordId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isActivityInCreateMode && isDefined(temporaryActivityForEditor)) {
|
||||
deleteActivityFromCache(temporaryActivityForEditor);
|
||||
setTemporaryActivityForEditor(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNonEmptyString(activityIdInDrawer)) {
|
||||
const activityTargetIdsToDelete: string[] =
|
||||
activity.activityTargets.map(mapToRecordId) ?? [];
|
||||
|
||||
deleteActivityFromCache(activity);
|
||||
activity.activityTargets.forEach((activityTarget: ActivityTarget) => {
|
||||
deleteActivityTargetFromCache(activityTarget);
|
||||
});
|
||||
|
||||
refreshShowPageFindManyActivitiesQueries();
|
||||
|
||||
if (isNonEmptyArray(activityTargetIdsToDelete)) {
|
||||
await deleteManyActivityTargets(activityTargetIdsToDelete);
|
||||
}
|
||||
|
||||
await deleteOneActivity?.(viewableRecordId);
|
||||
}
|
||||
},
|
||||
[
|
||||
activityIdInDrawer,
|
||||
setIsRightDrawerOpen,
|
||||
viewableRecordId,
|
||||
isActivityInCreateMode,
|
||||
temporaryActivityForEditor,
|
||||
deleteActivityFromCache,
|
||||
setTemporaryActivityForEditor,
|
||||
refreshShowPageFindManyActivitiesQueries,
|
||||
deleteOneActivity,
|
||||
deleteActivityTargetFromCache,
|
||||
deleteManyActivityTargets,
|
||||
],
|
||||
);
|
||||
|
||||
const actionsAreDisabled = isUpsertingActivityInDB;
|
||||
|
||||
return (
|
||||
<StyledButtonContainer>
|
||||
<IconButton
|
||||
Icon={IconTrash}
|
||||
onClick={deleteActivity}
|
||||
size="medium"
|
||||
variant="secondary"
|
||||
disabled={actionsAreDisabled}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
);
|
||||
};
|
||||
@ -1,39 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ActivityEditor } from '@/activities/components/ActivityEditor';
|
||||
import { ActivityEditorEffect } from '@/activities/components/ActivityEditorEffect';
|
||||
import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: space-between;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
type RightDrawerActivityProps = {
|
||||
activityId: string;
|
||||
showComment?: boolean;
|
||||
fillTitleFromBody?: boolean;
|
||||
};
|
||||
|
||||
export const RightDrawerActivity = ({
|
||||
activityId,
|
||||
showComment = false,
|
||||
fillTitleFromBody = false,
|
||||
}: RightDrawerActivityProps) => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<RecordValueSetterEffect recordId={activityId} />
|
||||
<ActivityEditorEffect activityId={activityId} />
|
||||
<ActivityEditor
|
||||
activityId={activityId}
|
||||
showComment={showComment}
|
||||
fillTitleFromBody={fillTitleFromBody}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
@ -1,21 +0,0 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
|
||||
import { RightDrawerActivity } from '../RightDrawerActivity';
|
||||
|
||||
export const RightDrawerCreateActivity = () => {
|
||||
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
||||
|
||||
return (
|
||||
<>
|
||||
{viewableRecordId && (
|
||||
<RightDrawerActivity
|
||||
activityId={viewableRecordId}
|
||||
showComment={false}
|
||||
fillTitleFromBody={true}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,17 +0,0 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||
|
||||
import { RightDrawerActivity } from '../RightDrawerActivity';
|
||||
|
||||
export const RightDrawerEditActivity = () => {
|
||||
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
||||
|
||||
return (
|
||||
<>
|
||||
{viewableRecordId && (
|
||||
<RightDrawerActivity activityId={viewableRecordId} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,6 +0,0 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const activityIdInDrawerState = createState<string | null>({
|
||||
key: 'activityIdInDrawerState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@ -1,3 +0,0 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const TasksRecoilScopeContext = createContext<string | null>(null);
|
||||
@ -1,9 +0,0 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
import { ActivityForEditor } from '@/activities/types/ActivityForEditor';
|
||||
|
||||
export const temporaryActivityForEditorState =
|
||||
createState<ActivityForEditor | null>({
|
||||
key: 'temporaryActivityForEditorState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@ -1,6 +1,5 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
||||
@ -9,7 +8,7 @@ import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWith
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { mockedTasks } from '~/testing/mock-data/activities';
|
||||
import { mockedTasks } from '~/testing/mock-data/tasks';
|
||||
|
||||
const meta: Meta<typeof TaskGroups> = {
|
||||
title: 'Modules/Activity/TaskGroups',
|
||||
@ -25,9 +24,6 @@ const meta: Meta<typeof TaskGroups> = {
|
||||
ObjectMetadataItemsDecorator,
|
||||
SnackBarDecorator,
|
||||
],
|
||||
parameters: {
|
||||
customRecoilScopeContext: TasksRecoilScopeContext,
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
@ -39,7 +35,7 @@ export const WithTasks: Story = {
|
||||
args: {
|
||||
targetableObjects: [
|
||||
{
|
||||
id: mockedTasks[0].authorId,
|
||||
id: mockedTasks[0].taskTargets?.[0].personId,
|
||||
targetObjectNameSingular: 'person',
|
||||
},
|
||||
] as ActivityTargetableObject[],
|
||||
|
||||
@ -2,168 +2,10 @@ import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator } from 'twenty-ui';
|
||||
|
||||
import { TaskList } from '@/activities/tasks/components/TaskList';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
|
||||
const workspaceMember: WorkspaceMember = {
|
||||
__typename: 'WorkspaceMember',
|
||||
id: '374fe3a5-df1e-4119-afe0-2a62a2ba481e',
|
||||
name: {
|
||||
firstName: 'Charles',
|
||||
lastName: 'Test',
|
||||
},
|
||||
avatarUrl: '',
|
||||
locale: 'en',
|
||||
createdAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
userId: 'e2409670-1088-46b4-858e-f20a598d9d0f',
|
||||
userEmail: 'charles@test.com',
|
||||
colorScheme: 'Light',
|
||||
};
|
||||
|
||||
const mockedActivities: Array<Activity> = [
|
||||
{
|
||||
id: '3ecaa1be-aac7-463a-a38e-64078dd451d5',
|
||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
reminderAt: null,
|
||||
title: 'My very first note',
|
||||
type: 'Note',
|
||||
body: '',
|
||||
dueAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
completedAt: null,
|
||||
author: workspaceMember,
|
||||
assignee: workspaceMember,
|
||||
assigneeId: workspaceMember.id,
|
||||
authorId: workspaceMember.id,
|
||||
comments: [],
|
||||
activityTargets: [
|
||||
{
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
|
||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
targetObjectNameSingular: 'company',
|
||||
personId: null,
|
||||
companyId: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
|
||||
company: {
|
||||
__typename: 'Company',
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
|
||||
name: 'Airbnb',
|
||||
domainName: 'airbnb.com',
|
||||
},
|
||||
person: null,
|
||||
activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
|
||||
activity: {
|
||||
__typename: 'Activity',
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
|
||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
},
|
||||
__typename: 'ActivityTarget',
|
||||
},
|
||||
{
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb301',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
targetObjectNameSingular: 'company',
|
||||
personId: null,
|
||||
companyId: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae',
|
||||
company: {
|
||||
__typename: 'Company',
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278',
|
||||
name: 'Aircall',
|
||||
domainName: 'aircall.io',
|
||||
},
|
||||
person: null,
|
||||
activityId: 'b396e6b9-dc5c-4643-bcff-61b6cf7523ae',
|
||||
activity: {
|
||||
__typename: 'Activity',
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb231',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
__typename: 'ActivityTarget',
|
||||
},
|
||||
],
|
||||
__typename: 'Activity',
|
||||
},
|
||||
{
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278a',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
reminderAt: null,
|
||||
title: 'Another note',
|
||||
body: '',
|
||||
type: 'Note',
|
||||
completedAt: null,
|
||||
dueAt: '2029-08-26T10:12:42.33625+00:00',
|
||||
author: {
|
||||
...workspaceMember,
|
||||
},
|
||||
assignee: { ...workspaceMember },
|
||||
assigneeId: workspaceMember.id,
|
||||
authorId: workspaceMember.id,
|
||||
comments: [],
|
||||
activityTargets: [
|
||||
{
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278t',
|
||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
targetObjectNameSingular: 'person',
|
||||
personId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b', // Alexandre
|
||||
person: {
|
||||
__typename: 'Person',
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6b',
|
||||
name: {
|
||||
firstName: 'Alexandre',
|
||||
lastName: 'Test',
|
||||
},
|
||||
avatarUrl: '',
|
||||
},
|
||||
company: null,
|
||||
companyId: null,
|
||||
activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb278a',
|
||||
activity: {
|
||||
__typename: 'Activity',
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278a',
|
||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||
},
|
||||
__typename: 'ActivityTarget',
|
||||
},
|
||||
{
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb279t',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
personId: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d', // Jean d'Eau
|
||||
companyId: null,
|
||||
targetObjectNameSingular: 'person',
|
||||
company: null,
|
||||
person: {
|
||||
__typename: 'Person',
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6d',
|
||||
name: {
|
||||
firstName: 'Jean',
|
||||
lastName: "d'Eau",
|
||||
},
|
||||
avatarUrl: '',
|
||||
},
|
||||
activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb278a',
|
||||
activity: {
|
||||
__typename: 'Activity',
|
||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb278a',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
__typename: 'ActivityTarget',
|
||||
},
|
||||
],
|
||||
__typename: 'Activity',
|
||||
},
|
||||
];
|
||||
import { mockedTasks } from '~/testing/mock-data/tasks';
|
||||
|
||||
const meta: Meta<typeof TaskList> = {
|
||||
title: 'Modules/Activity/TaskList',
|
||||
@ -171,7 +13,7 @@ const meta: Meta<typeof TaskList> = {
|
||||
decorators: [MemoryRouterDecorator, ComponentDecorator, SnackBarDecorator],
|
||||
args: {
|
||||
title: 'Tasks',
|
||||
tasks: mockedActivities,
|
||||
tasks: mockedTasks,
|
||||
},
|
||||
parameters: {
|
||||
msw: graphqlMocks,
|
||||
@ -184,6 +26,6 @@ type Story = StoryObj<typeof TaskList>;
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
title: 'Tasks',
|
||||
tasks: mockedActivities,
|
||||
tasks: mockedTasks,
|
||||
},
|
||||
};
|
||||
|
||||
@ -3,6 +3,7 @@ import { IconPlus } from 'twenty-ui';
|
||||
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
|
||||
export const AddTaskButton = ({
|
||||
@ -10,7 +11,9 @@ export const AddTaskButton = ({
|
||||
}: {
|
||||
activityTargetableObjects?: ActivityTargetableObject[];
|
||||
}) => {
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
const openCreateActivity = useOpenCreateActivityDrawer({
|
||||
activityObjectNameSingular: CoreObjectNameSingular.Task,
|
||||
});
|
||||
|
||||
if (!isNonEmptyArray(activityTargetableObjects)) {
|
||||
return <></>;
|
||||
@ -24,7 +27,6 @@ export const AddTaskButton = ({
|
||||
title="Add task"
|
||||
onClick={() =>
|
||||
openCreateActivity({
|
||||
type: 'Task',
|
||||
targetableObjects: activityTargetableObjects,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentUserDueTaskCountState } from '@/activities/tasks/states/currentUserTaskCountState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { parseDate } from '~/utils/date-utils';
|
||||
|
||||
export const CurrentUserDueTaskCountEffect = () => {
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const [currentUserDueTaskCount, setCurrentUserDueTaskCount] = useRecoilState(
|
||||
currentUserDueTaskCountState,
|
||||
);
|
||||
|
||||
const { records: tasks } = useFindManyRecords<Activity>({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
filter: {
|
||||
type: { eq: 'Task' },
|
||||
completedAt: { is: 'NULL' },
|
||||
assigneeId: { eq: currentWorkspaceMember?.id },
|
||||
},
|
||||
});
|
||||
|
||||
const computedCurrentUserDueTaskCount = tasks.filter((task) => {
|
||||
if (!task.dueAt) {
|
||||
return false;
|
||||
}
|
||||
const dueDate = parseDate(task.dueAt).toJSDate();
|
||||
const today = DateTime.now().endOf('day').toJSDate();
|
||||
return dueDate <= today;
|
||||
}).length;
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUserDueTaskCount !== computedCurrentUserDueTaskCount) {
|
||||
setCurrentUserDueTaskCount(computedCurrentUserDueTaskCount);
|
||||
}
|
||||
}, [
|
||||
computedCurrentUserDueTaskCount,
|
||||
currentUserDueTaskCount,
|
||||
setCurrentUserDueTaskCount,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -1,10 +1,8 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
@ -21,11 +19,9 @@ export const ObjectTasks = ({
|
||||
}) => {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<RecoilScope CustomRecoilScopeContext={TasksRecoilScopeContext}>
|
||||
<ObjectFilterDropdownScope filterScopeId="entity-tasks-filter-scope">
|
||||
<TaskGroups targetableObjects={[targetableObject]} showAddButton />
|
||||
</ObjectFilterDropdownScope>
|
||||
</RecoilScope>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,14 +1,16 @@
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
|
||||
|
||||
export const PageAddTaskButton = () => {
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
const openCreateActivity = useOpenCreateActivityDrawer({
|
||||
activityObjectNameSingular: CoreObjectNameSingular.Task,
|
||||
});
|
||||
|
||||
// TODO: fetch workspace member from filter here
|
||||
|
||||
const handleClick = () => {
|
||||
openCreateActivity({
|
||||
type: 'Task',
|
||||
targetableObjects: [],
|
||||
});
|
||||
};
|
||||
|
||||
@ -18,6 +18,9 @@ import {
|
||||
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import groupBy from 'lodash.groupby';
|
||||
import { AddTaskButton } from './AddTaskButton';
|
||||
import { TaskList } from './TaskList';
|
||||
|
||||
@ -33,37 +36,27 @@ type TaskGroupsProps = {
|
||||
};
|
||||
|
||||
export const TaskGroups = ({
|
||||
filterDropdownId,
|
||||
targetableObjects,
|
||||
showAddButton,
|
||||
}: TaskGroupsProps) => {
|
||||
const {
|
||||
todayOrPreviousTasks,
|
||||
upcomingTasks,
|
||||
unscheduledTasks,
|
||||
completedTasks,
|
||||
incompleteTasksLoading,
|
||||
completeTasksLoading,
|
||||
} = useTasks({
|
||||
filterDropdownId: filterDropdownId,
|
||||
const { tasks, tasksLoading } = useTasks({
|
||||
targetableObjects: targetableObjects ?? [],
|
||||
});
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
const openCreateActivity = useOpenCreateActivityDrawer({
|
||||
activityObjectNameSingular: CoreObjectNameSingular.Task,
|
||||
});
|
||||
|
||||
const { activeTabIdState } = useTabList(TASKS_TAB_LIST_COMPONENT_ID);
|
||||
const activeTabId = useRecoilValue(activeTabIdState);
|
||||
|
||||
const isLoading =
|
||||
(activeTabId !== 'done' && incompleteTasksLoading) ||
|
||||
(activeTabId === 'done' && completeTasksLoading);
|
||||
(activeTabId !== 'done' && tasksLoading) ||
|
||||
(activeTabId === 'done' && tasksLoading);
|
||||
|
||||
const isTasksEmpty =
|
||||
(activeTabId !== 'done' &&
|
||||
todayOrPreviousTasks?.length === 0 &&
|
||||
upcomingTasks?.length === 0 &&
|
||||
unscheduledTasks?.length === 0) ||
|
||||
(activeTabId === 'done' && completedTasks?.length === 0);
|
||||
(activeTabId !== 'done' && tasks?.length === 0) ||
|
||||
(activeTabId === 'done' && tasks?.length === 0);
|
||||
|
||||
if (isLoading && isTasksEmpty) {
|
||||
return <SkeletonLoader />;
|
||||
@ -90,7 +83,6 @@ export const TaskGroups = ({
|
||||
variant={'secondary'}
|
||||
onClick={() =>
|
||||
openCreateActivity({
|
||||
type: 'Task',
|
||||
targetableObjects: targetableObjects ?? [],
|
||||
})
|
||||
}
|
||||
@ -101,48 +93,19 @@ export const TaskGroups = ({
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{activeTabId === 'done' ? (
|
||||
{Object.entries(groupBy(tasks, ({ status }) => status)).map(
|
||||
([status, tasksByStatus]: [string, Task[]]) => (
|
||||
<TaskList
|
||||
tasks={completedTasks ?? []}
|
||||
key={status}
|
||||
title={status}
|
||||
tasks={tasksByStatus}
|
||||
button={
|
||||
showAddButton && (
|
||||
<AddTaskButton activityTargetableObjects={targetableObjects} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<TaskList
|
||||
title="Today"
|
||||
tasks={todayOrPreviousTasks ?? []}
|
||||
button={
|
||||
showAddButton && (
|
||||
<AddTaskButton activityTargetableObjects={targetableObjects} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<TaskList
|
||||
title="Upcoming"
|
||||
tasks={upcomingTasks ?? []}
|
||||
button={
|
||||
showAddButton &&
|
||||
!todayOrPreviousTasks?.length && (
|
||||
<AddTaskButton activityTargetableObjects={targetableObjects} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<TaskList
|
||||
title="Unscheduled"
|
||||
tasks={unscheduledTasks ?? []}
|
||||
button={
|
||||
showAddButton &&
|
||||
!todayOrPreviousTasks?.length &&
|
||||
!upcomingTasks?.length && (
|
||||
<AddTaskButton activityTargetableObjects={targetableObjects} />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
import { ReactElement } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { TaskRow } from './TaskRow';
|
||||
|
||||
type TaskListProps = {
|
||||
title?: string;
|
||||
tasks: Activity[];
|
||||
tasks: Task[];
|
||||
button?: ReactElement | false;
|
||||
};
|
||||
|
||||
|
||||
@ -1,18 +1,16 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
IconCalendar,
|
||||
IconComment,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui';
|
||||
import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui';
|
||||
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
|
||||
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
|
||||
import { beautifyExactDate, hasDatePassed } from '~/utils/date-utils';
|
||||
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||
import { useCompleteTask } from '../hooks/useCompleteTask';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -52,13 +50,6 @@ const StyledTaskTitle = styled.div<{
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const StyledCommentIcon = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: flex;
|
||||
margin-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledDueDate = styled.div<{
|
||||
isPast: boolean;
|
||||
}>`
|
||||
@ -89,13 +80,22 @@ const StyledCheckboxContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const TaskRow = ({ task }: { task: Activity }) => {
|
||||
export const TaskRow = ({ task }: { task: Task }) => {
|
||||
const theme = useTheme();
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer({
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
});
|
||||
|
||||
const body = getActivitySummary(task.body);
|
||||
const { completeTask } = useCompleteTask(task);
|
||||
|
||||
const { FieldContextProvider: TaskTargetsContextProvider } = useFieldContext({
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
objectRecordId: task.id,
|
||||
fieldMetadataName: 'taskTargets',
|
||||
fieldPosition: 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledContainer
|
||||
onClick={() => {
|
||||
@ -109,33 +109,33 @@ export const TaskRow = ({ task }: { task: Activity }) => {
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
checked={!!task.completedAt}
|
||||
checked={task.status === 'DONE'}
|
||||
shape={CheckboxShape.Rounded}
|
||||
onCheckedChange={completeTask}
|
||||
/>
|
||||
</StyledCheckboxContainer>
|
||||
<StyledTaskTitle completed={task.completedAt !== null}>
|
||||
<StyledTaskTitle completed={task.status === 'DONE'}>
|
||||
{task.title || <StyledPlaceholder>Task title</StyledPlaceholder>}
|
||||
</StyledTaskTitle>
|
||||
<StyledTaskBody>
|
||||
<OverflowingTextWithTooltip text={body} />
|
||||
{task.comments && task.comments.length > 0 && (
|
||||
<StyledCommentIcon>
|
||||
<IconComment size={theme.icon.size.md} />
|
||||
</StyledCommentIcon>
|
||||
)}
|
||||
</StyledTaskBody>
|
||||
</StyledLeftSideContainer>
|
||||
<StyledRightSideContainer>
|
||||
{TaskTargetsContextProvider && (
|
||||
<TaskTargetsContextProvider>
|
||||
<ActivityTargetsInlineCell
|
||||
activityObjectNameSingular={CoreObjectNameSingular.Task}
|
||||
activity={task}
|
||||
showLabel={false}
|
||||
maxWidth={200}
|
||||
readonly
|
||||
/>
|
||||
</TaskTargetsContextProvider>
|
||||
)}
|
||||
<StyledDueDate
|
||||
isPast={
|
||||
!!task.dueAt && hasDatePassed(task.dueAt) && !task.completedAt
|
||||
!!task.dueAt && hasDatePassed(task.dueAt) && task.status === 'TODO'
|
||||
}
|
||||
>
|
||||
<IconCalendar size={theme.icon.size.md} />
|
||||
|
||||
@ -1,16 +1,25 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import gql from 'graphql-tag';
|
||||
import { ReactNode } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useCompleteTask } from '@/activities/tasks/hooks/useCompleteTask';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
|
||||
const task = { id: '123', completedAt: '2024-03-15T07:33:14.212Z' };
|
||||
|
||||
const mockedDate = task.completedAt;
|
||||
const toISOStringMock = jest.fn(() => mockedDate);
|
||||
global.Date.prototype.toISOString = toISOStringMock;
|
||||
const task: Task = {
|
||||
id: '123',
|
||||
status: null,
|
||||
title: 'Test',
|
||||
body: 'Test',
|
||||
dueAt: '2024-03-15T07:33:14.212Z',
|
||||
createdAt: '2024-03-15T07:33:14.212Z',
|
||||
updatedAt: '2024-03-15T07:33:14.212Z',
|
||||
assignee: null,
|
||||
assigneeId: null,
|
||||
taskTargets: [],
|
||||
__typename: 'Task',
|
||||
};
|
||||
|
||||
const mocks: MockedResponse[] = [
|
||||
{
|
||||
@ -26,7 +35,7 @@ const mocks: MockedResponse[] = [
|
||||
reminderAt
|
||||
authorId
|
||||
title
|
||||
completedAt
|
||||
status
|
||||
updatedAt
|
||||
body
|
||||
dueAt
|
||||
@ -38,7 +47,7 @@ const mocks: MockedResponse[] = [
|
||||
`,
|
||||
variables: {
|
||||
idToUpdate: task.id,
|
||||
input: { completedAt: task.completedAt },
|
||||
input: { status: task.status },
|
||||
},
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
@ -49,11 +58,11 @@ const mocks: MockedResponse[] = [
|
||||
reminderAt: null,
|
||||
authorId: '123',
|
||||
title: 'Test',
|
||||
completedAt: '2024-03-15T07:33:14.212Z',
|
||||
status: 'DONE',
|
||||
updatedAt: '2024-03-15T07:33:14.212Z',
|
||||
body: 'Test',
|
||||
dueAt: '2024-03-15T07:33:14.212Z',
|
||||
type: 'Task',
|
||||
type: 'TASK',
|
||||
id: '123',
|
||||
assigneeId: '123',
|
||||
},
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useCurrentUserTaskCount } from '@/activities/tasks/hooks/useCurrentUserDueTaskCount';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { mockedActivities } from '~/testing/mock-data/activities';
|
||||
|
||||
const useFindManyRecordsMock = jest.fn(() => ({
|
||||
records: [...mockedActivities, { id: '2' }],
|
||||
}));
|
||||
|
||||
jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
|
||||
useFindManyRecords: jest.fn(),
|
||||
}));
|
||||
|
||||
(useFindManyRecords as jest.Mock).mockImplementation(useFindManyRecordsMock);
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>{children}</RecoilRoot>
|
||||
);
|
||||
|
||||
describe('useCurrentUserTaskCount', () => {
|
||||
it('should return the current user task count', async () => {
|
||||
const { result } = renderHook(() => useCurrentUserTaskCount(), {
|
||||
wrapper: Wrapper,
|
||||
});
|
||||
|
||||
expect(result.current.currentUserDueTaskCount).toBe(1);
|
||||
});
|
||||
});
|
||||
@ -1,33 +1,27 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useActivities } from '@/activities/hooks/useActivities';
|
||||
import { useTasks } from '@/activities/tasks/hooks/useTasks';
|
||||
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
||||
|
||||
const completedTasks = [
|
||||
const tasks = [
|
||||
{
|
||||
id: '1',
|
||||
completedAt: '2024-03-15T07:33:14.212Z',
|
||||
status: 'DONE',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
completedAt: '2024-03-15T07:33:14.212Z',
|
||||
status: 'DONE',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
completedAt: '2024-03-15T07:33:14.212Z',
|
||||
status: 'DONE',
|
||||
},
|
||||
];
|
||||
|
||||
const unscheduledTasks = [
|
||||
{
|
||||
id: '4',
|
||||
},
|
||||
];
|
||||
|
||||
const todayOrPreviousTasks = [
|
||||
{
|
||||
id: '5',
|
||||
dueAt: '2024-03-15T07:33:14.212Z',
|
||||
@ -38,20 +32,11 @@ const todayOrPreviousTasks = [
|
||||
},
|
||||
];
|
||||
|
||||
const useActivitiesMock = jest.fn(
|
||||
({
|
||||
activitiesFilters,
|
||||
}: {
|
||||
activitiesFilters: { completedAt: { is: 'NULL' | 'NOT_NULL' } };
|
||||
}) => {
|
||||
const isCompletedFilter = activitiesFilters.completedAt.is === 'NOT_NULL';
|
||||
const useActivitiesMock = jest.fn(() => {
|
||||
return {
|
||||
activities: isCompletedFilter
|
||||
? completedTasks
|
||||
: [...todayOrPreviousTasks, ...unscheduledTasks],
|
||||
activities: tasks,
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
jest.mock('@/activities/hooks/useActivities', () => ({
|
||||
useActivities: jest.fn(),
|
||||
@ -74,10 +59,7 @@ describe('useTasks', () => {
|
||||
});
|
||||
|
||||
expect(result.current).toEqual({
|
||||
todayOrPreviousTasks,
|
||||
upcomingTasks: [],
|
||||
unscheduledTasks,
|
||||
completedTasks,
|
||||
tasks: tasks,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,23 +1,21 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
|
||||
type Task = Pick<Activity, 'id' | 'completedAt'>;
|
||||
|
||||
export const useCompleteTask = (task: Task) => {
|
||||
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Activity>({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Task>({
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
});
|
||||
|
||||
const completeTask = useCallback(
|
||||
async (value: boolean) => {
|
||||
const completedAt = value ? new Date().toISOString() : null;
|
||||
const status = value ? 'DONE' : 'TODO';
|
||||
await updateOneActivity?.({
|
||||
idToUpdate: task.id,
|
||||
updateOneRecordInput: {
|
||||
completedAt,
|
||||
status,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
import { DateTime } from 'luxon';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { parseDate } from '~/utils/date-utils';
|
||||
|
||||
export const useCurrentUserTaskCount = () => {
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const { records: tasks } = useFindManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
filter: {
|
||||
type: { eq: 'Task' },
|
||||
completedAt: { is: 'NULL' },
|
||||
assigneeId: { eq: currentWorkspaceMember?.id },
|
||||
},
|
||||
});
|
||||
|
||||
const currentUserDueTaskCount = tasks.filter((task) => {
|
||||
if (!task.dueAt) {
|
||||
return false;
|
||||
}
|
||||
const dueDate = parseDate(task.dueAt).toJSDate();
|
||||
const today = DateTime.now().endOf('day').toJSDate();
|
||||
return dueDate <= today;
|
||||
}).length;
|
||||
|
||||
return {
|
||||
currentUserDueTaskCount,
|
||||
};
|
||||
};
|
||||
@ -1,156 +1,23 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
|
||||
import { useActivities } from '@/activities/hooks/useActivities';
|
||||
import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState';
|
||||
import { currentIncompleteTaskQueryVariablesState } from '@/activities/tasks/states/currentIncompleteTaskQueryVariablesState';
|
||||
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FindManyTimelineActivitiesOrderBy';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timelineActivities/constants/FindManyTimelineActivitiesOrderBy';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
|
||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
||||
import { parseDate } from '~/utils/date-utils';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
|
||||
type UseTasksProps = {
|
||||
filterDropdownId?: string;
|
||||
targetableObjects: ActivityTargetableObject[];
|
||||
};
|
||||
|
||||
export const useTasks = ({
|
||||
export const useTasks = ({ targetableObjects }: UseTasksProps) => {
|
||||
const { activities: tasks, loading: tasksLoading } = useActivities<Task>({
|
||||
objectNameSingular: CoreObjectNameSingular.Task,
|
||||
targetableObjects,
|
||||
filterDropdownId,
|
||||
}: UseTasksProps) => {
|
||||
const { selectedFilterState } = useFilterDropdown({
|
||||
filterDropdownId,
|
||||
activitiesFilters: {},
|
||||
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||
});
|
||||
|
||||
const selectedFilter = useRecoilValue(selectedFilterState);
|
||||
|
||||
const assigneeIdFilter = useMemo(
|
||||
() =>
|
||||
selectedFilter
|
||||
? {
|
||||
assigneeId: {
|
||||
in: JSON.parse(selectedFilter.value),
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
[selectedFilter],
|
||||
);
|
||||
|
||||
const completedQueryVariables = useMemo(
|
||||
() =>
|
||||
({
|
||||
filter: {
|
||||
completedAt: { is: 'NOT_NULL' },
|
||||
type: { eq: 'Task' },
|
||||
...assigneeIdFilter,
|
||||
},
|
||||
orderBy: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||
}) as RecordGqlOperationVariables,
|
||||
[assigneeIdFilter],
|
||||
);
|
||||
|
||||
const incompleteQueryVariables = useMemo(
|
||||
() =>
|
||||
({
|
||||
filter: {
|
||||
completedAt: { is: 'NULL' },
|
||||
type: { eq: 'Task' },
|
||||
...assigneeIdFilter,
|
||||
},
|
||||
orderBy: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||
}) as RecordGqlOperationVariables,
|
||||
[assigneeIdFilter],
|
||||
);
|
||||
|
||||
const [
|
||||
currentCompletedTaskQueryVariables,
|
||||
setCurrentCompletedTaskQueryVariables,
|
||||
] = useRecoilState(currentCompletedTaskQueryVariablesState);
|
||||
|
||||
const [
|
||||
currentIncompleteTaskQueryVariables,
|
||||
setCurrentIncompleteTaskQueryVariables,
|
||||
] = useRecoilState(currentIncompleteTaskQueryVariablesState);
|
||||
|
||||
// TODO: fix useEffect, remove with better pattern
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
completedQueryVariables,
|
||||
currentCompletedTaskQueryVariables,
|
||||
)
|
||||
) {
|
||||
setCurrentCompletedTaskQueryVariables(completedQueryVariables);
|
||||
}
|
||||
}, [
|
||||
completedQueryVariables,
|
||||
currentCompletedTaskQueryVariables,
|
||||
setCurrentCompletedTaskQueryVariables,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
incompleteQueryVariables,
|
||||
currentIncompleteTaskQueryVariables,
|
||||
)
|
||||
) {
|
||||
setCurrentIncompleteTaskQueryVariables(incompleteQueryVariables);
|
||||
}
|
||||
}, [
|
||||
incompleteQueryVariables,
|
||||
currentIncompleteTaskQueryVariables,
|
||||
setCurrentIncompleteTaskQueryVariables,
|
||||
]);
|
||||
|
||||
const { activities: completeTasksData, loading: completeTasksLoading } =
|
||||
useActivities({
|
||||
targetableObjects,
|
||||
activitiesFilters: completedQueryVariables.filter ?? {},
|
||||
activitiesOrderByVariables: completedQueryVariables.orderBy ?? [{}],
|
||||
});
|
||||
|
||||
const { activities: incompleteTaskData, loading: incompleteTasksLoading } =
|
||||
useActivities({
|
||||
targetableObjects,
|
||||
activitiesFilters: incompleteQueryVariables.filter ?? {},
|
||||
activitiesOrderByVariables: incompleteQueryVariables.orderBy ?? [{}],
|
||||
});
|
||||
|
||||
const todayOrPreviousTasks = incompleteTaskData?.filter((task) => {
|
||||
if (!task.dueAt) {
|
||||
return false;
|
||||
}
|
||||
const dueDate = parseDate(task.dueAt).toJSDate();
|
||||
const today = DateTime.now().endOf('day').toJSDate();
|
||||
return dueDate <= today;
|
||||
});
|
||||
|
||||
const upcomingTasks = incompleteTaskData?.filter((task) => {
|
||||
if (!task.dueAt) {
|
||||
return false;
|
||||
}
|
||||
const dueDate = parseDate(task.dueAt).toJSDate();
|
||||
const today = DateTime.now().endOf('day').toJSDate();
|
||||
return dueDate > today;
|
||||
});
|
||||
|
||||
const unscheduledTasks = incompleteTaskData?.filter((task) => {
|
||||
return !task.dueAt;
|
||||
});
|
||||
|
||||
const completedTasks = completeTasksData;
|
||||
|
||||
return {
|
||||
todayOrPreviousTasks: (todayOrPreviousTasks ?? []) as Activity[],
|
||||
upcomingTasks: (upcomingTasks ?? []) as Activity[],
|
||||
unscheduledTasks: (unscheduledTasks ?? []) as Activity[],
|
||||
completedTasks: (completedTasks ?? []) as Activity[],
|
||||
completeTasksLoading,
|
||||
incompleteTasksLoading,
|
||||
tasks: (tasks ?? []) as Task[],
|
||||
tasksLoading,
|
||||
};
|
||||
};
|
||||
|
||||
@ -1,218 +0,0 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { AppTooltip, Avatar, IconCheckbox, IconNotes } from 'twenty-ui';
|
||||
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { timelineActivityWithoutTargetsFamilyState } from '@/activities/timeline/states/timelineActivityWithoutTargetsFamilyState';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
import {
|
||||
beautifyExactDateTime,
|
||||
beautifyPastDateRelativeToNow,
|
||||
} from '~/utils/date-utils';
|
||||
|
||||
const StyledAvatarContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
height: 26px;
|
||||
justify-content: center;
|
||||
user-select: none;
|
||||
width: 26px;
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
height: 16px;
|
||||
justify-content: center;
|
||||
text-decoration-line: underline;
|
||||
width: 16px;
|
||||
`;
|
||||
|
||||
const StyledActivityTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledActivityLink = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
overflow: hidden;
|
||||
text-decoration-line: underline;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const StyledItemContainer = styled.div`
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
span {
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
}
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledItemTitleContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-flow: row ${() => (useIsMobile() ? 'wrap' : 'nowrap')};
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledItemAuthorText = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledItemTitle = styled.div`
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledItemTitleDate = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
justify-content: flex-end;
|
||||
margin-left: auto;
|
||||
`;
|
||||
|
||||
const StyledVerticalLineContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
const StyledVerticalLine = styled.div`
|
||||
align-self: stretch;
|
||||
background: ${({ theme }) => theme.border.color.light};
|
||||
flex-shrink: 0;
|
||||
width: 2px;
|
||||
`;
|
||||
|
||||
const StyledTimelineItemContainer = styled.div<{ isGap?: boolean }>`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
height: ${({ isGap, theme }) =>
|
||||
isGap ? (useIsMobile() ? theme.spacing(6) : theme.spacing(3)) : 'auto'};
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
type TimelineActivityProps = {
|
||||
isLastActivity?: boolean;
|
||||
activityId: string;
|
||||
};
|
||||
|
||||
export const TimelineActivity = ({
|
||||
isLastActivity,
|
||||
activityId,
|
||||
}: TimelineActivityProps) => {
|
||||
const activityForTimeline = useRecoilValue(
|
||||
timelineActivityWithoutTargetsFamilyState(activityId),
|
||||
);
|
||||
|
||||
const beautifiedCreatedAt = activityForTimeline
|
||||
? beautifyPastDateRelativeToNow(activityForTimeline.createdAt)
|
||||
: '';
|
||||
const exactCreatedAt = activityForTimeline
|
||||
? beautifyExactDateTime(activityForTimeline.createdAt)
|
||||
: '';
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||
const theme = useTheme();
|
||||
|
||||
const activityFromStore = useRecoilValue(
|
||||
recordStoreFamilyState(activityForTimeline?.id ?? ''),
|
||||
);
|
||||
|
||||
if (!activityForTimeline) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTimelineItemContainer>
|
||||
<StyledAvatarContainer>
|
||||
<Avatar
|
||||
avatarUrl={activityForTimeline.author?.avatarUrl}
|
||||
placeholder={activityForTimeline.author?.name.firstName ?? ''}
|
||||
size="sm"
|
||||
type="rounded"
|
||||
/>
|
||||
</StyledAvatarContainer>
|
||||
<StyledItemContainer>
|
||||
<StyledItemTitleContainer>
|
||||
<StyledItemAuthorText>
|
||||
<span>
|
||||
{activityForTimeline.author?.name.firstName}{' '}
|
||||
{activityForTimeline.author?.name.lastName}
|
||||
</span>
|
||||
created a {activityForTimeline.type.toLowerCase()}
|
||||
</StyledItemAuthorText>
|
||||
<StyledItemTitle>
|
||||
<StyledIconContainer>
|
||||
{activityForTimeline.type === 'Note' && (
|
||||
<IconNotes size={theme.icon.size.sm} />
|
||||
)}
|
||||
{activityForTimeline.type === 'Task' && (
|
||||
<IconCheckbox size={theme.icon.size.sm} />
|
||||
)}
|
||||
</StyledIconContainer>
|
||||
{(activityForTimeline.type === 'Note' ||
|
||||
activityForTimeline.type === 'Task') && (
|
||||
<StyledActivityTitle
|
||||
onClick={() =>
|
||||
openActivityRightDrawer(activityForTimeline.id)
|
||||
}
|
||||
>
|
||||
“
|
||||
<StyledActivityLink
|
||||
title={activityFromStore?.title ?? '(No Title)'}
|
||||
>
|
||||
{activityFromStore?.title ?? '(No Title)'}
|
||||
</StyledActivityLink>
|
||||
“
|
||||
</StyledActivityTitle>
|
||||
)}
|
||||
</StyledItemTitle>
|
||||
</StyledItemTitleContainer>
|
||||
<StyledItemTitleDate id={`id-${activityForTimeline.id}`}>
|
||||
{beautifiedCreatedAt}
|
||||
</StyledItemTitleDate>
|
||||
<AppTooltip
|
||||
anchorSelect={`#id-${activityForTimeline.id}`}
|
||||
content={exactCreatedAt}
|
||||
clickable
|
||||
noArrow
|
||||
/>
|
||||
</StyledItemContainer>
|
||||
</StyledTimelineItemContainer>
|
||||
{!isLastActivity && (
|
||||
<StyledTimelineItemContainer isGap>
|
||||
<StyledVerticalLineContainer>
|
||||
<StyledVerticalLine></StyledVerticalLine>
|
||||
</StyledVerticalLineContainer>
|
||||
</StyledTimelineItemContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,62 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui';
|
||||
|
||||
import { ActivityType } from '@/activities/types/Activity';
|
||||
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.lg};
|
||||
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTitleText = styled.div<{
|
||||
completed?: boolean;
|
||||
hasCheckbox?: boolean;
|
||||
}>`
|
||||
text-decoration: ${({ completed }) => (completed ? 'line-through' : 'none')};
|
||||
width: ${({ hasCheckbox, theme }) =>
|
||||
!hasCheckbox ? '100%;' : `calc(100% - ${theme.spacing(5)});`};
|
||||
`;
|
||||
|
||||
const StyledCheckboxContainer = styled.div<{ hasCheckbox?: boolean }>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
type TimelineActivityTitleProps = {
|
||||
title: string;
|
||||
completed?: boolean;
|
||||
type: ActivityType;
|
||||
onCompletionChange?: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const TimelineActivityTitle = ({
|
||||
title,
|
||||
completed,
|
||||
type,
|
||||
onCompletionChange,
|
||||
}: TimelineActivityTitleProps) => (
|
||||
<StyledTitleContainer>
|
||||
{type === 'Task' && (
|
||||
<StyledCheckboxContainer
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
onCompletionChange?.(!completed);
|
||||
}}
|
||||
>
|
||||
<Checkbox checked={completed ?? false} shape={CheckboxShape.Rounded} />
|
||||
</StyledCheckboxContainer>
|
||||
)}
|
||||
<StyledTitleText completed={completed} hasCheckbox={type === 'Task'}>
|
||||
<OverflowingTextWithTooltip text={title ? title : 'Task title'} />
|
||||
</StyledTitleText>
|
||||
</StyledTitleContainer>
|
||||
);
|
||||
@ -1,55 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { timelineActivitiesForGroupState } from '@/activities/timeline/states/timelineActivitiesForGroupState';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
|
||||
import { groupActivitiesByMonth } from '../utils/groupActivitiesByMonth';
|
||||
|
||||
import { TimelineActivityGroup } from './TimelingeActivityGroup';
|
||||
|
||||
const StyledTimelineContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
justify-content: flex-start;
|
||||
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(8)});
|
||||
`;
|
||||
|
||||
const StyledScrollWrapper = styled(ScrollWrapper)``;
|
||||
|
||||
export const TimelineItemsContainer = () => {
|
||||
const timelineActivitiesForGroup = useRecoilValue(
|
||||
timelineActivitiesForGroupState,
|
||||
);
|
||||
|
||||
const groupedActivities = groupActivitiesByMonth(timelineActivitiesForGroup);
|
||||
|
||||
return (
|
||||
<StyledScrollWrapper>
|
||||
<StyledTimelineContainer>
|
||||
{groupedActivities.map((group, index) => (
|
||||
<TimelineActivityGroup
|
||||
key={group.year.toString() + group.month}
|
||||
group={group}
|
||||
month={new Date(group.items[0].createdAt).toLocaleString(
|
||||
'default',
|
||||
{ month: 'long' },
|
||||
)}
|
||||
year={
|
||||
index === 0 || group.year !== groupedActivities[index - 1].year
|
||||
? group.year
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</StyledTimelineContainer>
|
||||
</StyledScrollWrapper>
|
||||
);
|
||||
};
|
||||
@ -1,113 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
|
||||
|
||||
import { useActivities } from '@/activities/hooks/useActivities';
|
||||
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FindManyTimelineActivitiesOrderBy';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
||||
import { timelineActivitiesFamilyState } from '@/activities/timeline/states/timelineActivitiesFamilyState';
|
||||
import { timelineActivitiesForGroupState } from '@/activities/timeline/states/timelineActivitiesForGroupState';
|
||||
import { timelineActivityWithoutTargetsFamilyState } from '@/activities/timeline/states/timelineActivityWithoutTargetsFamilyState';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { sortObjectRecordByDateField } from '@/object-record/utils/sortObjectRecordByDateField';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const TimelineQueryEffect = ({
|
||||
targetableObject,
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const setTimelineTargetableObject = useSetRecoilState(
|
||||
objectShowPageTargetableObjectState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTimelineTargetableObject(targetableObject);
|
||||
}, [targetableObject, setTimelineTargetableObject]);
|
||||
|
||||
const { activities } = useActivities({
|
||||
targetableObjects: [targetableObject],
|
||||
activitiesFilters: {},
|
||||
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||
skip: !isDefined(targetableObject),
|
||||
});
|
||||
|
||||
const [timelineActivitiesForGroup, setTimelineActivitiesForGroup] =
|
||||
useRecoilState(timelineActivitiesForGroupState);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDefined(targetableObject)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const activitiesForGroup = [
|
||||
...activities.map((activity) => ({
|
||||
id: activity.id,
|
||||
createdAt: activity.createdAt,
|
||||
__typename: activity.__typename,
|
||||
})),
|
||||
].sort(sortObjectRecordByDateField('createdAt', 'DescNullsLast'));
|
||||
|
||||
const timelineActivitiesForGroupSorted = [
|
||||
...timelineActivitiesForGroup,
|
||||
].sort(sortObjectRecordByDateField('createdAt', 'DescNullsLast'));
|
||||
|
||||
if (!isDeeplyEqual(activitiesForGroup, timelineActivitiesForGroupSorted)) {
|
||||
setTimelineActivitiesForGroup(activitiesForGroup);
|
||||
}
|
||||
}, [
|
||||
activities,
|
||||
targetableObject,
|
||||
timelineActivitiesForGroup,
|
||||
setTimelineActivitiesForGroup,
|
||||
]);
|
||||
|
||||
const updateTimelineActivities = useRecoilCallback(
|
||||
({ snapshot, set }) =>
|
||||
(newActivities: Activity[]) => {
|
||||
for (const newActivity of newActivities) {
|
||||
const currentActivity = snapshot
|
||||
.getLoadable(timelineActivitiesFamilyState(newActivity.id))
|
||||
.getValue();
|
||||
|
||||
if (!isDeeplyEqual(newActivity, currentActivity)) {
|
||||
set(timelineActivitiesFamilyState(newActivity.id), newActivity);
|
||||
}
|
||||
|
||||
const currentActivityWithoutTarget = snapshot
|
||||
.getLoadable(
|
||||
timelineActivityWithoutTargetsFamilyState(newActivity.id),
|
||||
)
|
||||
.getValue();
|
||||
|
||||
const newActivityWithoutTarget = {
|
||||
id: newActivity.id,
|
||||
title: newActivity.title,
|
||||
createdAt: newActivity.createdAt,
|
||||
author: newActivity.author,
|
||||
type: newActivity.type,
|
||||
};
|
||||
|
||||
if (
|
||||
!isDeeplyEqual(
|
||||
newActivityWithoutTarget,
|
||||
currentActivityWithoutTarget,
|
||||
)
|
||||
) {
|
||||
set(
|
||||
timelineActivityWithoutTargetsFamilyState(newActivity.id),
|
||||
newActivityWithoutTarget,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateTimelineActivities(activities);
|
||||
}, [activities, updateTimelineActivities]);
|
||||
|
||||
return <></>;
|
||||
};
|
||||
@ -1,78 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ActivityGroup } from '../utils/groupActivitiesByMonth';
|
||||
|
||||
import { TimelineActivity } from './TimelineActivity';
|
||||
|
||||
type TimelineActivityGroupProps = {
|
||||
group: ActivityGroup;
|
||||
month: string;
|
||||
year?: number;
|
||||
};
|
||||
|
||||
const StyledActivityGroup = styled.div`
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledActivityGroupContainer = styled.div`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const StyledActivityGroupBar = styled.div`
|
||||
align-items: center;
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.xl};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 24px;
|
||||
`;
|
||||
|
||||
const StyledMonthSeperator = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
const StyledMonthSeperatorLine = styled.div`
|
||||
background: ${({ theme }) => theme.border.color.light};
|
||||
border-radius: 50px;
|
||||
flex: 1 0 0;
|
||||
height: 1px;
|
||||
`;
|
||||
|
||||
export const TimelineActivityGroup = ({
|
||||
group,
|
||||
month,
|
||||
year,
|
||||
}: TimelineActivityGroupProps) => {
|
||||
return (
|
||||
<StyledActivityGroup>
|
||||
<StyledMonthSeperator>
|
||||
{month} {year}
|
||||
<StyledMonthSeperatorLine />
|
||||
</StyledMonthSeperator>
|
||||
<StyledActivityGroupContainer>
|
||||
<StyledActivityGroupBar />
|
||||
{group.items.map((activity, index) => (
|
||||
<TimelineActivity
|
||||
key={activity.id}
|
||||
activityId={activity.id}
|
||||
isLastActivity={index === group.items.length - 1}
|
||||
/>
|
||||
))}
|
||||
</StyledActivityGroupContainer>
|
||||
</StyledActivityGroup>
|
||||
);
|
||||
};
|
||||
@ -1,38 +0,0 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { useTimelineActivities } from '@/activities/timeline/hooks/useTimelineActivities';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<MockedProvider addTypename={false}>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</MockedProvider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
// FIXME: The hook is re-rendering so many times that it's causing a maximum
|
||||
// update depth exceeded error. We need to fix this before we can write a proper test.
|
||||
describe('useTimelineActivities', () => {
|
||||
it('works as expected', () => {
|
||||
try {
|
||||
renderHook(
|
||||
() =>
|
||||
useTimelineActivities({
|
||||
targetableObject: {
|
||||
id: '123',
|
||||
targetObjectNameSingular: 'person',
|
||||
},
|
||||
}),
|
||||
{ wrapper: Wrapper },
|
||||
);
|
||||
} catch (e) {
|
||||
expect((e as Error).message).toMatch(/^Maximum update depth exceeded/);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -1,77 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { isNonEmptyArray, isNonEmptyString } from '@sniptt/guards';
|
||||
import { useRecoilCallback, useRecoilState } from 'recoil';
|
||||
|
||||
import { useActivityTargetsForTargetableObject } from '@/activities/hooks/useActivityTargetsForTargetableObject';
|
||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
||||
import { makeTimelineActivitiesQueryVariables } from '@/activities/timeline/utils/makeTimelineActivitiesQueryVariables';
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { sortByAscString } from '~/utils/array/sortByAscString';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const useTimelineActivities = ({
|
||||
targetableObject,
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
const [, setObjectShowPageTargetableObject] = useRecoilState(
|
||||
objectShowPageTargetableObjectState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(targetableObject)) {
|
||||
setObjectShowPageTargetableObject(targetableObject);
|
||||
}
|
||||
}, [targetableObject, setObjectShowPageTargetableObject]);
|
||||
|
||||
const { activityTargets, loadingActivityTargets } =
|
||||
useActivityTargetsForTargetableObject({
|
||||
targetableObject,
|
||||
});
|
||||
|
||||
const activityIds = Array.from(
|
||||
new Set(
|
||||
activityTargets
|
||||
? [
|
||||
...activityTargets
|
||||
.map((activityTarget) => activityTarget.activityId)
|
||||
.filter(isNonEmptyString),
|
||||
].sort(sortByAscString)
|
||||
: [],
|
||||
),
|
||||
);
|
||||
|
||||
const timelineActivitiesQueryVariables = makeTimelineActivitiesQueryVariables(
|
||||
{
|
||||
activityIds,
|
||||
},
|
||||
);
|
||||
|
||||
const { records: activities, loading: loadingActivities } =
|
||||
useFindManyRecords<Activity>({
|
||||
skip: loadingActivityTargets || !isNonEmptyArray(activityTargets),
|
||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
||||
filter: timelineActivitiesQueryVariables.filter,
|
||||
orderBy: timelineActivitiesQueryVariables.orderBy,
|
||||
onCompleted: useRecoilCallback(
|
||||
({ set }) =>
|
||||
(activities) => {
|
||||
for (const activity of activities) {
|
||||
set(recordStoreFamilyState(activity.id), activity);
|
||||
}
|
||||
},
|
||||
[],
|
||||
),
|
||||
});
|
||||
|
||||
const loading = loadingActivities || loadingActivityTargets;
|
||||
|
||||
return {
|
||||
activities,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
@ -1,10 +0,0 @@
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||
|
||||
export const timelineActivitiesFamilyState = createFamilyState<
|
||||
Activity | null,
|
||||
string
|
||||
>({
|
||||
key: 'timelineActivitiesFamilyState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@ -1,10 +0,0 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
import { ActivityForActivityGroup } from '@/activities/timeline/utils/groupActivitiesByMonth';
|
||||
|
||||
export const timelineActivitiesForGroupState = createState<
|
||||
ActivityForActivityGroup[]
|
||||
>({
|
||||
key: 'timelineActivitiesForGroupState',
|
||||
defaultValue: [],
|
||||
});
|
||||
@ -1,10 +0,0 @@
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState';
|
||||
|
||||
export const timelineActivityWithoutTargetsFamilyState = createFamilyState<
|
||||
Pick<Activity, 'id' | 'title' | 'createdAt' | 'author' | 'type'> | null,
|
||||
string
|
||||
>({
|
||||
key: 'timelineActivityWithoutTargetsFamilyState',
|
||||
defaultValue: null,
|
||||
});
|
||||
@ -1,22 +0,0 @@
|
||||
import { ActivityForDrawer } from '@/activities/types/ActivityForDrawer';
|
||||
import { mockedActivities } from '~/testing/mock-data/activities';
|
||||
|
||||
import { groupActivitiesByMonth } from '../groupActivitiesByMonth';
|
||||
|
||||
describe('groupActivitiesByMonth', () => {
|
||||
it('should group activities by month', () => {
|
||||
const grouped = groupActivitiesByMonth(
|
||||
mockedActivities as unknown as ActivityForDrawer[],
|
||||
);
|
||||
|
||||
expect(grouped).toHaveLength(2);
|
||||
expect(grouped[0].items).toHaveLength(1);
|
||||
expect(grouped[1].items).toHaveLength(1);
|
||||
|
||||
expect(grouped[0].year).toBe(new Date().getFullYear());
|
||||
expect(grouped[1].year).toBe(2023);
|
||||
|
||||
expect(grouped[0].month).toBe(new Date().getMonth());
|
||||
expect(grouped[1].month).toBe(3);
|
||||
});
|
||||
});
|
||||
@ -1,40 +0,0 @@
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export type ActivityForActivityGroup = Pick<
|
||||
Activity,
|
||||
'id' | 'createdAt' | '__typename'
|
||||
>;
|
||||
|
||||
export type ActivityGroup = {
|
||||
month: number;
|
||||
year: number;
|
||||
items: ActivityForActivityGroup[];
|
||||
};
|
||||
|
||||
export const groupActivitiesByMonth = (
|
||||
activities: ActivityForActivityGroup[],
|
||||
) => {
|
||||
const acitivityGroups: ActivityGroup[] = [];
|
||||
|
||||
for (const activity of activities) {
|
||||
const d = new Date(activity.createdAt);
|
||||
const month = d.getMonth();
|
||||
const year = d.getFullYear();
|
||||
|
||||
const matchingGroup = acitivityGroups.find(
|
||||
(x) => x.year === year && x.month === month,
|
||||
);
|
||||
if (isDefined(matchingGroup)) {
|
||||
matchingGroup.items.push(activity);
|
||||
} else {
|
||||
acitivityGroups.push({
|
||||
year,
|
||||
month,
|
||||
items: [activity],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return acitivityGroups.sort((a, b) => b.year - a.year || b.month - a.month);
|
||||
};
|
||||
@ -1,22 +0,0 @@
|
||||
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
|
||||
import { sortByAscString } from '~/utils/array/sortByAscString';
|
||||
|
||||
// Todo: this should be replace by the operationSignatureFactory pattern
|
||||
export const makeTimelineActivitiesQueryVariables = ({
|
||||
activityIds,
|
||||
}: {
|
||||
activityIds: string[];
|
||||
}): RecordGqlOperationVariables => {
|
||||
return {
|
||||
filter: {
|
||||
id: {
|
||||
in: [...activityIds].sort(sortByAscString),
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{
|
||||
createdAt: 'DescNullsFirst',
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
@ -1,9 +1,9 @@
|
||||
import { useContext } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useContext } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { useLinkedObject } from '@/activities/timeline/hooks/useLinkedObject';
|
||||
import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext';
|
||||
import { useLinkedObject } from '@/activities/timelineActivities/hooks/useLinkedObject';
|
||||
import { EventIconDynamicComponent } from '@/activities/timelineActivities/rows/components/EventIconDynamicComponent';
|
||||
import { EventRowDynamicComponent } from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||
|
||||
@ -2,8 +2,8 @@ import styled from '@emotion/styled';
|
||||
|
||||
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
||||
import { SkeletonLoader } from '@/activities/components/SkeletonLoader';
|
||||
import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup';
|
||||
import { EventList } from '@/activities/timelineActivities/components/EventList';
|
||||
import { TimelineCreateButtonGroup } from '@/activities/timelineActivities/components/TimelineCreateButtonGroup';
|
||||
import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
|
||||
@ -35,8 +35,10 @@ const StyledMainContainer = styled.div`
|
||||
|
||||
export const TimelineActivities = ({
|
||||
targetableObject,
|
||||
isInRightDrawer = false,
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
isInRightDrawer?: boolean;
|
||||
}) => {
|
||||
const { timelineActivities, loading, fetchMoreRecords } =
|
||||
useTimelineActivities(targetableObject);
|
||||
@ -63,7 +65,7 @@ export const TimelineActivities = ({
|
||||
There are no activities associated with this record.{' '}
|
||||
</AnimatedPlaceholderEmptySubTitle>
|
||||
</AnimatedPlaceholderEmptyTextContainer>
|
||||
<TimelineCreateButtonGroup />
|
||||
<TimelineCreateButtonGroup isInRightDrawer={isInRightDrawer} />
|
||||
</AnimatedPlaceholderEmptyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import { useActivities } from '@/activities/hooks/useActivities';
|
||||
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline/constants/FindManyTimelineActivitiesOrderBy';
|
||||
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timelineActivities/constants/FindManyTimelineActivitiesOrderBy';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
export const TimelineActivitiesQueryEffect = ({
|
||||
@ -9,6 +10,8 @@ export const TimelineActivitiesQueryEffect = ({
|
||||
targetableObject: ActivityTargetableObject;
|
||||
}) => {
|
||||
useActivities({
|
||||
objectNameSingular:
|
||||
targetableObject.targetObjectNameSingular as CoreObjectNameSingular,
|
||||
targetableObjects: [targetableObject],
|
||||
activitiesFilters: {},
|
||||
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||
|
||||
@ -6,8 +6,14 @@ import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup';
|
||||
import { TAB_LIST_COMPONENT_ID } from '@/ui/layout/show-page/components/ShowPageRightContainer';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
|
||||
export const TimelineCreateButtonGroup = () => {
|
||||
const { activeTabIdState } = useTabList(TAB_LIST_COMPONENT_ID);
|
||||
export const TimelineCreateButtonGroup = ({
|
||||
isInRightDrawer = false,
|
||||
}: {
|
||||
isInRightDrawer?: boolean;
|
||||
}) => {
|
||||
const { activeTabIdState } = useTabList(
|
||||
`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`,
|
||||
);
|
||||
const setActiveTabId = useSetRecoilState(activeTabIdState);
|
||||
|
||||
return (
|
||||
@ -1,7 +1,9 @@
|
||||
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
|
||||
// do we need to test this?
|
||||
@ -12,6 +14,10 @@ export const useTimelineActivities = (
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
});
|
||||
|
||||
const { objectMetadataItem } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.TimelineActivity,
|
||||
});
|
||||
|
||||
const {
|
||||
records: TimelineActivities,
|
||||
loading,
|
||||
@ -28,18 +34,7 @@ export const useTimelineActivities = (
|
||||
createdAt: 'DescNullsFirst',
|
||||
},
|
||||
],
|
||||
recordGqlFields: {
|
||||
id: true,
|
||||
createdAt: true,
|
||||
linkedObjectMetadataId: true,
|
||||
linkedRecordCachedName: true,
|
||||
linkedRecordId: true,
|
||||
name: true,
|
||||
properties: true,
|
||||
happensAt: true,
|
||||
workspaceMember: true,
|
||||
person: true,
|
||||
},
|
||||
recordGqlFields: generateDepthOneRecordGqlFields({ objectMetadataItem }),
|
||||
fetchPolicy: 'cache-and-network',
|
||||
});
|
||||
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
StyledEventRowItemAction,
|
||||
StyledEventRowItemColumn,
|
||||
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
|
||||
type EventRowActivityProps = EventRowDynamicComponentProps;
|
||||
@ -19,11 +20,10 @@ const StyledLinkedActivity = styled.span`
|
||||
export const EventRowActivity = ({
|
||||
event,
|
||||
authorFullName,
|
||||
}: EventRowActivityProps) => {
|
||||
objectNameSingular,
|
||||
}: EventRowActivityProps & { objectNameSingular: CoreObjectNameSingular }) => {
|
||||
const [, eventAction] = event.name.split('.');
|
||||
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||
|
||||
if (!event.linkedRecordId) {
|
||||
throw new Error('Could not find linked record id for event');
|
||||
}
|
||||
@ -32,6 +32,10 @@ export const EventRowActivity = ({
|
||||
recordStoreFamilyState(event.linkedRecordId),
|
||||
);
|
||||
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer({
|
||||
objectNameSingular,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
|
||||
|
||||
@ -5,6 +5,7 @@ import { EventRowCalendarEvent } from '@/activities/timelineActivities/rows/cale
|
||||
import { EventRowMainObject } from '@/activities/timelineActivities/rows/main-object/components/EventRowMainObject';
|
||||
import { EventRowMessage } from '@/activities/timelineActivities/rows/message/components/EventRowMessage';
|
||||
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
export interface EventRowDynamicComponentProps {
|
||||
@ -57,8 +58,7 @@ export const EventRowDynamicComponent = ({
|
||||
authorFullName={authorFullName}
|
||||
/>
|
||||
);
|
||||
case 'task':
|
||||
case 'note':
|
||||
case 'linked-task':
|
||||
return (
|
||||
<EventRowActivity
|
||||
labelIdentifierValue={labelIdentifierValue}
|
||||
@ -66,6 +66,18 @@ export const EventRowDynamicComponent = ({
|
||||
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||
linkedObjectMetadataItem={linkedObjectMetadataItem}
|
||||
authorFullName={authorFullName}
|
||||
objectNameSingular={CoreObjectNameSingular.Task}
|
||||
/>
|
||||
);
|
||||
case 'linked-note':
|
||||
return (
|
||||
<EventRowActivity
|
||||
labelIdentifierValue={labelIdentifierValue}
|
||||
event={event}
|
||||
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||
linkedObjectMetadataItem={linkedObjectMetadataItem}
|
||||
authorFullName={authorFullName}
|
||||
objectNameSingular={CoreObjectNameSingular.Note}
|
||||
/>
|
||||
);
|
||||
case mainObjectMetadataItem?.nameSingular:
|
||||
|
||||
@ -13,4 +13,4 @@ export type TimelineActivity = {
|
||||
linkedRecordId: string;
|
||||
linkedObjectMetadataId: string;
|
||||
__typename: 'TimelineActivity';
|
||||
};
|
||||
} & Record<string, any>;
|
||||
|
||||
@ -1,24 +1,7 @@
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { Comment } from '@/activities/types/Comment';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
|
||||
export type ActivityType = 'Task' | 'Note';
|
||||
|
||||
export type Activity = {
|
||||
__typename: 'Activity';
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
completedAt: string | null;
|
||||
reminderAt: string | null;
|
||||
dueAt: string | null;
|
||||
activityTargets: ActivityTarget[];
|
||||
type: ActivityType;
|
||||
title: string;
|
||||
body: string;
|
||||
author: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'>;
|
||||
authorId: string;
|
||||
assignee: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
|
||||
assigneeId: string | null;
|
||||
comments: Comment[];
|
||||
body: string | null;
|
||||
};
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
|
||||
export type ActivityForDrawer = Activity;
|
||||
@ -1,27 +1,6 @@
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { Comment } from '@/activities/types/Comment';
|
||||
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
||||
import { Note } from '@/activities/types/Note';
|
||||
import { Task } from '@/activities/types/Task';
|
||||
|
||||
export type ActivityForEditor = Pick<
|
||||
Activity,
|
||||
| 'id'
|
||||
| 'title'
|
||||
| 'body'
|
||||
| 'type'
|
||||
| 'completedAt'
|
||||
| 'dueAt'
|
||||
| 'updatedAt'
|
||||
| '__typename'
|
||||
> & {
|
||||
comments?: Comment[];
|
||||
} & {
|
||||
assignee?: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
|
||||
} & {
|
||||
activityTargets?: Array<
|
||||
Pick<
|
||||
ActivityTarget,
|
||||
'id' | 'companyId' | 'personId' | 'createdAt' | 'updatedAt' | 'activity'
|
||||
>
|
||||
>;
|
||||
};
|
||||
export type ActivityForEditor = Partial<Task | Note> &
|
||||
Partial<Pick<Task, 'status' | 'dueAt' | 'assignee' | 'taskTargets'>> &
|
||||
Partial<Pick<Note, 'noteTargets'>>;
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { NoteTarget } from '@/activities/types/NoteTarget';
|
||||
import { TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
|
||||
export type ActivityTargetWithTargetRecord = {
|
||||
targetObjectMetadataItem: ObjectMetadataItem;
|
||||
activityTarget: ActivityTarget;
|
||||
activityTarget: NoteTarget | TaskTarget;
|
||||
targetObject: ObjectRecord;
|
||||
};
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
||||
import { Comment } from '@/activities/types/Comment';
|
||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
||||
|
||||
export type ActivityType = 'Task' | 'Note';
|
||||
|
||||
type ActivityTargetNode = {
|
||||
node: ActivityTarget;
|
||||
};
|
||||
|
||||
type CommentNode = {
|
||||
node: Comment;
|
||||
};
|
||||
|
||||
export type GraphQLActivity = {
|
||||
__typename: 'Activity';
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
completedAt: string | null;
|
||||
dueAt: string | null;
|
||||
activityTargets: {
|
||||
edges: ActivityTargetNode[];
|
||||
};
|
||||
type: ActivityType;
|
||||
title: string;
|
||||
body: string;
|
||||
author: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'>;
|
||||
authorId: string;
|
||||
assignee: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
|
||||
assigneeId: string | null;
|
||||
comments: CommentNode[];
|
||||
};
|
||||
@ -1,5 +1,7 @@
|
||||
import { Activity } from '@/activities/types/Activity';
|
||||
import { NoteTarget } from '@/activities/types/NoteTarget';
|
||||
|
||||
export type Note = Activity & {
|
||||
type: 'Note';
|
||||
noteTargets?: NoteTarget[];
|
||||
__typename: 'Note';
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user