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',
|
'mockServiceWorker.js',
|
||||||
'**/generated*/*',
|
'**/generated*/*',
|
||||||
'**/generated/standard-metadata-query-result.ts',
|
'**/generated/standard-metadata-query-result.ts',
|
||||||
|
'**/getObjectMetadataItemsMock.ts',
|
||||||
'tsup.config.ts',
|
'tsup.config.ts',
|
||||||
'build',
|
'build',
|
||||||
'coverage',
|
'coverage',
|
||||||
|
|||||||
@ -84,7 +84,6 @@ import { SettingsBilling } from '~/pages/settings/SettingsBilling';
|
|||||||
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
|
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
|
||||||
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
|
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
|
||||||
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
|
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
|
||||||
import { Tasks } from '~/pages/tasks/Tasks';
|
|
||||||
import { getPageTitleFromPath } from '~/utils/title-utils';
|
import { getPageTitleFromPath } from '~/utils/title-utils';
|
||||||
|
|
||||||
const ProvidersThatNeedRouterContext = () => {
|
const ProvidersThatNeedRouterContext = () => {
|
||||||
@ -158,7 +157,6 @@ const createRouter = (
|
|||||||
element={<PaymentSuccess />}
|
element={<PaymentSuccess />}
|
||||||
/>
|
/>
|
||||||
<Route path={indexAppPath.getIndexAppPath()} element={<></>} />
|
<Route path={indexAppPath.getIndexAppPath()} element={<></>} />
|
||||||
<Route path={AppPath.TasksPage} element={<Tasks />} />
|
|
||||||
<Route path={AppPath.Impersonate} element={<ImpersonateEffect />} />
|
<Route path={AppPath.Impersonate} element={<ImpersonateEffect />} />
|
||||||
<Route path={AppPath.RecordIndexPage} element={<RecordIndexPage />} />
|
<Route path={AppPath.RecordIndexPage} element={<RecordIndexPage />} />
|
||||||
<Route path={AppPath.RecordShowPage} element={<RecordShowPage />} />
|
<Route path={AppPath.RecordShowPage} element={<RecordShowPage />} />
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCapt
|
|||||||
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
|
import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState';
|
||||||
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
|
||||||
import { CommandType } from '@/command-menu/types/Command';
|
import { CommandType } from '@/command-menu/types/Command';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope';
|
||||||
import { AppBasePath } from '@/types/AppBasePath';
|
import { AppBasePath } from '@/types/AppBasePath';
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
@ -38,7 +39,9 @@ export const PageChangeEffect = () => {
|
|||||||
|
|
||||||
const { addToCommandMenu, setToInitialCommandMenu } = useCommandMenu();
|
const { addToCommandMenu, setToInitialCommandMenu } = useCommandMenu();
|
||||||
|
|
||||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
const openCreateActivity = useOpenCreateActivityDrawer({
|
||||||
|
activityObjectNameSingular: CoreObjectNameSingular.Task,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!previousLocation || previousLocation !== location.pathname) {
|
if (!previousLocation || previousLocation !== location.pathname) {
|
||||||
@ -143,8 +146,7 @@ export const PageChangeEffect = () => {
|
|||||||
label: 'Create Task',
|
label: 'Create Task',
|
||||||
type: CommandType.Create,
|
type: CommandType.Create,
|
||||||
Icon: IconCheckbox,
|
Icon: IconCheckbox,
|
||||||
onCommandClick: () =>
|
onCommandClick: () => openCreateActivity({ targetableObjects: [] }),
|
||||||
openCreateActivity({ type: 'Task', targetableObjects: [] }),
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
}, [addToCommandMenu, setToInitialCommandMenu, openCreateActivity]);
|
}, [addToCommandMenu, setToInitialCommandMenu, openCreateActivity]);
|
||||||
|
|||||||
@ -361,6 +361,7 @@ export enum FieldMetadataType {
|
|||||||
Rating = 'RATING',
|
Rating = 'RATING',
|
||||||
RawJson = 'RAW_JSON',
|
RawJson = 'RAW_JSON',
|
||||||
Relation = 'RELATION',
|
Relation = 'RELATION',
|
||||||
|
RichText = 'RICH_TEXT',
|
||||||
Select = 'SELECT',
|
Select = 'SELECT',
|
||||||
Text = 'TEXT',
|
Text = 'TEXT',
|
||||||
Uuid = 'UUID'
|
Uuid = 'UUID'
|
||||||
|
|||||||
@ -250,6 +250,7 @@ export enum FieldMetadataType {
|
|||||||
Rating = 'RATING',
|
Rating = 'RATING',
|
||||||
RawJson = 'RAW_JSON',
|
RawJson = 'RAW_JSON',
|
||||||
Relation = 'RELATION',
|
Relation = 'RELATION',
|
||||||
|
RichText = 'RICH_TEXT',
|
||||||
Select = 'SELECT',
|
Select = 'SELECT',
|
||||||
Text = 'TEXT',
|
Text = 'TEXT',
|
||||||
Uuid = 'UUID'
|
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 { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState';
|
||||||
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
|
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
|
||||||
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
import { canCreateActivityState } from '@/activities/states/canCreateActivityState';
|
||||||
import { Activity } from '@/activities/types/Activity';
|
|
||||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
|
||||||
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
import { BlockEditor } from '@/ui/input/editor/components/BlockEditor';
|
||||||
@ -29,22 +27,31 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
|||||||
|
|
||||||
import { getFileType } from '../files/utils/getFileType';
|
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/core/fonts/inter.css';
|
||||||
import '@blocknote/mantine/style.css';
|
import '@blocknote/mantine/style.css';
|
||||||
|
import '@blocknote/react/style.css';
|
||||||
|
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
||||||
|
|
||||||
type ActivityBodyEditorProps = {
|
type RichTextEditorProps = {
|
||||||
activityId: string;
|
activityId: string;
|
||||||
fillTitleFromBody: boolean;
|
fillTitleFromBody: boolean;
|
||||||
|
activityObjectNameSingular:
|
||||||
|
| CoreObjectNameSingular.Task
|
||||||
|
| CoreObjectNameSingular.Note;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ActivityBodyEditor = ({
|
export const RichTextEditor = ({
|
||||||
activityId,
|
activityId,
|
||||||
fillTitleFromBody,
|
fillTitleFromBody,
|
||||||
}: ActivityBodyEditorProps) => {
|
activityObjectNameSingular,
|
||||||
|
}: RichTextEditorProps) => {
|
||||||
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
|
const [activityInStore] = useRecoilState(recordStoreFamilyState(activityId));
|
||||||
|
|
||||||
const cache = useApolloClient().cache;
|
const cache = useApolloClient().cache;
|
||||||
const activity = activityInStore as Activity | null;
|
const activity = activityInStore as Task | Note | null;
|
||||||
|
|
||||||
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
|
const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState(
|
||||||
activityTitleHasBeenSetFamilyState({
|
activityTitleHasBeenSetFamilyState({
|
||||||
@ -60,7 +67,7 @@ export const ActivityBodyEditor = ({
|
|||||||
|
|
||||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||||
useObjectMetadataItem({
|
useObjectMetadataItem({
|
||||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
objectNameSingular: activityObjectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -68,7 +75,9 @@ export const ActivityBodyEditor = ({
|
|||||||
setHotkeyScopeAndMemorizePreviousScope,
|
setHotkeyScopeAndMemorizePreviousScope,
|
||||||
} = usePreviousHotkeyScope();
|
} = usePreviousHotkeyScope();
|
||||||
|
|
||||||
const { upsertActivity } = useUpsertActivity();
|
const { upsertActivity } = useUpsertActivity({
|
||||||
|
activityObjectNameSingular: activityObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
const persistBodyDebounced = useDebouncedCallback((newBody: string) => {
|
const persistBodyDebounced = useDebouncedCallback((newBody: string) => {
|
||||||
if (isDefined(activity)) {
|
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';
|
import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory';
|
||||||
|
|
||||||
export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatureFactory =
|
export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatureFactory =
|
||||||
({ objectMetadataItems }: { objectMetadataItems: ObjectMetadataItem[] }) => ({
|
({
|
||||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
objectMetadataItems,
|
||||||
|
objectNameSingular,
|
||||||
|
}: {
|
||||||
|
objectMetadataItems: ObjectMetadataItem[];
|
||||||
|
objectNameSingular: CoreObjectNameSingular;
|
||||||
|
}) => ({
|
||||||
|
objectNameSingular: objectNameSingular,
|
||||||
variables: {},
|
variables: {},
|
||||||
fields: {
|
fields: {
|
||||||
id: true,
|
id: true,
|
||||||
@ -28,18 +34,32 @@ export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatur
|
|||||||
attachments: true,
|
attachments: true,
|
||||||
body: true,
|
body: true,
|
||||||
title: true,
|
title: true,
|
||||||
completedAt: true,
|
status: true,
|
||||||
dueAt: true,
|
dueAt: true,
|
||||||
reminderAt: true,
|
reminderAt: true,
|
||||||
type: true,
|
type: true,
|
||||||
activityTargets: {
|
...(objectNameSingular === CoreObjectNameSingular.Note
|
||||||
id: true,
|
? {
|
||||||
__typename: true,
|
noteTargets: {
|
||||||
createdAt: true,
|
id: true,
|
||||||
updatedAt: true,
|
__typename: true,
|
||||||
activity: true,
|
createdAt: true,
|
||||||
activityId: true,
|
updatedAt: true,
|
||||||
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
|
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 { generateActivityTargetMorphFieldKeys } from '@/activities/utils/generateActivityTargetMorphFieldKeys';
|
||||||
|
import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory';
|
import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory';
|
||||||
|
|
||||||
export const findActivityTargetsOperationSignatureFactory: RecordGqlOperationSignatureFactory =
|
export const findActivityTargetsOperationSignatureFactory: RecordGqlOperationSignatureFactory =
|
||||||
({ objectMetadataItems }: { objectMetadataItems: ObjectMetadataItem[] }) => ({
|
({
|
||||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
objectNameSingular,
|
||||||
|
objectMetadataItems,
|
||||||
|
}: {
|
||||||
|
objectNameSingular: CoreObjectNameSingular;
|
||||||
|
objectMetadataItems: ObjectMetadataItem[];
|
||||||
|
}) => ({
|
||||||
|
objectNameSingular: getJoinObjectNameSingular(objectNameSingular),
|
||||||
variables: {},
|
variables: {},
|
||||||
fields: {
|
fields: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
|
|||||||
|
|
||||||
import { useActivities } from '@/activities/hooks/useActivities';
|
import { useActivities } from '@/activities/hooks/useActivities';
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||||
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||||
|
|
||||||
@ -20,16 +21,14 @@ const mockActivityTarget = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const mockActivity = {
|
const mockActivity = {
|
||||||
__typename: 'Activity',
|
__typename: 'Note',
|
||||||
updatedAt: '2021-08-03T19:20:06.000Z',
|
updatedAt: '2021-08-03T19:20:06.000Z',
|
||||||
createdAt: '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',
|
reminderAt: '2021-08-03T19:20:06.000Z',
|
||||||
title: 'title',
|
title: 'title',
|
||||||
authorId: '1',
|
|
||||||
body: 'body',
|
body: 'body',
|
||||||
dueAt: '2021-08-03T19:20:06.000Z',
|
dueAt: '2021-08-03T19:20:06.000Z',
|
||||||
type: 'type',
|
|
||||||
assigneeId: '1',
|
assigneeId: '1',
|
||||||
id: '234',
|
id: '234',
|
||||||
};
|
};
|
||||||
@ -102,13 +101,13 @@ const mocks: MockedResponse[] = [
|
|||||||
{
|
{
|
||||||
request: {
|
request: {
|
||||||
query: gql`
|
query: gql`
|
||||||
query FindManyActivities(
|
query FindManyTasks(
|
||||||
$filter: ActivityFilterInput
|
$filter: TaskFilterInput
|
||||||
$orderBy: [ActivityOrderByInput]
|
$orderBy: [TaskOrderByInput]
|
||||||
$lastCursor: String
|
$lastCursor: String
|
||||||
$limit: Int
|
$limit: Int
|
||||||
) {
|
) {
|
||||||
activities(
|
tasks(
|
||||||
filter: $filter
|
filter: $filter
|
||||||
orderBy: $orderBy
|
orderBy: $orderBy
|
||||||
first: $limit
|
first: $limit
|
||||||
@ -117,17 +116,13 @@ const mocks: MockedResponse[] = [
|
|||||||
edges {
|
edges {
|
||||||
node {
|
node {
|
||||||
__typename
|
__typename
|
||||||
createdAt
|
|
||||||
reminderAt
|
|
||||||
authorId
|
|
||||||
title
|
title
|
||||||
completedAt
|
|
||||||
updatedAt
|
|
||||||
body
|
|
||||||
dueAt
|
|
||||||
type
|
|
||||||
id
|
id
|
||||||
assigneeId
|
updatedAt
|
||||||
|
createdAt
|
||||||
|
body
|
||||||
|
status
|
||||||
|
dueAt
|
||||||
}
|
}
|
||||||
cursor
|
cursor
|
||||||
}
|
}
|
||||||
@ -178,6 +173,7 @@ describe('useActivities', () => {
|
|||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() =>
|
() =>
|
||||||
useActivities({
|
useActivities({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Task,
|
||||||
targetableObjects: [],
|
targetableObjects: [],
|
||||||
activitiesFilters: {},
|
activitiesFilters: {},
|
||||||
activitiesOrderByVariables: [{}],
|
activitiesOrderByVariables: [{}],
|
||||||
@ -200,6 +196,7 @@ describe('useActivities', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const activities = useActivities({
|
const activities = useActivities({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Task,
|
||||||
targetableObjects: [
|
targetableObjects: [
|
||||||
{ targetObjectNameSingular: 'company', id: '123' },
|
{ targetObjectNameSingular: 'company', id: '123' },
|
||||||
],
|
],
|
||||||
|
|||||||
@ -7,8 +7,8 @@ import { RecoilRoot, useSetRecoilState } from 'recoil';
|
|||||||
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
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 { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||||
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
||||||
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members';
|
||||||
@ -16,108 +16,95 @@ const mockObjectMetadataItems = getObjectMetadataItemsMock();
|
|||||||
|
|
||||||
const cache = new InMemoryCache();
|
const cache = new InMemoryCache();
|
||||||
|
|
||||||
const activityNode = {
|
const taskTarget = {
|
||||||
id: '3ecaa1be-aac7-463a-a38e-64078dd451d5',
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
|
||||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||||
reminderAt: null,
|
companyId: null,
|
||||||
title: 'My very first note',
|
company: null,
|
||||||
type: 'Note',
|
personId: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
|
||||||
body: '',
|
person: {
|
||||||
dueAt: '2023-04-26T10:12:42.33625+00:00',
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
|
||||||
completedAt: null,
|
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
author: null,
|
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||||
assignee: null,
|
city: 'City',
|
||||||
assigneeId: null,
|
name: {
|
||||||
authorId: null,
|
firstName: 'John',
|
||||||
comments: {
|
lastName: 'Doe',
|
||||||
edges: [],
|
},
|
||||||
|
__typename: 'Person',
|
||||||
},
|
},
|
||||||
activityTargets: {
|
taskId: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
|
||||||
edges: [
|
task: {
|
||||||
{
|
id: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
|
||||||
node: {
|
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
||||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb300',
|
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
||||||
createdAt: '2023-04-26T10:12:42.33625+00:00',
|
dueAt: null,
|
||||||
updatedAt: '2023-04-26T10:23:42.33625+00:00',
|
body: '{}',
|
||||||
personId: null,
|
title: 'Task title',
|
||||||
companyId: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
|
assigneeId: null,
|
||||||
company: {
|
__typename: 'Task',
|
||||||
id: '89bb825c-171e-4bcc-9cf7-43448d6fb280',
|
|
||||||
name: 'Airbnb',
|
|
||||||
domainName: {
|
|
||||||
primaryLinkUrl: 'https://www.airbnb.com',
|
|
||||||
primaryLinkLabel: '',
|
|
||||||
secondaryLinks: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
person: null,
|
|
||||||
activityId: '89bb825c-171e-4bcc-9cf7-43448d6fb230',
|
|
||||||
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',
|
|
||||||
},
|
|
||||||
__typename: 'ActivityTargetEdge',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
__typename: 'ActivityTargetConnection',
|
|
||||||
},
|
},
|
||||||
__typename: 'Activity' as const,
|
__typename: 'TaskTarget',
|
||||||
};
|
};
|
||||||
|
|
||||||
cache.writeFragment({
|
cache.writeFragment({
|
||||||
fragment: gql`
|
fragment: gql`
|
||||||
fragment CreateOneActivityInCache on Activity {
|
fragment TaskTargetFragment on TaskTarget {
|
||||||
id
|
__typename
|
||||||
createdAt
|
|
||||||
updatedAt
|
updatedAt
|
||||||
reminderAt
|
createdAt
|
||||||
title
|
personId
|
||||||
body
|
taskId
|
||||||
dueAt
|
companyId
|
||||||
completedAt
|
id
|
||||||
author
|
task {
|
||||||
assignee
|
__typename
|
||||||
assigneeId
|
createdAt
|
||||||
authorId
|
title
|
||||||
activityTargets {
|
updatedAt
|
||||||
edges {
|
body
|
||||||
node {
|
dueAt
|
||||||
id
|
id
|
||||||
createdAt
|
assigneeId
|
||||||
updatedAt
|
}
|
||||||
personId
|
person {
|
||||||
companyId
|
__typename
|
||||||
company {
|
id
|
||||||
id
|
createdAt
|
||||||
name
|
updatedAt
|
||||||
domainName {
|
city
|
||||||
primaryLinkUrl
|
name {
|
||||||
primaryLinkLabel
|
firstName
|
||||||
secondaryLinks
|
lastName
|
||||||
}
|
|
||||||
}
|
|
||||||
person
|
|
||||||
activityId
|
|
||||||
activity {
|
|
||||||
id
|
|
||||||
createdAt
|
|
||||||
updatedAt
|
|
||||||
}
|
|
||||||
__typename
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
__typename
|
company {
|
||||||
|
__typename
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
id: activityNode.id,
|
id: `TaskTarget:${taskTarget.id}`,
|
||||||
data: activityNode,
|
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 }) => (
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
<MockedProvider cache={cache}>
|
<MockedProvider cache={cache}>
|
||||||
@ -142,7 +129,8 @@ describe('useActivityTargetObjectRecords', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
|
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
|
||||||
getRecordFromRecordNode({ recordNode: activityNode as any }),
|
task,
|
||||||
|
CoreObjectNameSingular.Task,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -158,18 +146,17 @@ describe('useActivityTargetObjectRecords', () => {
|
|||||||
result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
|
result.current.setCurrentWorkspaceMember(mockWorkspaceMembers[0]);
|
||||||
result.current.setObjectMetadataItems(mockObjectMetadataItems);
|
result.current.setObjectMetadataItems(mockObjectMetadataItems);
|
||||||
});
|
});
|
||||||
|
|
||||||
const activityTargetObjectRecords =
|
const activityTargetObjectRecords =
|
||||||
result.current.activityTargetObjectRecords;
|
result.current.activityTargetObjectRecords;
|
||||||
|
|
||||||
expect(activityTargetObjectRecords).toHaveLength(1);
|
expect(activityTargetObjectRecords).toHaveLength(1);
|
||||||
expect(activityTargetObjectRecords[0].activityTarget).toEqual(
|
expect(activityTargetObjectRecords[0].activityTarget).toEqual(taskTarget);
|
||||||
activityNode.activityTargets.edges[0].node,
|
|
||||||
);
|
|
||||||
expect(activityTargetObjectRecords[0].targetObject).toEqual(
|
expect(activityTargetObjectRecords[0].targetObject).toEqual(
|
||||||
activityNode.activityTargets.edges[0].node.company,
|
taskTarget.person,
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
activityTargetObjectRecords[0].targetObjectMetadataItem.nameSingular,
|
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 { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB';
|
import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
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 mockedDate = '2024-03-15T12:00:00.000Z';
|
||||||
const toISOStringMock = jest.fn(() => mockedDate);
|
const toISOStringMock = jest.fn(() => mockedDate);
|
||||||
global.Date.prototype.toISOString = toISOStringMock;
|
global.Date.prototype.toISOString = toISOStringMock;
|
||||||
|
|
||||||
const mockedActivity = {
|
const mockedActivity = {
|
||||||
...pick(mockedActivities[0], [
|
...pick(mockedTasks[0], ['id', 'title', 'body', 'type', 'status', 'dueAt']),
|
||||||
'id',
|
|
||||||
'title',
|
|
||||||
'body',
|
|
||||||
'type',
|
|
||||||
'completedAt',
|
|
||||||
'dueAt',
|
|
||||||
]),
|
|
||||||
updatedAt: mockedDate,
|
updatedAt: mockedDate,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -36,7 +30,7 @@ const mocks: MockedResponse[] = [
|
|||||||
reminderAt
|
reminderAt
|
||||||
authorId
|
authorId
|
||||||
title
|
title
|
||||||
completedAt
|
status
|
||||||
updatedAt
|
updatedAt
|
||||||
body
|
body
|
||||||
dueAt
|
dueAt
|
||||||
@ -77,14 +71,19 @@ const Wrapper = ({ children }: { children: ReactNode }) => (
|
|||||||
|
|
||||||
describe('useCreateActivityInDB', () => {
|
describe('useCreateActivityInDB', () => {
|
||||||
it('Should create activity in DB', async () => {
|
it('Should create activity in DB', async () => {
|
||||||
const { result } = renderHook(() => useCreateActivityInDB(), {
|
const { result } = renderHook(
|
||||||
wrapper: Wrapper,
|
() =>
|
||||||
});
|
useCreateActivityInDB({
|
||||||
|
activityObjectNameSingular: CoreObjectNameSingular.Task,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
wrapper: Wrapper,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await result.current.createActivityInDB({
|
await result.current.createActivityInDB({
|
||||||
...mockedActivity,
|
...mockedActivity,
|
||||||
__typename: 'Activity',
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
import { MockedProvider } from '@apollo/client/testing';
|
||||||
import { act, renderHook } from '@testing-library/react';
|
import { act, renderHook } from '@testing-library/react';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
import { RecoilRoot, useRecoilValue } from 'recoil';
|
import { RecoilRoot, useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
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';
|
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||||
|
|
||||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
@ -17,12 +17,12 @@ describe('useOpenActivityRightDrawer', () => {
|
|||||||
it('works as expected', () => {
|
it('works as expected', () => {
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => {
|
() => {
|
||||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
const openActivityRightDrawer = useOpenActivityRightDrawer({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Task,
|
||||||
|
});
|
||||||
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
||||||
const activityIdInDrawer = useRecoilValue(activityIdInDrawerState);
|
|
||||||
return {
|
return {
|
||||||
openActivityRightDrawer,
|
openActivityRightDrawer,
|
||||||
activityIdInDrawer,
|
|
||||||
viewableRecordId,
|
viewableRecordId,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@ -31,12 +31,10 @@ describe('useOpenActivityRightDrawer', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(result.current.activityIdInDrawer).toBeNull();
|
|
||||||
expect(result.current.viewableRecordId).toBeNull();
|
expect(result.current.viewableRecordId).toBeNull();
|
||||||
act(() => {
|
act(() => {
|
||||||
result.current.openActivityRightDrawer('123');
|
result.current.openActivityRightDrawer('123');
|
||||||
});
|
});
|
||||||
expect(result.current.activityIdInDrawer).toBe('123');
|
|
||||||
expect(result.current.viewableRecordId).toBe('123');
|
expect(result.current.viewableRecordId).toBe('123');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,23 +1,71 @@
|
|||||||
import { ReactNode } from 'react';
|
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
|
||||||
import { MockedProvider } from '@apollo/client/testing';
|
|
||||||
import { act, renderHook } from '@testing-library/react';
|
import { act, renderHook } from '@testing-library/react';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil';
|
import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||||
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
|
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
import { getObjectMetadataItemsMock } from '@/object-metadata/utils/getObjectMetadataItemsMock';
|
||||||
import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
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', () => ({
|
const mockedActivity = {
|
||||||
v4: () => mockUUID,
|
...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 }) => (
|
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||||
<RecoilRoot>
|
<RecoilRoot>
|
||||||
<MockedProvider addTypename={false}>{children}</MockedProvider>
|
<MockedProvider addTypename={false} mocks={mocks}>
|
||||||
|
{children}
|
||||||
|
</MockedProvider>
|
||||||
</RecoilRoot>
|
</RecoilRoot>
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -27,15 +75,15 @@ describe('useOpenCreateActivityDrawer', () => {
|
|||||||
it('works as expected', async () => {
|
it('works as expected', async () => {
|
||||||
const { result } = renderHook(
|
const { result } = renderHook(
|
||||||
() => {
|
() => {
|
||||||
const openActivityRightDrawer = useOpenCreateActivityDrawer();
|
const openActivityRightDrawer = useOpenCreateActivityDrawer({
|
||||||
|
activityObjectNameSingular: CoreObjectNameSingular.Note,
|
||||||
|
});
|
||||||
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
const viewableRecordId = useRecoilValue(viewableRecordIdState);
|
||||||
const activityIdInDrawer = useRecoilValue(activityIdInDrawerState);
|
|
||||||
const setObjectMetadataItems = useSetRecoilState(
|
const setObjectMetadataItems = useSetRecoilState(
|
||||||
objectMetadataItemsState,
|
objectMetadataItemsState,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
openActivityRightDrawer,
|
openActivityRightDrawer,
|
||||||
activityIdInDrawer,
|
|
||||||
viewableRecordId,
|
viewableRecordId,
|
||||||
setObjectMetadataItems,
|
setObjectMetadataItems,
|
||||||
};
|
};
|
||||||
@ -49,15 +97,11 @@ describe('useOpenCreateActivityDrawer', () => {
|
|||||||
result.current.setObjectMetadataItems(mockObjectMetadataItems);
|
result.current.setObjectMetadataItems(mockObjectMetadataItems);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.activityIdInDrawer).toBeNull();
|
|
||||||
expect(result.current.viewableRecordId).toBeNull();
|
expect(result.current.viewableRecordId).toBeNull();
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
result.current.openActivityRightDrawer({
|
result.current.openActivityRightDrawer({
|
||||||
type: 'Note',
|
|
||||||
targetableObjects: [],
|
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 { findActivitiesOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory';
|
||||||
import { useActivityTargetsForTargetableObjects } from '@/activities/hooks/useActivityTargetsForTargetableObjects';
|
import { useActivityTargetsForTargetableObjects } from '@/activities/hooks/useActivityTargetsForTargetableObjects';
|
||||||
import { Activity } from '@/activities/types/Activity';
|
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
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 { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
|
||||||
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
import { RecordGqlOperationOrderBy } from '@/object-record/graphql/types/RecordGqlOperationOrderBy';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { sortByAscString } from '~/utils/array/sortByAscString';
|
import { sortByAscString } from '~/utils/array/sortByAscString';
|
||||||
|
|
||||||
export const useActivities = ({
|
export const useActivities = <T extends Task | Note>({
|
||||||
|
objectNameSingular,
|
||||||
targetableObjects,
|
targetableObjects,
|
||||||
activitiesFilters,
|
activitiesFilters,
|
||||||
activitiesOrderByVariables,
|
activitiesOrderByVariables,
|
||||||
skip,
|
skip,
|
||||||
}: {
|
}: {
|
||||||
|
objectNameSingular: CoreObjectNameSingular;
|
||||||
targetableObjects: ActivityTargetableObject[];
|
targetableObjects: ActivityTargetableObject[];
|
||||||
activitiesFilters: RecordGqlOperationFilter;
|
activitiesFilters: RecordGqlOperationFilter;
|
||||||
activitiesOrderByVariables: RecordGqlOperationOrderBy;
|
activitiesOrderByVariables: RecordGqlOperationOrderBy;
|
||||||
@ -27,6 +31,7 @@ export const useActivities = ({
|
|||||||
|
|
||||||
const { activityTargets, loadingActivityTargets } =
|
const { activityTargets, loadingActivityTargets } =
|
||||||
useActivityTargetsForTargetableObjects({
|
useActivityTargetsForTargetableObjects({
|
||||||
|
objectNameSingular,
|
||||||
targetableObjects,
|
targetableObjects,
|
||||||
skip: skip,
|
skip: skip,
|
||||||
});
|
});
|
||||||
@ -36,7 +41,10 @@ export const useActivities = ({
|
|||||||
activityTargets
|
activityTargets
|
||||||
? [
|
? [
|
||||||
...activityTargets
|
...activityTargets
|
||||||
.map((activityTarget) => activityTarget.activityId)
|
.map(
|
||||||
|
(activityTarget) =>
|
||||||
|
activityTarget.taskId ?? activityTarget.noteId,
|
||||||
|
)
|
||||||
.filter(isNonEmptyString),
|
.filter(isNonEmptyString),
|
||||||
].sort(sortByAscString)
|
].sort(sortByAscString)
|
||||||
: [],
|
: [],
|
||||||
@ -54,10 +62,13 @@ export const useActivities = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FIND_ACTIVITIES_OPERATION_SIGNATURE =
|
const FIND_ACTIVITIES_OPERATION_SIGNATURE =
|
||||||
findActivitiesOperationSignatureFactory({ objectMetadataItems });
|
findActivitiesOperationSignatureFactory({
|
||||||
|
objectMetadataItems,
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
const { records: activities, loading: loadingActivities } =
|
const { records: activities, loading: loadingActivities } =
|
||||||
useFindManyRecords<Activity>({
|
useFindManyRecords<Task | Note>({
|
||||||
skip: skip || loadingActivityTargets,
|
skip: skip || loadingActivityTargets,
|
||||||
objectNameSingular:
|
objectNameSingular:
|
||||||
FIND_ACTIVITIES_OPERATION_SIGNATURE.objectNameSingular,
|
FIND_ACTIVITIES_OPERATION_SIGNATURE.objectNameSingular,
|
||||||
@ -76,7 +87,7 @@ export const useActivities = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activities,
|
activities: activities as T[],
|
||||||
loading: loadingActivities || loadingActivityTargets,
|
loading: loadingActivities || loadingActivityTargets,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2,31 +2,41 @@ import { useApolloClient } from '@apollo/client';
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { Nullable } from 'twenty-ui';
|
import { Nullable } from 'twenty-ui';
|
||||||
|
|
||||||
import { Activity } from '@/activities/types/Activity';
|
|
||||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
|
||||||
import { ActivityTargetWithTargetRecord } from '@/activities/types/ActivityTargetObject';
|
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 { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const useActivityTargetObjectRecords = (activity: Activity) => {
|
export const useActivityTargetObjectRecords = (
|
||||||
|
activity: Task | Note,
|
||||||
|
objectNameSingular: CoreObjectNameSingular,
|
||||||
|
) => {
|
||||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
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({
|
const getRecordFromCache = useGetRecordFromCache({
|
||||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
objectNameSingular: getJoinObjectNameSingular(objectNameSingular),
|
||||||
});
|
});
|
||||||
|
|
||||||
const apolloClient = useApolloClient();
|
const apolloClient = useApolloClient();
|
||||||
|
|
||||||
const activityTargetObjectRecords = activityTargets
|
const activityTargetObjectRecords = activityTargets
|
||||||
.map<Nullable<ActivityTargetWithTargetRecord>>((activityTarget) => {
|
.map<Nullable<ActivityTargetWithTargetRecord>>((activityTarget) => {
|
||||||
const activityTargetFromCache = getRecordFromCache<ActivityTarget>(
|
const activityTargetFromCache = getRecordFromCache<
|
||||||
activityTarget.id,
|
NoteTarget | TaskTarget
|
||||||
apolloClient.cache,
|
>(activityTarget.id, apolloClient.cache);
|
||||||
);
|
|
||||||
|
|
||||||
if (!isDefined(activityTargetFromCache)) {
|
if (!isDefined(activityTargetFromCache)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@ -37,7 +47,9 @@ export const useActivityTargetObjectRecords = (activity: Activity) => {
|
|||||||
const correspondingObjectMetadataItem = objectMetadataItems.find(
|
const correspondingObjectMetadataItem = objectMetadataItems.find(
|
||||||
(objectMetadataItem) =>
|
(objectMetadataItem) =>
|
||||||
isDefined(activityTargetFromCache[objectMetadataItem.nameSingular]) &&
|
isDefined(activityTargetFromCache[objectMetadataItem.nameSingular]) &&
|
||||||
!objectMetadataItem.isSystem,
|
![CoreObjectNameSingular.Note, CoreObjectNameSingular.Task].includes(
|
||||||
|
objectMetadataItem.nameSingular as CoreObjectNameSingular,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!correspondingObjectMetadataItem) {
|
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 { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { findActivityTargetsOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/findActivityTargetsOperationSignatureFactory';
|
import { findActivityTargetsOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/findActivityTargetsOperationSignatureFactory';
|
||||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
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 { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter';
|
||||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
|
|
||||||
export const useActivityTargetsForTargetableObjects = ({
|
export const useActivityTargetsForTargetableObjects = ({
|
||||||
|
objectNameSingular,
|
||||||
targetableObjects,
|
targetableObjects,
|
||||||
skip,
|
skip,
|
||||||
onCompleted,
|
onCompleted,
|
||||||
}: {
|
}: {
|
||||||
|
objectNameSingular: CoreObjectNameSingular;
|
||||||
targetableObjects: Pick<
|
targetableObjects: Pick<
|
||||||
ActivityTargetableObject,
|
ActivityTargetableObject,
|
||||||
'id' | 'targetObjectNameSingular'
|
'id' | 'targetObjectNameSingular'
|
||||||
>[];
|
>[];
|
||||||
skip?: boolean;
|
skip?: boolean;
|
||||||
onCompleted?: (activityTargets: ActivityTarget[]) => void;
|
onCompleted?: (activityTargets: (TaskTarget | NoteTarget)[]) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const activityTargetsFilter = getActivityTargetsFilter({
|
const activityTargetsFilter = getActivityTargetsFilter({
|
||||||
targetableObjects: targetableObjects,
|
targetableObjects: targetableObjects,
|
||||||
@ -26,13 +30,16 @@ export const useActivityTargetsForTargetableObjects = ({
|
|||||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||||
|
|
||||||
const FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE =
|
const FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE =
|
||||||
findActivityTargetsOperationSignatureFactory({ objectMetadataItems });
|
findActivityTargetsOperationSignatureFactory({
|
||||||
|
objectNameSingular,
|
||||||
|
objectMetadataItems,
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: We want to optimistically remove from this request
|
// 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
|
// 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
|
// See also if we need to update useTimelineActivities
|
||||||
const { records: activityTargets, loading: loadingActivityTargets } =
|
const { records: activityTargets, loading: loadingActivityTargets } =
|
||||||
useFindManyRecords<ActivityTarget>({
|
useFindManyRecords<TaskTarget | NoteTarget>({
|
||||||
skip,
|
skip,
|
||||||
objectNameSingular:
|
objectNameSingular:
|
||||||
FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE.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 { isNonEmptyArray } from '@sniptt/guards';
|
||||||
|
|
||||||
import { CREATE_ONE_ACTIVITY_OPERATION_SIGNATURE } from '@/activities/graphql/operation-signatures/CreateOneActivityOperationSignature';
|
|
||||||
import { ActivityForEditor } from '@/activities/types/ActivityForEditor';
|
import { ActivityForEditor } from '@/activities/types/ActivityForEditor';
|
||||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
@ -13,33 +11,48 @@ import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
|||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
import { useApolloClient } from '@apollo/client';
|
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 { useRecoilCallback } from 'recoil';
|
||||||
import { capitalize } from '~/utils/string/capitalize';
|
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({
|
const { createOneRecord: createOneActivity } = useCreateOneRecord({
|
||||||
objectNameSingular:
|
objectNameSingular: activityObjectNameSingular,
|
||||||
CREATE_ONE_ACTIVITY_OPERATION_SIGNATURE.objectNameSingular,
|
recordGqlFields: createOneActivityOperationSignature.fields,
|
||||||
recordGqlFields: CREATE_ONE_ACTIVITY_OPERATION_SIGNATURE.fields,
|
|
||||||
shouldMatchRootQueryFilter: true,
|
shouldMatchRootQueryFilter: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createManyRecords: createManyActivityTargets } =
|
const { createManyRecords: createManyActivityTargets } = useCreateManyRecords<
|
||||||
useCreateManyRecords<ActivityTarget>({
|
TaskTarget | NoteTarget
|
||||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
>({
|
||||||
shouldMatchRootQueryFilter: true,
|
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||||
});
|
shouldMatchRootQueryFilter: true,
|
||||||
|
});
|
||||||
|
|
||||||
const { objectMetadataItems } = useObjectMetadataItems();
|
const { objectMetadataItems } = useObjectMetadataItems();
|
||||||
|
|
||||||
const { objectMetadataItem: objectMetadataItemActivityTarget } =
|
const { objectMetadataItem: objectMetadataItemActivityTarget } =
|
||||||
useObjectMetadataItem({
|
useObjectMetadataItem({
|
||||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||||
useObjectMetadataItem({
|
useObjectMetadataItem({
|
||||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
objectNameSingular: activityObjectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cache = useApolloClient().cache;
|
const cache = useApolloClient().cache;
|
||||||
@ -52,7 +65,8 @@ export const useCreateActivityInDB = () => {
|
|||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const activityTargetsToCreate = activityToCreate.activityTargets ?? [];
|
const activityTargetsToCreate =
|
||||||
|
activityToCreate.noteTargets ?? activityToCreate.taskTargets ?? [];
|
||||||
|
|
||||||
if (isNonEmptyArray(activityTargetsToCreate)) {
|
if (isNonEmptyArray(activityTargetsToCreate)) {
|
||||||
await createManyActivityTargets(activityTargetsToCreate);
|
await createManyActivityTargets(activityTargetsToCreate);
|
||||||
|
|||||||
@ -1,25 +1,34 @@
|
|||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
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 { 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 { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||||
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
|
||||||
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
|
|
||||||
export const useOpenActivityRightDrawer = () => {
|
export const useOpenActivityRightDrawer = ({
|
||||||
|
objectNameSingular,
|
||||||
|
}: {
|
||||||
|
objectNameSingular: CoreObjectNameSingular;
|
||||||
|
}) => {
|
||||||
const { openRightDrawer, isRightDrawerOpen, rightDrawerPage } =
|
const { openRightDrawer, isRightDrawerOpen, rightDrawerPage } =
|
||||||
useRightDrawer();
|
useRightDrawer();
|
||||||
const [viewableRecordId, setViewableRecordId] = useRecoilState(
|
const [viewableRecordId, setViewableRecordId] = useRecoilState(
|
||||||
viewableRecordIdState,
|
viewableRecordIdState,
|
||||||
);
|
);
|
||||||
const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState);
|
|
||||||
|
const setViewableRecordNameSingular = useSetRecoilState(
|
||||||
|
viewableRecordNameSingularState,
|
||||||
|
);
|
||||||
|
|
||||||
const setHotkeyScope = useSetHotkeyScope();
|
const setHotkeyScope = useSetHotkeyScope();
|
||||||
|
|
||||||
return (activityId: string) => {
|
return (activityId: string) => {
|
||||||
if (
|
if (
|
||||||
isRightDrawerOpen &&
|
isRightDrawerOpen &&
|
||||||
rightDrawerPage === RightDrawerPages.EditActivity &&
|
rightDrawerPage === RightDrawerPages.ViewRecord &&
|
||||||
viewableRecordId === activityId
|
viewableRecordId === activityId
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@ -27,7 +36,7 @@ export const useOpenActivityRightDrawer = () => {
|
|||||||
|
|
||||||
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||||
setViewableRecordId(activityId);
|
setViewableRecordId(activityId);
|
||||||
setActivityIdInDrawer(activityId);
|
setViewableRecordNameSingular(objectNameSingular);
|
||||||
openRightDrawer(RightDrawerPages.EditActivity);
|
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 { activityTargetableEntityArrayState } from '@/activities/states/activityTargetableEntityArrayState';
|
||||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
|
||||||
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
|
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 { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState';
|
||||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
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 { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
|
||||||
import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
|
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';
|
import { ActivityTargetableObject } from '../types/ActivityTargetableEntity';
|
||||||
|
|
||||||
export const useOpenCreateActivityDrawer = () => {
|
export const useOpenCreateActivityDrawer = ({
|
||||||
|
activityObjectNameSingular,
|
||||||
|
}: {
|
||||||
|
activityObjectNameSingular:
|
||||||
|
| CoreObjectNameSingular.Note
|
||||||
|
| CoreObjectNameSingular.Task;
|
||||||
|
}) => {
|
||||||
const { openRightDrawer } = useRightDrawer();
|
const { openRightDrawer } = useRightDrawer();
|
||||||
|
|
||||||
const setHotkeyScope = useSetHotkeyScope();
|
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(
|
const setActivityTargetableEntityArray = useSetRecoilState(
|
||||||
activityTargetableEntityArrayState,
|
activityTargetableEntityArrayState,
|
||||||
);
|
);
|
||||||
const setViewableRecordId = useSetRecoilState(viewableRecordIdState);
|
const setViewableRecordId = useSetRecoilState(viewableRecordIdState);
|
||||||
|
const setViewableRecordNameSingular = useSetRecoilState(
|
||||||
const setIsCreatingActivity = useSetRecoilState(isActivityInCreateModeState);
|
viewableRecordNameSingularState,
|
||||||
|
|
||||||
const setTemporaryActivityForEditor = useSetRecoilState(
|
|
||||||
temporaryActivityForEditorState,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState);
|
const setIsUpsertingActivityInDB = useSetRecoilState(
|
||||||
|
|
||||||
const [, setIsUpsertingActivityInDB] = useRecoilState(
|
|
||||||
isUpsertingActivityInDBState,
|
isUpsertingActivityInDBState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const openCreateActivityDrawer = async ({
|
const openCreateActivityDrawer = async ({
|
||||||
type,
|
|
||||||
targetableObjects,
|
targetableObjects,
|
||||||
customAssignee,
|
customAssignee,
|
||||||
}: {
|
}: {
|
||||||
type: ActivityType;
|
|
||||||
targetableObjects: ActivityTargetableObject[];
|
targetableObjects: ActivityTargetableObject[];
|
||||||
customAssignee?: WorkspaceMember;
|
customAssignee?: WorkspaceMember;
|
||||||
}) => {
|
}) => {
|
||||||
const { createdActivityInCache } = createActivityInCache({
|
const activity = await createOneActivity({
|
||||||
type,
|
assigneeId: customAssignee?.id,
|
||||||
targetObject: targetableObjects[0],
|
});
|
||||||
customAssignee,
|
|
||||||
|
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 });
|
setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
|
||||||
setViewableRecordId(createdActivityInCache.id);
|
setViewableRecordId(activity.id);
|
||||||
|
setViewableRecordNameSingular(activityObjectNameSingular);
|
||||||
setActivityTargetableEntityArray(targetableObjects ?? []);
|
setActivityTargetableEntityArray(targetableObjects ?? []);
|
||||||
openRightDrawer(RightDrawerPages.CreateActivity);
|
|
||||||
|
openRightDrawer(RightDrawerPages.ViewRecord);
|
||||||
setIsUpsertingActivityInDB(false);
|
setIsUpsertingActivityInDB(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { useApolloClient } from '@apollo/client';
|
import { useApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
import { findActivitiesOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/findActivitiesOperationSignatureFactory';
|
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 { 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 { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
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 { sortByAscString } from '~/utils/array/sortByAscString';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const usePrepareFindManyActivitiesQuery = () => {
|
export const usePrepareFindManyActivitiesQuery = ({
|
||||||
|
activityObjectNameSingular,
|
||||||
|
}: {
|
||||||
|
activityObjectNameSingular: CoreObjectNameSingular;
|
||||||
|
}) => {
|
||||||
const { objectMetadataItem: objectMetadataItemActivity } =
|
const { objectMetadataItem: objectMetadataItemActivity } =
|
||||||
useObjectMetadataItem({
|
useObjectMetadataItem({
|
||||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
objectNameSingular: activityObjectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const getActivityFromCache = useGetRecordFromCache({
|
const getActivityFromCache = useGetRecordFromCache({
|
||||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
objectNameSingular: activityObjectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cache = useApolloClient().cache;
|
const cache = useApolloClient().cache;
|
||||||
@ -39,7 +45,7 @@ export const usePrepareFindManyActivitiesQuery = () => {
|
|||||||
}: {
|
}: {
|
||||||
additionalFilter?: Record<string, unknown>;
|
additionalFilter?: Record<string, unknown>;
|
||||||
targetableObject: ActivityTargetableObject;
|
targetableObject: ActivityTargetableObject;
|
||||||
shouldActivityBeExcluded?: (activityTarget: Activity) => boolean;
|
shouldActivityBeExcluded?: (activityTarget: Task | Note) => boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const targetableObjectMetadataItem = objectMetadataItems.find(
|
const targetableObjectMetadataItem = objectMetadataItems.find(
|
||||||
(objectMetadataItem) =>
|
(objectMetadataItem) =>
|
||||||
@ -60,8 +66,10 @@ export const usePrepareFindManyActivitiesQuery = () => {
|
|||||||
cache,
|
cache,
|
||||||
});
|
});
|
||||||
|
|
||||||
const activityTargets: ActivityTarget[] =
|
const activityTargets: (TaskTarget | NoteTarget)[] =
|
||||||
targetableObjectRecord?.activityTargets ?? [];
|
targetableObjectRecord?.taskTargets ??
|
||||||
|
targetableObjectRecord?.noteTargets ??
|
||||||
|
[];
|
||||||
|
|
||||||
const activityTargetIds = [
|
const activityTargetIds = [
|
||||||
...new Set(
|
...new Set(
|
||||||
@ -71,7 +79,7 @@ export const usePrepareFindManyActivitiesQuery = () => {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
const activities: Activity[] = activityTargetIds
|
const activities: (Task | Note)[] = activityTargetIds
|
||||||
.map((activityTargetId) => {
|
.map((activityTargetId) => {
|
||||||
const activityTarget = activityTargets.find(
|
const activityTarget = activityTargets.find(
|
||||||
(activityTarget) => activityTarget.id === activityTargetId,
|
(activityTarget) => activityTarget.id === activityTargetId,
|
||||||
@ -81,7 +89,7 @@ export const usePrepareFindManyActivitiesQuery = () => {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return getActivityFromCache<Activity>(activityTarget.activityId);
|
return getActivityFromCache<Task | Note>(activityTarget.activityId);
|
||||||
})
|
})
|
||||||
.filter(isDefined);
|
.filter(isDefined);
|
||||||
|
|
||||||
@ -103,7 +111,10 @@ export const usePrepareFindManyActivitiesQuery = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const FIND_ACTIVITIES_OPERATION_SIGNATURE =
|
const FIND_ACTIVITIES_OPERATION_SIGNATURE =
|
||||||
findActivitiesOperationSignatureFactory({ objectMetadataItems });
|
findActivitiesOperationSignatureFactory({
|
||||||
|
objectNameSingular: activityObjectNameSingular,
|
||||||
|
objectMetadataItems,
|
||||||
|
});
|
||||||
|
|
||||||
upsertFindManyActivitiesInCache({
|
upsertFindManyActivitiesInCache({
|
||||||
objectRecordsToOverwrite: filteredActivities,
|
objectRecordsToOverwrite: filteredActivities,
|
||||||
|
|||||||
@ -1,20 +1,25 @@
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { usePrepareFindManyActivitiesQuery } from '@/activities/hooks/usePrepareFindManyActivitiesQuery';
|
import { usePrepareFindManyActivitiesQuery } from '@/activities/hooks/usePrepareFindManyActivitiesQuery';
|
||||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
import { objectShowPageTargetableObjectState } from '@/activities/timelineActivities/states/objectShowPageTargetableObjectIdState';
|
||||||
import { Activity } from '@/activities/types/Activity';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
// This hook should only be executed if the normalized cache is up-to-date
|
// 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
|
// It will take a targetableObject and prepare the queries for the activities
|
||||||
// based on the activityTargets of the targetableObject
|
// based on the activityTargets of the targetableObject
|
||||||
export const useRefreshShowPageFindManyActivitiesQueries = () => {
|
export const useRefreshShowPageFindManyActivitiesQueries = ({
|
||||||
|
activityObjectNameSingular,
|
||||||
|
}: {
|
||||||
|
activityObjectNameSingular: CoreObjectNameSingular;
|
||||||
|
}) => {
|
||||||
const objectShowPageTargetableObject = useRecoilValue(
|
const objectShowPageTargetableObject = useRecoilValue(
|
||||||
objectShowPageTargetableObjectState,
|
objectShowPageTargetableObjectState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { prepareFindManyActivitiesQuery } =
|
const { prepareFindManyActivitiesQuery } = usePrepareFindManyActivitiesQuery({
|
||||||
usePrepareFindManyActivitiesQuery();
|
activityObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
const refreshShowPageFindManyActivitiesQueries = () => {
|
const refreshShowPageFindManyActivitiesQueries = () => {
|
||||||
if (isDefined(objectShowPageTargetableObject)) {
|
if (isDefined(objectShowPageTargetableObject)) {
|
||||||
@ -24,21 +29,12 @@ export const useRefreshShowPageFindManyActivitiesQueries = () => {
|
|||||||
prepareFindManyActivitiesQuery({
|
prepareFindManyActivitiesQuery({
|
||||||
targetableObject: objectShowPageTargetableObject,
|
targetableObject: objectShowPageTargetableObject,
|
||||||
additionalFilter: {
|
additionalFilter: {
|
||||||
completedAt: { is: 'NULL' },
|
status: { eq: 'TODO' },
|
||||||
type: { eq: 'Task' },
|
|
||||||
},
|
|
||||||
shouldActivityBeExcluded: (activity: Activity) => {
|
|
||||||
return activity.type !== 'Task';
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
prepareFindManyActivitiesQuery({
|
prepareFindManyActivitiesQuery({
|
||||||
targetableObject: objectShowPageTargetableObject,
|
targetableObject: objectShowPageTargetableObject,
|
||||||
additionalFilter: {
|
additionalFilter: {},
|
||||||
type: { eq: 'Note' },
|
|
||||||
},
|
|
||||||
shouldActivityBeExcluded: (activity: Activity) => {
|
|
||||||
return activity.type !== 'Note';
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,50 +1,58 @@
|
|||||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB';
|
import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB';
|
||||||
import { useRefreshShowPageFindManyActivitiesQueries } from '@/activities/hooks/useRefreshShowPageFindManyActivitiesQueries';
|
import { useRefreshShowPageFindManyActivitiesQueries } from '@/activities/hooks/useRefreshShowPageFindManyActivitiesQueries';
|
||||||
import { activityIdInDrawerState } from '@/activities/states/activityIdInDrawerState';
|
|
||||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
||||||
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
|
import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState';
|
||||||
import { objectShowPageTargetableObjectState } from '@/activities/timeline/states/objectShowPageTargetableObjectIdState';
|
import { objectShowPageTargetableObjectState } from '@/activities/timelineActivities/states/objectShowPageTargetableObjectIdState';
|
||||||
import { Activity } from '@/activities/types/Activity';
|
import { Note } from '@/activities/types/Note';
|
||||||
|
import { Task } from '@/activities/types/Task';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const useUpsertActivity = () => {
|
export const useUpsertActivity = ({
|
||||||
const [isActivityInCreateMode, setIsActivityInCreateMode] = useRecoilState(
|
activityObjectNameSingular,
|
||||||
isActivityInCreateModeState,
|
}: {
|
||||||
);
|
activityObjectNameSingular:
|
||||||
|
| CoreObjectNameSingular.Task
|
||||||
|
| CoreObjectNameSingular.Note;
|
||||||
|
}) => {
|
||||||
|
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
|
||||||
|
|
||||||
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Activity>({
|
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<
|
||||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
Task | Note
|
||||||
|
>({
|
||||||
|
objectNameSingular: activityObjectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { createActivityInDB } = useCreateActivityInDB();
|
const { createActivityInDB } = useCreateActivityInDB({
|
||||||
|
activityObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
const [, setIsUpsertingActivityInDB] = useRecoilState(
|
const [, setIsUpsertingActivityInDB] = useRecoilState(
|
||||||
isUpsertingActivityInDBState,
|
isUpsertingActivityInDBState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState);
|
|
||||||
|
|
||||||
const objectShowPageTargetableObject = useRecoilValue(
|
const objectShowPageTargetableObject = useRecoilValue(
|
||||||
objectShowPageTargetableObjectState,
|
objectShowPageTargetableObjectState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { refreshShowPageFindManyActivitiesQueries } =
|
const { refreshShowPageFindManyActivitiesQueries } =
|
||||||
useRefreshShowPageFindManyActivitiesQueries();
|
useRefreshShowPageFindManyActivitiesQueries({
|
||||||
|
activityObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
const upsertActivity = async ({
|
const upsertActivity = async ({
|
||||||
activity,
|
activity,
|
||||||
input,
|
input,
|
||||||
}: {
|
}: {
|
||||||
activity: Activity;
|
activity: Task | Note;
|
||||||
input: Partial<Activity>;
|
input: Partial<Task | Note>;
|
||||||
}) => {
|
}) => {
|
||||||
setIsUpsertingActivityInDB(true);
|
setIsUpsertingActivityInDB(true);
|
||||||
if (isActivityInCreateMode) {
|
if (isActivityInCreateMode) {
|
||||||
const activityToCreate: Activity = {
|
const activityToCreate: Partial<Task | Note> = {
|
||||||
...activity,
|
...activity,
|
||||||
...input,
|
...input,
|
||||||
};
|
};
|
||||||
@ -54,9 +62,6 @@ export const useUpsertActivity = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await createActivityInDB(activityToCreate);
|
await createActivityInDB(activityToCreate);
|
||||||
|
|
||||||
setActivityIdInDrawer(activityToCreate.id);
|
|
||||||
setIsActivityInCreateMode(false);
|
|
||||||
} else {
|
} else {
|
||||||
await updateOneActivity?.({
|
await updateOneActivity?.({
|
||||||
idToUpdate: activity.id,
|
idToUpdate: activity.id,
|
||||||
|
|||||||
@ -6,10 +6,13 @@ import { v4 } from 'uuid';
|
|||||||
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
|
||||||
import { ActivityTargetObjectRecordEffect } from '@/activities/inline-cell/components/ActivityTargetObjectRecordEffect';
|
import { ActivityTargetObjectRecordEffect } from '@/activities/inline-cell/components/ActivityTargetObjectRecordEffect';
|
||||||
import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState';
|
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 { 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 { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||||
|
import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useCreateManyRecordsInCache } from '@/object-record/cache/hooks/useCreateManyRecordsInCache';
|
import { useCreateManyRecordsInCache } from '@/object-record/cache/hooks/useCreateManyRecordsInCache';
|
||||||
@ -35,13 +38,17 @@ const StyledSelectContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
type ActivityTargetInlineCellEditModeProps = {
|
type ActivityTargetInlineCellEditModeProps = {
|
||||||
activity: Activity;
|
activity: Task | Note;
|
||||||
activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[];
|
activityTargetWithTargetRecords: ActivityTargetWithTargetRecord[];
|
||||||
|
activityObjectNameSingular:
|
||||||
|
| CoreObjectNameSingular.Note
|
||||||
|
| CoreObjectNameSingular.Task;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ActivityTargetInlineCellEditMode = ({
|
export const ActivityTargetInlineCellEditMode = ({
|
||||||
activity,
|
activity,
|
||||||
activityTargetWithTargetRecords,
|
activityTargetWithTargetRecords,
|
||||||
|
activityObjectNameSingular,
|
||||||
}: ActivityTargetInlineCellEditModeProps) => {
|
}: ActivityTargetInlineCellEditModeProps) => {
|
||||||
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
|
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
|
||||||
const relationPickerScopeId = `relation-picker-${activity.id}`;
|
const relationPickerScopeId = `relation-picker-${activity.id}`;
|
||||||
@ -53,24 +60,27 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { createManyRecords: createManyActivityTargets } =
|
const { createManyRecords: createManyActivityTargets } = useCreateManyRecords<
|
||||||
useCreateManyRecords<ActivityTarget>({
|
NoteTarget | TaskTarget
|
||||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
>({
|
||||||
});
|
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||||
|
});
|
||||||
|
|
||||||
const { deleteManyRecords: deleteManyActivityTargets } = useDeleteManyRecords(
|
const { deleteManyRecords: deleteManyActivityTargets } = useDeleteManyRecords(
|
||||||
{
|
{
|
||||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const { closeInlineCell: closeEditableField } = useInlineCell();
|
const { closeInlineCell: closeEditableField } = useInlineCell();
|
||||||
|
|
||||||
const { upsertActivity } = useUpsertActivity();
|
const { upsertActivity } = useUpsertActivity({
|
||||||
|
activityObjectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
const { objectMetadataItem: objectMetadataItemActivityTarget } =
|
const { objectMetadataItem: objectMetadataItemActivityTarget } =
|
||||||
useObjectMetadataItem({
|
useObjectMetadataItem({
|
||||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||||
});
|
});
|
||||||
|
|
||||||
const setActivityFromStore = useSetRecoilState(
|
const setActivityFromStore = useSetRecoilState(
|
||||||
@ -78,8 +88,8 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { createManyRecordsInCache: createManyActivityTargetsInCache } =
|
const { createManyRecordsInCache: createManyActivityTargetsInCache } =
|
||||||
useCreateManyRecordsInCache<ActivityTarget>({
|
useCreateManyRecordsInCache<NoteTarget | TaskTarget>({
|
||||||
objectNameSingular: CoreObjectNameSingular.ActivityTarget,
|
objectNameSingular: getJoinObjectNameSingular(activityObjectNameSingular),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = useRecoilCallback(
|
const handleSubmit = useRecoilCallback(
|
||||||
@ -166,12 +176,19 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
const fieldNameWithIdSuffix = getActivityTargetObjectFieldIdName({
|
const fieldNameWithIdSuffix = getActivityTargetObjectFieldIdName({
|
||||||
nameSingular: record.objectMetadataItem.nameSingular,
|
nameSingular: record.objectMetadataItem.nameSingular,
|
||||||
});
|
});
|
||||||
const newActivityTarget = prefillRecord<ActivityTarget>({
|
|
||||||
|
const newActivityTarget = prefillRecord<NoteTarget | TaskTarget>({
|
||||||
objectMetadataItem: objectMetadataItemActivityTarget,
|
objectMetadataItem: objectMetadataItemActivityTarget,
|
||||||
input: {
|
input: {
|
||||||
id: newActivityTargetId,
|
id: newActivityTargetId,
|
||||||
activityId: activity.id,
|
taskId:
|
||||||
activity,
|
activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||||
|
? activity.id
|
||||||
|
: null,
|
||||||
|
noteId:
|
||||||
|
activityObjectNameSingular === CoreObjectNameSingular.Note
|
||||||
|
? activity.id
|
||||||
|
: null,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
[fieldName]: record.record,
|
[fieldName]: record.record,
|
||||||
@ -186,7 +203,11 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
upsertActivity({
|
upsertActivity({
|
||||||
activity,
|
activity,
|
||||||
input: {
|
input: {
|
||||||
activityTargets: activityTargetsAfterUpdate,
|
[activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||||
|
? 'taskTargets'
|
||||||
|
: activityObjectNameSingular === CoreObjectNameSingular.Note
|
||||||
|
? 'noteTargets'
|
||||||
|
: '']: activityTargetsAfterUpdate,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -219,7 +240,11 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
upsertActivity({
|
upsertActivity({
|
||||||
activity,
|
activity,
|
||||||
input: {
|
input: {
|
||||||
activityTargets: activityTargetsAfterUpdate,
|
[activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||||
|
? 'taskTargets'
|
||||||
|
: activityObjectNameSingular === CoreObjectNameSingular.Note
|
||||||
|
? 'noteTargets'
|
||||||
|
: '']: activityTargetsAfterUpdate,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -241,6 +266,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
|||||||
objectMetadataItemActivityTarget,
|
objectMetadataItemActivityTarget,
|
||||||
relationPickerScopeId,
|
relationPickerScopeId,
|
||||||
upsertActivity,
|
upsertActivity,
|
||||||
|
activityObjectNameSingular,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,9 @@ import { IconArrowUpRight, IconPencil } from 'twenty-ui';
|
|||||||
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
import { ActivityTargetChips } from '@/activities/components/ActivityTargetChips';
|
||||||
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
import { useActivityTargetObjectRecords } from '@/activities/hooks/useActivityTargetObjectRecords';
|
||||||
import { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode';
|
import { ActivityTargetInlineCellEditMode } from '@/activities/inline-cell/components/ActivityTargetInlineCellEditMode';
|
||||||
import { Activity } from '@/activities/types/Activity';
|
|
||||||
import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope';
|
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 { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||||
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
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';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
|
|
||||||
type ActivityTargetsInlineCellProps = {
|
type ActivityTargetsInlineCellProps = {
|
||||||
activity: Activity;
|
activity: Task | Note;
|
||||||
showLabel?: boolean;
|
showLabel?: boolean;
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
|
activityObjectNameSingular:
|
||||||
|
| CoreObjectNameSingular.Note
|
||||||
|
| CoreObjectNameSingular.Task;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ActivityTargetsInlineCell = ({
|
export const ActivityTargetsInlineCell = ({
|
||||||
@ -28,9 +32,13 @@ export const ActivityTargetsInlineCell = ({
|
|||||||
showLabel = true,
|
showLabel = true,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
readonly,
|
readonly,
|
||||||
|
activityObjectNameSingular,
|
||||||
}: ActivityTargetsInlineCellProps) => {
|
}: ActivityTargetsInlineCellProps) => {
|
||||||
const { activityTargetObjectRecords } =
|
const { activityTargetObjectRecords } = useActivityTargetObjectRecords(
|
||||||
useActivityTargetObjectRecords(activity);
|
activity,
|
||||||
|
activityObjectNameSingular,
|
||||||
|
);
|
||||||
|
|
||||||
const { closeInlineCell } = useInlineCell();
|
const { closeInlineCell } = useInlineCell();
|
||||||
|
|
||||||
const { fieldDefinition } = useContext(FieldContext);
|
const { fieldDefinition } = useContext(FieldContext);
|
||||||
@ -45,9 +53,9 @@ export const ActivityTargetsInlineCell = ({
|
|||||||
|
|
||||||
const { FieldContextProvider: ActivityTargetsContextProvider } =
|
const { FieldContextProvider: ActivityTargetsContextProvider } =
|
||||||
useFieldContext({
|
useFieldContext({
|
||||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
objectNameSingular: activityObjectNameSingular,
|
||||||
objectRecordId: activity.id,
|
objectRecordId: activity.id,
|
||||||
fieldMetadataName: 'activityTargets',
|
fieldMetadataName: fieldDefinition.metadata.fieldName,
|
||||||
fieldPosition: 3,
|
fieldPosition: 3,
|
||||||
overridenIsFieldEmpty: activityTargetObjectRecords.length === 0,
|
overridenIsFieldEmpty: activityTargetObjectRecords.length === 0,
|
||||||
});
|
});
|
||||||
@ -70,6 +78,7 @@ export const ActivityTargetsInlineCell = ({
|
|||||||
<ActivityTargetInlineCellEditMode
|
<ActivityTargetInlineCellEditMode
|
||||||
activity={activity}
|
activity={activity}
|
||||||
activityTargetWithTargetRecords={activityTargetObjectRecords}
|
activityTargetWithTargetRecords={activityTargetObjectRecords}
|
||||||
|
activityObjectNameSingular={activityObjectNameSingular}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
label="Relations"
|
label="Relations"
|
||||||
|
|||||||
@ -1,16 +1,11 @@
|
|||||||
import { useMemo } from 'react';
|
|
||||||
import { useTheme } from '@emotion/react';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { IconComment } from 'twenty-ui';
|
|
||||||
|
|
||||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||||
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
||||||
import { Note } from '@/activities/types/Note';
|
import { Note } from '@/activities/types/Note';
|
||||||
import { getActivityPreview } from '@/activities/utils/getActivityPreview';
|
import { getActivityPreview } from '@/activities/utils/getActivityPreview';
|
||||||
import {
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
FieldContext,
|
import { useFieldContext } from '@/object-record/hooks/useFieldContext';
|
||||||
GenericFieldContextType,
|
|
||||||
} from '@/object-record/record-field/contexts/FieldContext';
|
|
||||||
|
|
||||||
const StyledCard = styled.div<{ isSingleNote: boolean }>`
|
const StyledCard = styled.div<{ isSingleNote: boolean }>`
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@ -66,14 +61,6 @@ const StyledFooter = styled.div`
|
|||||||
width: calc(100% - ${({ theme }) => theme.spacing(4)});
|
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 = ({
|
export const NoteCard = ({
|
||||||
note,
|
note,
|
||||||
isSingleNote,
|
isSingleNote,
|
||||||
@ -81,34 +68,37 @@ export const NoteCard = ({
|
|||||||
note: Note;
|
note: Note;
|
||||||
isSingleNote: boolean;
|
isSingleNote: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const openActivityRightDrawer = useOpenActivityRightDrawer({
|
||||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
objectNameSingular: CoreObjectNameSingular.Note,
|
||||||
|
});
|
||||||
const body = getActivityPreview(note.body);
|
const body = getActivityPreview(note.body);
|
||||||
|
|
||||||
const fieldContext = useMemo(
|
const { FieldContextProvider: NoteTargetsContextProvider } = useFieldContext({
|
||||||
() => ({ recoilScopeId: note?.id ?? '' }),
|
objectNameSingular: CoreObjectNameSingular.Note,
|
||||||
[note?.id],
|
objectRecordId: note.id,
|
||||||
);
|
fieldMetadataName: 'noteTargets',
|
||||||
|
fieldPosition: 0,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FieldContext.Provider value={fieldContext as GenericFieldContextType}>
|
<StyledCard isSingleNote={isSingleNote}>
|
||||||
<StyledCard isSingleNote={isSingleNote}>
|
<StyledCardDetailsContainer
|
||||||
<StyledCardDetailsContainer
|
onClick={() => openActivityRightDrawer(note.id)}
|
||||||
onClick={() => openActivityRightDrawer(note.id)}
|
>
|
||||||
>
|
<StyledNoteTitle>{note.title ?? 'Task Title'}</StyledNoteTitle>
|
||||||
<StyledNoteTitle>{note.title ?? 'Task Title'}</StyledNoteTitle>
|
<StyledCardContent>{body}</StyledCardContent>
|
||||||
<StyledCardContent>{body}</StyledCardContent>
|
</StyledCardDetailsContainer>
|
||||||
</StyledCardDetailsContainer>
|
<StyledFooter>
|
||||||
<StyledFooter>
|
{NoteTargetsContextProvider && (
|
||||||
<ActivityTargetsInlineCell activity={note} readonly />
|
<NoteTargetsContextProvider>
|
||||||
{note.comments && note.comments.length > 0 && (
|
<ActivityTargetsInlineCell
|
||||||
<StyledCommentIcon>
|
activity={note}
|
||||||
<IconComment size={theme.icon.size.md} />
|
activityObjectNameSingular={CoreObjectNameSingular.Note}
|
||||||
{note.comments.length}
|
readonly
|
||||||
</StyledCommentIcon>
|
/>
|
||||||
)}
|
</NoteTargetsContextProvider>
|
||||||
</StyledFooter>
|
)}
|
||||||
</StyledCard>
|
</StyledFooter>
|
||||||
</FieldContext.Provider>
|
</StyledCard>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateAct
|
|||||||
import { NoteList } from '@/activities/notes/components/NoteList';
|
import { NoteList } from '@/activities/notes/components/NoteList';
|
||||||
import { useNotes } from '@/activities/notes/hooks/useNotes';
|
import { useNotes } from '@/activities/notes/hooks/useNotes';
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
|
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
|
||||||
import {
|
import {
|
||||||
@ -31,7 +32,9 @@ export const Notes = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { notes, loading } = useNotes(targetableObject);
|
const { notes, loading } = useNotes(targetableObject);
|
||||||
|
|
||||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
const openCreateActivity = useOpenCreateActivityDrawer({
|
||||||
|
activityObjectNameSingular: CoreObjectNameSingular.Note,
|
||||||
|
});
|
||||||
|
|
||||||
const isNotesEmpty = !notes || notes.length === 0;
|
const isNotesEmpty = !notes || notes.length === 0;
|
||||||
|
|
||||||
@ -60,7 +63,6 @@ export const Notes = ({
|
|||||||
variant="secondary"
|
variant="secondary"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openCreateActivity({
|
openCreateActivity({
|
||||||
type: 'Note',
|
|
||||||
targetableObjects: [targetableObject],
|
targetableObjects: [targetableObject],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -82,7 +84,6 @@ export const Notes = ({
|
|||||||
title="Add note"
|
title="Add note"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openCreateActivity({
|
openCreateActivity({
|
||||||
type: 'Note',
|
|
||||||
targetableObjects: [targetableObject],
|
targetableObjects: [targetableObject],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,9 +16,7 @@ jest.mock('recoil', () => {
|
|||||||
...actualRecoil,
|
...actualRecoil,
|
||||||
useRecoilState: jest.fn(() => {
|
useRecoilState: jest.fn(() => {
|
||||||
const mockCurrentNotesQueryVariables = {
|
const mockCurrentNotesQueryVariables = {
|
||||||
filter: {
|
filter: {},
|
||||||
type: { eq: 'Note' },
|
|
||||||
},
|
|
||||||
orderBy: 'mockOrderBy',
|
orderBy: 'mockOrderBy',
|
||||||
};
|
};
|
||||||
return [mockCurrentNotesQueryVariables, jest.fn()];
|
return [mockCurrentNotesQueryVariables, jest.fn()];
|
||||||
|
|||||||
@ -3,26 +3,26 @@ import { useRecoilState } from 'recoil';
|
|||||||
|
|
||||||
import { useActivities } from '@/activities/hooks/useActivities';
|
import { useActivities } from '@/activities/hooks/useActivities';
|
||||||
import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState';
|
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 { Note } from '@/activities/types/Note';
|
||||||
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
|
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||||
|
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { ActivityTargetableObject } from '../../types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '../../types/ActivityTargetableEntity';
|
||||||
|
|
||||||
export const useNotes = (targetableObject: ActivityTargetableObject) => {
|
export const useNotes = (targetableObject: ActivityTargetableObject) => {
|
||||||
const notesQueryVariables = useMemo(
|
const notesQueryVariables = useMemo(
|
||||||
() =>
|
() =>
|
||||||
({
|
({
|
||||||
filter: {
|
filter: {},
|
||||||
type: { eq: 'Note' },
|
|
||||||
},
|
|
||||||
orderBy: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
orderBy: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
||||||
}) as RecordGqlOperationVariables,
|
}) as RecordGqlOperationVariables,
|
||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const { activities, loading } = useActivities({
|
const { activities, loading } = useActivities<Note>({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Note,
|
||||||
activitiesFilters: notesQueryVariables.filter ?? {},
|
activitiesFilters: notesQueryVariables.filter ?? {},
|
||||||
activitiesOrderByVariables: notesQueryVariables.orderBy ?? [{}],
|
activitiesOrderByVariables: notesQueryVariables.orderBy ?? [{}],
|
||||||
targetableObjects: [targetableObject],
|
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 { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
|
||||||
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
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 { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
import { mockedTasks } from '~/testing/mock-data/activities';
|
import { mockedTasks } from '~/testing/mock-data/tasks';
|
||||||
|
|
||||||
const meta: Meta<typeof TaskGroups> = {
|
const meta: Meta<typeof TaskGroups> = {
|
||||||
title: 'Modules/Activity/TaskGroups',
|
title: 'Modules/Activity/TaskGroups',
|
||||||
@ -25,9 +24,6 @@ const meta: Meta<typeof TaskGroups> = {
|
|||||||
ObjectMetadataItemsDecorator,
|
ObjectMetadataItemsDecorator,
|
||||||
SnackBarDecorator,
|
SnackBarDecorator,
|
||||||
],
|
],
|
||||||
parameters: {
|
|
||||||
customRecoilScopeContext: TasksRecoilScopeContext,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
@ -39,7 +35,7 @@ export const WithTasks: Story = {
|
|||||||
args: {
|
args: {
|
||||||
targetableObjects: [
|
targetableObjects: [
|
||||||
{
|
{
|
||||||
id: mockedTasks[0].authorId,
|
id: mockedTasks[0].taskTargets?.[0].personId,
|
||||||
targetObjectNameSingular: 'person',
|
targetObjectNameSingular: 'person',
|
||||||
},
|
},
|
||||||
] as ActivityTargetableObject[],
|
] as ActivityTargetableObject[],
|
||||||
|
|||||||
@ -2,168 +2,10 @@ import { Meta, StoryObj } from '@storybook/react';
|
|||||||
import { ComponentDecorator } from 'twenty-ui';
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
|
|
||||||
import { TaskList } from '@/activities/tasks/components/TaskList';
|
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 { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
|
import { mockedTasks } from '~/testing/mock-data/tasks';
|
||||||
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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const meta: Meta<typeof TaskList> = {
|
const meta: Meta<typeof TaskList> = {
|
||||||
title: 'Modules/Activity/TaskList',
|
title: 'Modules/Activity/TaskList',
|
||||||
@ -171,7 +13,7 @@ const meta: Meta<typeof TaskList> = {
|
|||||||
decorators: [MemoryRouterDecorator, ComponentDecorator, SnackBarDecorator],
|
decorators: [MemoryRouterDecorator, ComponentDecorator, SnackBarDecorator],
|
||||||
args: {
|
args: {
|
||||||
title: 'Tasks',
|
title: 'Tasks',
|
||||||
tasks: mockedActivities,
|
tasks: mockedTasks,
|
||||||
},
|
},
|
||||||
parameters: {
|
parameters: {
|
||||||
msw: graphqlMocks,
|
msw: graphqlMocks,
|
||||||
@ -184,6 +26,6 @@ type Story = StoryObj<typeof TaskList>;
|
|||||||
export const Default: Story = {
|
export const Default: Story = {
|
||||||
args: {
|
args: {
|
||||||
title: 'Tasks',
|
title: 'Tasks',
|
||||||
tasks: mockedActivities,
|
tasks: mockedTasks,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { IconPlus } from 'twenty-ui';
|
|||||||
|
|
||||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
|
|
||||||
export const AddTaskButton = ({
|
export const AddTaskButton = ({
|
||||||
@ -10,7 +11,9 @@ export const AddTaskButton = ({
|
|||||||
}: {
|
}: {
|
||||||
activityTargetableObjects?: ActivityTargetableObject[];
|
activityTargetableObjects?: ActivityTargetableObject[];
|
||||||
}) => {
|
}) => {
|
||||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
const openCreateActivity = useOpenCreateActivityDrawer({
|
||||||
|
activityObjectNameSingular: CoreObjectNameSingular.Task,
|
||||||
|
});
|
||||||
|
|
||||||
if (!isNonEmptyArray(activityTargetableObjects)) {
|
if (!isNonEmptyArray(activityTargetableObjects)) {
|
||||||
return <></>;
|
return <></>;
|
||||||
@ -24,7 +27,6 @@ export const AddTaskButton = ({
|
|||||||
title="Add task"
|
title="Add task"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openCreateActivity({
|
openCreateActivity({
|
||||||
type: 'Task',
|
|
||||||
targetableObjects: activityTargetableObjects,
|
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 styled from '@emotion/styled';
|
||||||
|
|
||||||
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
|
||||||
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
||||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -21,11 +19,9 @@ export const ObjectTasks = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<RecoilScope CustomRecoilScopeContext={TasksRecoilScopeContext}>
|
<ObjectFilterDropdownScope filterScopeId="entity-tasks-filter-scope">
|
||||||
<ObjectFilterDropdownScope filterScopeId="entity-tasks-filter-scope">
|
<TaskGroups targetableObjects={[targetableObject]} showAddButton />
|
||||||
<TaskGroups targetableObjects={[targetableObject]} showAddButton />
|
</ObjectFilterDropdownScope>
|
||||||
</ObjectFilterDropdownScope>
|
|
||||||
</RecoilScope>
|
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
|
import { PageAddButton } from '@/ui/layout/page/PageAddButton';
|
||||||
|
|
||||||
export const PageAddTaskButton = () => {
|
export const PageAddTaskButton = () => {
|
||||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
const openCreateActivity = useOpenCreateActivityDrawer({
|
||||||
|
activityObjectNameSingular: CoreObjectNameSingular.Task,
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: fetch workspace member from filter here
|
// TODO: fetch workspace member from filter here
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
openCreateActivity({
|
openCreateActivity({
|
||||||
type: 'Task',
|
|
||||||
targetableObjects: [],
|
targetableObjects: [],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -18,6 +18,9 @@ import {
|
|||||||
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
|
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
|
||||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
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 { AddTaskButton } from './AddTaskButton';
|
||||||
import { TaskList } from './TaskList';
|
import { TaskList } from './TaskList';
|
||||||
|
|
||||||
@ -33,37 +36,27 @@ type TaskGroupsProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const TaskGroups = ({
|
export const TaskGroups = ({
|
||||||
filterDropdownId,
|
|
||||||
targetableObjects,
|
targetableObjects,
|
||||||
showAddButton,
|
showAddButton,
|
||||||
}: TaskGroupsProps) => {
|
}: TaskGroupsProps) => {
|
||||||
const {
|
const { tasks, tasksLoading } = useTasks({
|
||||||
todayOrPreviousTasks,
|
|
||||||
upcomingTasks,
|
|
||||||
unscheduledTasks,
|
|
||||||
completedTasks,
|
|
||||||
incompleteTasksLoading,
|
|
||||||
completeTasksLoading,
|
|
||||||
} = useTasks({
|
|
||||||
filterDropdownId: filterDropdownId,
|
|
||||||
targetableObjects: targetableObjects ?? [],
|
targetableObjects: targetableObjects ?? [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
const openCreateActivity = useOpenCreateActivityDrawer({
|
||||||
|
activityObjectNameSingular: CoreObjectNameSingular.Task,
|
||||||
|
});
|
||||||
|
|
||||||
const { activeTabIdState } = useTabList(TASKS_TAB_LIST_COMPONENT_ID);
|
const { activeTabIdState } = useTabList(TASKS_TAB_LIST_COMPONENT_ID);
|
||||||
const activeTabId = useRecoilValue(activeTabIdState);
|
const activeTabId = useRecoilValue(activeTabIdState);
|
||||||
|
|
||||||
const isLoading =
|
const isLoading =
|
||||||
(activeTabId !== 'done' && incompleteTasksLoading) ||
|
(activeTabId !== 'done' && tasksLoading) ||
|
||||||
(activeTabId === 'done' && completeTasksLoading);
|
(activeTabId === 'done' && tasksLoading);
|
||||||
|
|
||||||
const isTasksEmpty =
|
const isTasksEmpty =
|
||||||
(activeTabId !== 'done' &&
|
(activeTabId !== 'done' && tasks?.length === 0) ||
|
||||||
todayOrPreviousTasks?.length === 0 &&
|
(activeTabId === 'done' && tasks?.length === 0);
|
||||||
upcomingTasks?.length === 0 &&
|
|
||||||
unscheduledTasks?.length === 0) ||
|
|
||||||
(activeTabId === 'done' && completedTasks?.length === 0);
|
|
||||||
|
|
||||||
if (isLoading && isTasksEmpty) {
|
if (isLoading && isTasksEmpty) {
|
||||||
return <SkeletonLoader />;
|
return <SkeletonLoader />;
|
||||||
@ -90,7 +83,6 @@ export const TaskGroups = ({
|
|||||||
variant={'secondary'}
|
variant={'secondary'}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
openCreateActivity({
|
openCreateActivity({
|
||||||
type: 'Task',
|
|
||||||
targetableObjects: targetableObjects ?? [],
|
targetableObjects: targetableObjects ?? [],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -101,48 +93,19 @@ export const TaskGroups = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
{activeTabId === 'done' ? (
|
{Object.entries(groupBy(tasks, ({ status }) => status)).map(
|
||||||
<TaskList
|
([status, tasksByStatus]: [string, Task[]]) => (
|
||||||
tasks={completedTasks ?? []}
|
|
||||||
button={
|
|
||||||
showAddButton && (
|
|
||||||
<AddTaskButton activityTargetableObjects={targetableObjects} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<TaskList
|
<TaskList
|
||||||
title="Today"
|
key={status}
|
||||||
tasks={todayOrPreviousTasks ?? []}
|
title={status}
|
||||||
|
tasks={tasksByStatus}
|
||||||
button={
|
button={
|
||||||
showAddButton && (
|
showAddButton && (
|
||||||
<AddTaskButton activityTargetableObjects={targetableObjects} />
|
<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>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import { ReactElement } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
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';
|
import { TaskRow } from './TaskRow';
|
||||||
|
|
||||||
type TaskListProps = {
|
type TaskListProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
tasks: Activity[];
|
tasks: Task[];
|
||||||
button?: ReactElement | false;
|
button?: ReactElement | false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,16 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import {
|
import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui';
|
||||||
IconCalendar,
|
|
||||||
IconComment,
|
|
||||||
OverflowingTextWithTooltip,
|
|
||||||
} from 'twenty-ui';
|
|
||||||
|
|
||||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||||
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell';
|
||||||
import { Activity } from '@/activities/types/Activity';
|
|
||||||
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
|
import { getActivitySummary } from '@/activities/utils/getActivitySummary';
|
||||||
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
|
import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox';
|
||||||
import { beautifyExactDate, hasDatePassed } from '~/utils/date-utils';
|
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';
|
import { useCompleteTask } from '../hooks/useCompleteTask';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -52,13 +50,6 @@ const StyledTaskTitle = styled.div<{
|
|||||||
text-overflow: ellipsis;
|
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<{
|
const StyledDueDate = styled.div<{
|
||||||
isPast: boolean;
|
isPast: boolean;
|
||||||
}>`
|
}>`
|
||||||
@ -89,13 +80,22 @@ const StyledCheckboxContainer = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TaskRow = ({ task }: { task: Activity }) => {
|
export const TaskRow = ({ task }: { task: Task }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
const openActivityRightDrawer = useOpenActivityRightDrawer({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Task,
|
||||||
|
});
|
||||||
|
|
||||||
const body = getActivitySummary(task.body);
|
const body = getActivitySummary(task.body);
|
||||||
const { completeTask } = useCompleteTask(task);
|
const { completeTask } = useCompleteTask(task);
|
||||||
|
|
||||||
|
const { FieldContextProvider: TaskTargetsContextProvider } = useFieldContext({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.Task,
|
||||||
|
objectRecordId: task.id,
|
||||||
|
fieldMetadataName: 'taskTargets',
|
||||||
|
fieldPosition: 0,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer
|
<StyledContainer
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -109,33 +109,33 @@ export const TaskRow = ({ task }: { task: Activity }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={!!task.completedAt}
|
checked={task.status === 'DONE'}
|
||||||
shape={CheckboxShape.Rounded}
|
shape={CheckboxShape.Rounded}
|
||||||
onCheckedChange={completeTask}
|
onCheckedChange={completeTask}
|
||||||
/>
|
/>
|
||||||
</StyledCheckboxContainer>
|
</StyledCheckboxContainer>
|
||||||
<StyledTaskTitle completed={task.completedAt !== null}>
|
<StyledTaskTitle completed={task.status === 'DONE'}>
|
||||||
{task.title || <StyledPlaceholder>Task title</StyledPlaceholder>}
|
{task.title || <StyledPlaceholder>Task title</StyledPlaceholder>}
|
||||||
</StyledTaskTitle>
|
</StyledTaskTitle>
|
||||||
<StyledTaskBody>
|
<StyledTaskBody>
|
||||||
<OverflowingTextWithTooltip text={body} />
|
<OverflowingTextWithTooltip text={body} />
|
||||||
{task.comments && task.comments.length > 0 && (
|
|
||||||
<StyledCommentIcon>
|
|
||||||
<IconComment size={theme.icon.size.md} />
|
|
||||||
</StyledCommentIcon>
|
|
||||||
)}
|
|
||||||
</StyledTaskBody>
|
</StyledTaskBody>
|
||||||
</StyledLeftSideContainer>
|
</StyledLeftSideContainer>
|
||||||
<StyledRightSideContainer>
|
<StyledRightSideContainer>
|
||||||
<ActivityTargetsInlineCell
|
{TaskTargetsContextProvider && (
|
||||||
activity={task}
|
<TaskTargetsContextProvider>
|
||||||
showLabel={false}
|
<ActivityTargetsInlineCell
|
||||||
maxWidth={200}
|
activityObjectNameSingular={CoreObjectNameSingular.Task}
|
||||||
readonly
|
activity={task}
|
||||||
/>
|
showLabel={false}
|
||||||
|
maxWidth={200}
|
||||||
|
readonly
|
||||||
|
/>
|
||||||
|
</TaskTargetsContextProvider>
|
||||||
|
)}
|
||||||
<StyledDueDate
|
<StyledDueDate
|
||||||
isPast={
|
isPast={
|
||||||
!!task.dueAt && hasDatePassed(task.dueAt) && !task.completedAt
|
!!task.dueAt && hasDatePassed(task.dueAt) && task.status === 'TODO'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<IconCalendar size={theme.icon.size.md} />
|
<IconCalendar size={theme.icon.size.md} />
|
||||||
|
|||||||
@ -1,16 +1,25 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
|
import { MockedProvider, MockedResponse } from '@apollo/client/testing';
|
||||||
import { act, renderHook } from '@testing-library/react';
|
import { act, renderHook } from '@testing-library/react';
|
||||||
import gql from 'graphql-tag';
|
import gql from 'graphql-tag';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
import { useCompleteTask } from '@/activities/tasks/hooks/useCompleteTask';
|
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 task: Task = {
|
||||||
|
id: '123',
|
||||||
const mockedDate = task.completedAt;
|
status: null,
|
||||||
const toISOStringMock = jest.fn(() => mockedDate);
|
title: 'Test',
|
||||||
global.Date.prototype.toISOString = toISOStringMock;
|
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[] = [
|
const mocks: MockedResponse[] = [
|
||||||
{
|
{
|
||||||
@ -26,7 +35,7 @@ const mocks: MockedResponse[] = [
|
|||||||
reminderAt
|
reminderAt
|
||||||
authorId
|
authorId
|
||||||
title
|
title
|
||||||
completedAt
|
status
|
||||||
updatedAt
|
updatedAt
|
||||||
body
|
body
|
||||||
dueAt
|
dueAt
|
||||||
@ -38,7 +47,7 @@ const mocks: MockedResponse[] = [
|
|||||||
`,
|
`,
|
||||||
variables: {
|
variables: {
|
||||||
idToUpdate: task.id,
|
idToUpdate: task.id,
|
||||||
input: { completedAt: task.completedAt },
|
input: { status: task.status },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
result: jest.fn(() => ({
|
result: jest.fn(() => ({
|
||||||
@ -49,11 +58,11 @@ const mocks: MockedResponse[] = [
|
|||||||
reminderAt: null,
|
reminderAt: null,
|
||||||
authorId: '123',
|
authorId: '123',
|
||||||
title: 'Test',
|
title: 'Test',
|
||||||
completedAt: '2024-03-15T07:33:14.212Z',
|
status: 'DONE',
|
||||||
updatedAt: '2024-03-15T07:33:14.212Z',
|
updatedAt: '2024-03-15T07:33:14.212Z',
|
||||||
body: 'Test',
|
body: 'Test',
|
||||||
dueAt: '2024-03-15T07:33:14.212Z',
|
dueAt: '2024-03-15T07:33:14.212Z',
|
||||||
type: 'Task',
|
type: 'TASK',
|
||||||
id: '123',
|
id: '123',
|
||||||
assigneeId: '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 { renderHook } from '@testing-library/react';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
import { useActivities } from '@/activities/hooks/useActivities';
|
import { useActivities } from '@/activities/hooks/useActivities';
|
||||||
import { useTasks } from '@/activities/tasks/hooks/useTasks';
|
import { useTasks } from '@/activities/tasks/hooks/useTasks';
|
||||||
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope';
|
||||||
|
|
||||||
const completedTasks = [
|
const tasks = [
|
||||||
{
|
{
|
||||||
id: '1',
|
id: '1',
|
||||||
completedAt: '2024-03-15T07:33:14.212Z',
|
status: 'DONE',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '2',
|
id: '2',
|
||||||
completedAt: '2024-03-15T07:33:14.212Z',
|
status: 'DONE',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
completedAt: '2024-03-15T07:33:14.212Z',
|
status: 'DONE',
|
||||||
},
|
},
|
||||||
];
|
|
||||||
|
|
||||||
const unscheduledTasks = [
|
|
||||||
{
|
{
|
||||||
id: '4',
|
id: '4',
|
||||||
},
|
},
|
||||||
];
|
|
||||||
|
|
||||||
const todayOrPreviousTasks = [
|
|
||||||
{
|
{
|
||||||
id: '5',
|
id: '5',
|
||||||
dueAt: '2024-03-15T07:33:14.212Z',
|
dueAt: '2024-03-15T07:33:14.212Z',
|
||||||
@ -38,20 +32,11 @@ const todayOrPreviousTasks = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const useActivitiesMock = jest.fn(
|
const useActivitiesMock = jest.fn(() => {
|
||||||
({
|
return {
|
||||||
activitiesFilters,
|
activities: tasks,
|
||||||
}: {
|
};
|
||||||
activitiesFilters: { completedAt: { is: 'NULL' | 'NOT_NULL' } };
|
});
|
||||||
}) => {
|
|
||||||
const isCompletedFilter = activitiesFilters.completedAt.is === 'NOT_NULL';
|
|
||||||
return {
|
|
||||||
activities: isCompletedFilter
|
|
||||||
? completedTasks
|
|
||||||
: [...todayOrPreviousTasks, ...unscheduledTasks],
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
jest.mock('@/activities/hooks/useActivities', () => ({
|
jest.mock('@/activities/hooks/useActivities', () => ({
|
||||||
useActivities: jest.fn(),
|
useActivities: jest.fn(),
|
||||||
@ -74,10 +59,7 @@ describe('useTasks', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current).toEqual({
|
expect(result.current).toEqual({
|
||||||
todayOrPreviousTasks,
|
tasks: tasks,
|
||||||
upcomingTasks: [],
|
|
||||||
unscheduledTasks,
|
|
||||||
completedTasks,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,23 +1,21 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { Activity } from '@/activities/types/Activity';
|
import { Task } from '@/activities/types/Task';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||||
|
|
||||||
type Task = Pick<Activity, 'id' | 'completedAt'>;
|
|
||||||
|
|
||||||
export const useCompleteTask = (task: Task) => {
|
export const useCompleteTask = (task: Task) => {
|
||||||
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Activity>({
|
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord<Task>({
|
||||||
objectNameSingular: CoreObjectNameSingular.Activity,
|
objectNameSingular: CoreObjectNameSingular.Task,
|
||||||
});
|
});
|
||||||
|
|
||||||
const completeTask = useCallback(
|
const completeTask = useCallback(
|
||||||
async (value: boolean) => {
|
async (value: boolean) => {
|
||||||
const completedAt = value ? new Date().toISOString() : null;
|
const status = value ? 'DONE' : 'TODO';
|
||||||
await updateOneActivity?.({
|
await updateOneActivity?.({
|
||||||
idToUpdate: task.id,
|
idToUpdate: task.id,
|
||||||
updateOneRecordInput: {
|
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 { useActivities } from '@/activities/hooks/useActivities';
|
||||||
import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState';
|
import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timelineActivities/constants/FindManyTimelineActivitiesOrderBy';
|
||||||
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 { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables';
|
import { Task } from '@/activities/types/Task';
|
||||||
import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { parseDate } from '~/utils/date-utils';
|
|
||||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
|
||||||
|
|
||||||
type UseTasksProps = {
|
type UseTasksProps = {
|
||||||
filterDropdownId?: string;
|
|
||||||
targetableObjects: ActivityTargetableObject[];
|
targetableObjects: ActivityTargetableObject[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useTasks = ({
|
export const useTasks = ({ targetableObjects }: UseTasksProps) => {
|
||||||
targetableObjects,
|
const { activities: tasks, loading: tasksLoading } = useActivities<Task>({
|
||||||
filterDropdownId,
|
objectNameSingular: CoreObjectNameSingular.Task,
|
||||||
}: UseTasksProps) => {
|
targetableObjects,
|
||||||
const { selectedFilterState } = useFilterDropdown({
|
activitiesFilters: {},
|
||||||
filterDropdownId,
|
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 {
|
return {
|
||||||
todayOrPreviousTasks: (todayOrPreviousTasks ?? []) as Activity[],
|
tasks: (tasks ?? []) as Task[],
|
||||||
upcomingTasks: (upcomingTasks ?? []) as Activity[],
|
tasksLoading,
|
||||||
unscheduledTasks: (unscheduledTasks ?? []) as Activity[],
|
|
||||||
completedTasks: (completedTasks ?? []) as Activity[],
|
|
||||||
completeTasksLoading,
|
|
||||||
incompleteTasksLoading,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 styled from '@emotion/styled';
|
||||||
|
import { useContext } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { useLinkedObject } from '@/activities/timeline/hooks/useLinkedObject';
|
|
||||||
import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext';
|
import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext';
|
||||||
|
import { useLinkedObject } from '@/activities/timelineActivities/hooks/useLinkedObject';
|
||||||
import { EventIconDynamicComponent } from '@/activities/timelineActivities/rows/components/EventIconDynamicComponent';
|
import { EventIconDynamicComponent } from '@/activities/timelineActivities/rows/components/EventIconDynamicComponent';
|
||||||
import { EventRowDynamicComponent } from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
import { EventRowDynamicComponent } from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||||
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import styled from '@emotion/styled';
|
|||||||
|
|
||||||
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
||||||
import { SkeletonLoader } from '@/activities/components/SkeletonLoader';
|
import { SkeletonLoader } from '@/activities/components/SkeletonLoader';
|
||||||
import { TimelineCreateButtonGroup } from '@/activities/timeline/components/TimelineCreateButtonGroup';
|
|
||||||
import { EventList } from '@/activities/timelineActivities/components/EventList';
|
import { EventList } from '@/activities/timelineActivities/components/EventList';
|
||||||
|
import { TimelineCreateButtonGroup } from '@/activities/timelineActivities/components/TimelineCreateButtonGroup';
|
||||||
import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities';
|
import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities';
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
|
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
|
||||||
@ -35,8 +35,10 @@ const StyledMainContainer = styled.div`
|
|||||||
|
|
||||||
export const TimelineActivities = ({
|
export const TimelineActivities = ({
|
||||||
targetableObject,
|
targetableObject,
|
||||||
|
isInRightDrawer = false,
|
||||||
}: {
|
}: {
|
||||||
targetableObject: ActivityTargetableObject;
|
targetableObject: ActivityTargetableObject;
|
||||||
|
isInRightDrawer?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { timelineActivities, loading, fetchMoreRecords } =
|
const { timelineActivities, loading, fetchMoreRecords } =
|
||||||
useTimelineActivities(targetableObject);
|
useTimelineActivities(targetableObject);
|
||||||
@ -63,7 +65,7 @@ export const TimelineActivities = ({
|
|||||||
There are no activities associated with this record.{' '}
|
There are no activities associated with this record.{' '}
|
||||||
</AnimatedPlaceholderEmptySubTitle>
|
</AnimatedPlaceholderEmptySubTitle>
|
||||||
</AnimatedPlaceholderEmptyTextContainer>
|
</AnimatedPlaceholderEmptyTextContainer>
|
||||||
<TimelineCreateButtonGroup />
|
<TimelineCreateButtonGroup isInRightDrawer={isInRightDrawer} />
|
||||||
</AnimatedPlaceholderEmptyContainer>
|
</AnimatedPlaceholderEmptyContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { useActivities } from '@/activities/hooks/useActivities';
|
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 { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const TimelineActivitiesQueryEffect = ({
|
export const TimelineActivitiesQueryEffect = ({
|
||||||
@ -9,6 +10,8 @@ export const TimelineActivitiesQueryEffect = ({
|
|||||||
targetableObject: ActivityTargetableObject;
|
targetableObject: ActivityTargetableObject;
|
||||||
}) => {
|
}) => {
|
||||||
useActivities({
|
useActivities({
|
||||||
|
objectNameSingular:
|
||||||
|
targetableObject.targetObjectNameSingular as CoreObjectNameSingular,
|
||||||
targetableObjects: [targetableObject],
|
targetableObjects: [targetableObject],
|
||||||
activitiesFilters: {},
|
activitiesFilters: {},
|
||||||
activitiesOrderByVariables: FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY,
|
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 { TAB_LIST_COMPONENT_ID } from '@/ui/layout/show-page/components/ShowPageRightContainer';
|
||||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||||
|
|
||||||
export const TimelineCreateButtonGroup = () => {
|
export const TimelineCreateButtonGroup = ({
|
||||||
const { activeTabIdState } = useTabList(TAB_LIST_COMPONENT_ID);
|
isInRightDrawer = false,
|
||||||
|
}: {
|
||||||
|
isInRightDrawer?: boolean;
|
||||||
|
}) => {
|
||||||
|
const { activeTabIdState } = useTabList(
|
||||||
|
`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`,
|
||||||
|
);
|
||||||
const setActiveTabId = useSetRecoilState(activeTabIdState);
|
const setActiveTabId = useSetRecoilState(activeTabIdState);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -1,7 +1,9 @@
|
|||||||
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||||
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
|
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||||
|
|
||||||
// do we need to test this?
|
// do we need to test this?
|
||||||
@ -12,6 +14,10 @@ export const useTimelineActivities = (
|
|||||||
nameSingular: targetableObject.targetObjectNameSingular,
|
nameSingular: targetableObject.targetObjectNameSingular,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { objectMetadataItem } = useObjectMetadataItem({
|
||||||
|
objectNameSingular: CoreObjectNameSingular.TimelineActivity,
|
||||||
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
records: TimelineActivities,
|
records: TimelineActivities,
|
||||||
loading,
|
loading,
|
||||||
@ -28,18 +34,7 @@ export const useTimelineActivities = (
|
|||||||
createdAt: 'DescNullsFirst',
|
createdAt: 'DescNullsFirst',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
recordGqlFields: {
|
recordGqlFields: generateDepthOneRecordGqlFields({ objectMetadataItem }),
|
||||||
id: true,
|
|
||||||
createdAt: true,
|
|
||||||
linkedObjectMetadataId: true,
|
|
||||||
linkedRecordCachedName: true,
|
|
||||||
linkedRecordId: true,
|
|
||||||
name: true,
|
|
||||||
properties: true,
|
|
||||||
happensAt: true,
|
|
||||||
workspaceMember: true,
|
|
||||||
person: true,
|
|
||||||
},
|
|
||||||
fetchPolicy: 'cache-and-network',
|
fetchPolicy: 'cache-and-network',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import {
|
|||||||
StyledEventRowItemAction,
|
StyledEventRowItemAction,
|
||||||
StyledEventRowItemColumn,
|
StyledEventRowItemColumn,
|
||||||
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||||
|
|
||||||
type EventRowActivityProps = EventRowDynamicComponentProps;
|
type EventRowActivityProps = EventRowDynamicComponentProps;
|
||||||
@ -19,11 +20,10 @@ const StyledLinkedActivity = styled.span`
|
|||||||
export const EventRowActivity = ({
|
export const EventRowActivity = ({
|
||||||
event,
|
event,
|
||||||
authorFullName,
|
authorFullName,
|
||||||
}: EventRowActivityProps) => {
|
objectNameSingular,
|
||||||
|
}: EventRowActivityProps & { objectNameSingular: CoreObjectNameSingular }) => {
|
||||||
const [, eventAction] = event.name.split('.');
|
const [, eventAction] = event.name.split('.');
|
||||||
|
|
||||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
|
||||||
|
|
||||||
if (!event.linkedRecordId) {
|
if (!event.linkedRecordId) {
|
||||||
throw new Error('Could not find linked record id for event');
|
throw new Error('Could not find linked record id for event');
|
||||||
}
|
}
|
||||||
@ -32,6 +32,10 @@ export const EventRowActivity = ({
|
|||||||
recordStoreFamilyState(event.linkedRecordId),
|
recordStoreFamilyState(event.linkedRecordId),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const openActivityRightDrawer = useOpenActivityRightDrawer({
|
||||||
|
objectNameSingular,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
|
<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 { EventRowMainObject } from '@/activities/timelineActivities/rows/main-object/components/EventRowMainObject';
|
||||||
import { EventRowMessage } from '@/activities/timelineActivities/rows/message/components/EventRowMessage';
|
import { EventRowMessage } from '@/activities/timelineActivities/rows/message/components/EventRowMessage';
|
||||||
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
|
||||||
export interface EventRowDynamicComponentProps {
|
export interface EventRowDynamicComponentProps {
|
||||||
@ -57,8 +58,7 @@ export const EventRowDynamicComponent = ({
|
|||||||
authorFullName={authorFullName}
|
authorFullName={authorFullName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case 'task':
|
case 'linked-task':
|
||||||
case 'note':
|
|
||||||
return (
|
return (
|
||||||
<EventRowActivity
|
<EventRowActivity
|
||||||
labelIdentifierValue={labelIdentifierValue}
|
labelIdentifierValue={labelIdentifierValue}
|
||||||
@ -66,6 +66,18 @@ export const EventRowDynamicComponent = ({
|
|||||||
mainObjectMetadataItem={mainObjectMetadataItem}
|
mainObjectMetadataItem={mainObjectMetadataItem}
|
||||||
linkedObjectMetadataItem={linkedObjectMetadataItem}
|
linkedObjectMetadataItem={linkedObjectMetadataItem}
|
||||||
authorFullName={authorFullName}
|
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:
|
case mainObjectMetadataItem?.nameSingular:
|
||||||
|
|||||||
@ -13,4 +13,4 @@ export type TimelineActivity = {
|
|||||||
linkedRecordId: string;
|
linkedRecordId: string;
|
||||||
linkedObjectMetadataId: string;
|
linkedObjectMetadataId: string;
|
||||||
__typename: 'TimelineActivity';
|
__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 = {
|
export type Activity = {
|
||||||
__typename: 'Activity';
|
|
||||||
id: string;
|
id: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
completedAt: string | null;
|
|
||||||
reminderAt: string | null;
|
|
||||||
dueAt: string | null;
|
|
||||||
activityTargets: ActivityTarget[];
|
|
||||||
type: ActivityType;
|
|
||||||
title: string;
|
title: string;
|
||||||
body: string;
|
body: string | null;
|
||||||
author: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'>;
|
|
||||||
authorId: string;
|
|
||||||
assignee: Pick<WorkspaceMember, 'id' | 'name' | 'avatarUrl'> | null;
|
|
||||||
assigneeId: string | null;
|
|
||||||
comments: Comment[];
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 { Note } from '@/activities/types/Note';
|
||||||
import { ActivityTarget } from '@/activities/types/ActivityTarget';
|
import { Task } from '@/activities/types/Task';
|
||||||
import { Comment } from '@/activities/types/Comment';
|
|
||||||
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
|
||||||
|
|
||||||
export type ActivityForEditor = Pick<
|
export type ActivityForEditor = Partial<Task | Note> &
|
||||||
Activity,
|
Partial<Pick<Task, 'status' | 'dueAt' | 'assignee' | 'taskTargets'>> &
|
||||||
| 'id'
|
Partial<Pick<Note, 'noteTargets'>>;
|
||||||
| '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'
|
|
||||||
>
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
|
|||||||
@ -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 { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||||
|
|
||||||
export type ActivityTargetWithTargetRecord = {
|
export type ActivityTargetWithTargetRecord = {
|
||||||
targetObjectMetadataItem: ObjectMetadataItem;
|
targetObjectMetadataItem: ObjectMetadataItem;
|
||||||
activityTarget: ActivityTarget;
|
activityTarget: NoteTarget | TaskTarget;
|
||||||
targetObject: ObjectRecord;
|
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 { Activity } from '@/activities/types/Activity';
|
||||||
|
import { NoteTarget } from '@/activities/types/NoteTarget';
|
||||||
|
|
||||||
export type Note = Activity & {
|
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