mirror of
https://github.com/twentyhq/twenty.git
synced 2024-12-18 09:02:11 +03:00
Show Entity task/note tabs (#1282)
* - show task tab - tab bar * - add notes tab * - fixed unused style * - add button - fixed company edit note test * - fixed merge & dropdown * - added Tests - refactored directory structure activities - moved Task/Note Pages to corresponding modules - fixed TabList * lint
This commit is contained in:
parent
f8e3dd3f6b
commit
7e264565ef
@ -37,7 +37,6 @@ export function useOpenCreateActivityDrawer() {
|
||||
entities?: ActivityTargetableEntity[],
|
||||
) {
|
||||
const now = new Date().toISOString();
|
||||
|
||||
return createActivityMutation({
|
||||
variables: {
|
||||
data: {
|
||||
|
87
front/src/modules/activities/notes/components/NoteCard.tsx
Normal file
87
front/src/modules/activities/notes/components/NoteCard.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ActivityRelationEditableField } from '@/activities/editable-fields/components/ActivityRelationEditableField';
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { Activity, ActivityTarget } from '~/generated/graphql';
|
||||
|
||||
const StyledCard = styled.div`
|
||||
align-items: flex-start;
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 300px;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const StyledCardDetailsContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
height: calc(100% - 45px);
|
||||
justify-content: start;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(4)});
|
||||
`;
|
||||
|
||||
const StyledNoteTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledCardContent = styled.div`
|
||||
align-self: stretch;
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
line-break: anywhere;
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledFooter = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
border-top: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
justify-content: center;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
width: calc(100% - ${({ theme }) => theme.spacing(4)});
|
||||
`;
|
||||
|
||||
export function NoteCard({
|
||||
note,
|
||||
}: {
|
||||
note: Pick<
|
||||
Activity,
|
||||
'id' | 'title' | 'body' | 'type' | 'completedAt' | 'dueAt'
|
||||
> & {
|
||||
activityTargets?: Array<Pick<ActivityTarget, 'id'>> | null;
|
||||
};
|
||||
}) {
|
||||
const openActivityRightDrawer = useOpenActivityRightDrawer();
|
||||
const body = JSON.parse(note.body ?? '{}')[0]
|
||||
?.content.map((x: any) => x.text)
|
||||
.join('\n');
|
||||
|
||||
return (
|
||||
<StyledCard>
|
||||
<StyledCardDetailsContainer
|
||||
onClick={() => openActivityRightDrawer(note.id)}
|
||||
>
|
||||
<StyledNoteTitle>{note.title ?? 'Task Title'}</StyledNoteTitle>
|
||||
<StyledCardContent>{body}</StyledCardContent>
|
||||
</StyledCardDetailsContainer>
|
||||
<StyledFooter>
|
||||
<ActivityRelationEditableField activity={note} />
|
||||
</StyledFooter>
|
||||
</StyledCard>
|
||||
);
|
||||
}
|
70
front/src/modules/activities/notes/components/NoteList.tsx
Normal file
70
front/src/modules/activities/notes/components/NoteList.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import { ReactElement } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { NoteForList } from '../../types/NoteForList';
|
||||
|
||||
import { NoteCard } from './NoteCard';
|
||||
|
||||
type OwnProps = {
|
||||
title: string;
|
||||
notes: NoteForList[];
|
||||
button?: ReactElement | false;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 8px 24px;
|
||||
`;
|
||||
|
||||
const StyledTitleBar = styled.h3`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
place-items: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.h3`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
`;
|
||||
|
||||
const StyledCount = styled.span`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
margin-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledNoteContainer = styled.div`
|
||||
display: grid;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
grid-auto-rows: 1fr;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export function NoteList({ title, notes, button }: OwnProps) {
|
||||
return (
|
||||
<>
|
||||
{notes && notes.length > 0 && (
|
||||
<StyledContainer>
|
||||
<StyledTitleBar>
|
||||
<StyledTitle>
|
||||
{title} <StyledCount>{notes.length}</StyledCount>
|
||||
</StyledTitle>
|
||||
{button}
|
||||
</StyledTitleBar>
|
||||
<StyledNoteContainer>
|
||||
{notes.map((note) => (
|
||||
<NoteCard key={note.id} note={note} />
|
||||
))}
|
||||
</StyledNoteContainer>
|
||||
</StyledContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
91
front/src/modules/activities/notes/components/Notes.tsx
Normal file
91
front/src/modules/activities/notes/components/Notes.tsx
Normal file
@ -0,0 +1,91 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconNotes } from '@tabler/icons-react';
|
||||
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { NoteList } from '@/activities/notes/components/NoteList';
|
||||
import { useNotes } from '@/activities/notes/hooks/useNotes';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import {
|
||||
Button,
|
||||
ButtonSize,
|
||||
ButtonVariant,
|
||||
} from '@/ui/button/components/Button';
|
||||
import { ActivityType } from '~/generated/graphql';
|
||||
|
||||
const StyledTaskGroupEmptyContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
justify-content: center;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(16)};
|
||||
padding-left: ${({ theme }) => theme.spacing(4)};
|
||||
padding-right: ${({ theme }) => theme.spacing(4)};
|
||||
padding-top: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledEmptyTaskGroupTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.xxl};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.md};
|
||||
`;
|
||||
|
||||
const StyledEmptyTaskGroupSubTitle = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.extraLight};
|
||||
font-size: ${({ theme }) => theme.font.size.xxl};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
line-height: ${({ theme }) => theme.text.lineHeight.md};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledNotesContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export function Notes({ entity }: { entity: ActivityTargetableEntity }) {
|
||||
const { notes } = useNotes(entity);
|
||||
const theme = useTheme();
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
|
||||
if (notes?.length === 0) {
|
||||
return (
|
||||
<StyledTaskGroupEmptyContainer>
|
||||
<StyledEmptyTaskGroupTitle>No note yet</StyledEmptyTaskGroupTitle>
|
||||
<StyledEmptyTaskGroupSubTitle>Create one:</StyledEmptyTaskGroupSubTitle>
|
||||
<Button
|
||||
icon={<IconNotes size={theme.icon.size.sm} />}
|
||||
title="New note"
|
||||
variant={ButtonVariant.Secondary}
|
||||
onClick={() => openCreateActivity(ActivityType.Note, [entity])}
|
||||
/>
|
||||
</StyledTaskGroupEmptyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledNotesContainer>
|
||||
<NoteList
|
||||
title="All"
|
||||
notes={notes ?? []}
|
||||
button={
|
||||
<Button
|
||||
icon={<IconNotes size={theme.icon.size.md} />}
|
||||
size={ButtonSize.Small}
|
||||
variant={ButtonVariant.Secondary}
|
||||
title="Add note"
|
||||
onClick={() => openCreateActivity(ActivityType.Note, [entity])}
|
||||
></Button>
|
||||
}
|
||||
/>
|
||||
</StyledNotesContainer>
|
||||
);
|
||||
}
|
27
front/src/modules/activities/notes/hooks/useNotes.ts
Normal file
27
front/src/modules/activities/notes/hooks/useNotes.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { ActivityType, useGetActivitiesQuery } from '~/generated/graphql';
|
||||
|
||||
import { ActivityTargetableEntity } from '../../types/ActivityTargetableEntity';
|
||||
|
||||
export function useNotes(entity: ActivityTargetableEntity) {
|
||||
const { data: notesData } = useGetActivitiesQuery({
|
||||
variables: {
|
||||
where: {
|
||||
type: { equals: ActivityType.Note },
|
||||
activityTargets: {
|
||||
some: {
|
||||
OR: [
|
||||
{ companyId: { equals: entity.id } },
|
||||
{ personId: { equals: entity.id } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const notes = notesData?.findManyActivities;
|
||||
|
||||
return {
|
||||
notes,
|
||||
};
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
||||
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
||||
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
|
||||
import { TaskGroups } from '../TaskGroups';
|
||||
|
||||
const meta: Meta<typeof TaskGroups> = {
|
||||
title: 'Modules/Activity/TaskGroups',
|
||||
component: TaskGroups,
|
@ -4,12 +4,11 @@ import { graphql } from 'msw';
|
||||
|
||||
import { GET_ACTIVITIES } from '@/activities/graphql/queries/getActivities';
|
||||
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
||||
import { ComponentWithRecoilScopeDecorator } from '~/testing/decorators/ComponentWithRecoilScopeDecorator';
|
||||
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
|
||||
import { TaskGroups } from '../TaskGroups';
|
||||
|
||||
const meta: Meta<typeof TaskGroups> = {
|
||||
title: 'Modules/Activity/TaskGroupsWithoutTasks',
|
||||
component: TaskGroups,
|
@ -1,12 +1,11 @@
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import { TaskList } from '@/activities/tasks/components/TaskList';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { mockedActivities } from '~/testing/mock-data/activities';
|
||||
|
||||
import { TaskList } from '../TaskList';
|
||||
|
||||
const meta: Meta<typeof TaskList> = {
|
||||
title: 'Modules/Activity/TaskList',
|
||||
component: TaskList,
|
@ -0,0 +1,34 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import {
|
||||
Button,
|
||||
ButtonSize,
|
||||
ButtonVariant,
|
||||
} from '@/ui/button/components/Button';
|
||||
import { IconPlus } from '@/ui/icon';
|
||||
import { ActivityType } from '~/generated/graphql';
|
||||
|
||||
export function AddTaskButton({
|
||||
entity,
|
||||
}: {
|
||||
entity?: ActivityTargetableEntity;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
|
||||
if (!entity) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
icon={<IconPlus size={theme.icon.size.md} />}
|
||||
size={ButtonSize.Small}
|
||||
variant={ButtonVariant.Secondary}
|
||||
title="Add task"
|
||||
onClick={() => openCreateActivity(ActivityType.Task, [entity])}
|
||||
></Button>
|
||||
);
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
export function EntityTasks({ entity }: { entity: ActivityTargetableEntity }) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<RecoilScope SpecificContext={TasksRecoilScopeContext}>
|
||||
<TaskGroups entity={entity} showAddButton />
|
||||
</RecoilScope>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
@ -2,14 +2,20 @@ import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconCheckbox } from '@tabler/icons-react';
|
||||
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { useTasks } from '@/activities/tasks/hooks/useTasks';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { Button, ButtonVariant } from '@/ui/button/components/Button';
|
||||
import { ActivityType } from '~/generated/graphql';
|
||||
|
||||
import { useOpenCreateActivityDrawer } from '../hooks/useOpenCreateActivityDrawer';
|
||||
import { useTasks } from '../hooks/useTasks';
|
||||
|
||||
import { AddTaskButton } from './AddTaskButton';
|
||||
import { TaskList } from './TaskList';
|
||||
|
||||
type OwnProps = {
|
||||
entity?: ActivityTargetableEntity;
|
||||
showAddButton?: boolean;
|
||||
};
|
||||
|
||||
const StyledTaskGroupEmptyContainer = styled.div`
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
@ -44,8 +50,9 @@ const StyledContainer = styled.div`
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
export function TaskGroups() {
|
||||
const { todayOrPreviousTasks, upcomingTasks, unscheduledTasks } = useTasks();
|
||||
export function TaskGroups({ entity, showAddButton }: OwnProps) {
|
||||
const { todayOrPreviousTasks, upcomingTasks, unscheduledTasks } =
|
||||
useTasks(entity);
|
||||
const theme = useTheme();
|
||||
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
@ -63,7 +70,9 @@ export function TaskGroups() {
|
||||
icon={<IconCheckbox size={theme.icon.size.sm} />}
|
||||
title="New task"
|
||||
variant={ButtonVariant.Secondary}
|
||||
onClick={() => openCreateActivity(ActivityType.Task)}
|
||||
onClick={() =>
|
||||
openCreateActivity(ActivityType.Task, entity ? [entity] : undefined)
|
||||
}
|
||||
/>
|
||||
</StyledTaskGroupEmptyContainer>
|
||||
);
|
||||
@ -71,9 +80,28 @@ export function TaskGroups() {
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<TaskList title="Today" tasks={todayOrPreviousTasks ?? []} />
|
||||
<TaskList title="Upcoming" tasks={upcomingTasks ?? []} />
|
||||
<TaskList title="Unscheduled" tasks={unscheduledTasks ?? []} />
|
||||
<TaskList
|
||||
title="Today"
|
||||
tasks={todayOrPreviousTasks ?? []}
|
||||
button={showAddButton && <AddTaskButton entity={entity} />}
|
||||
/>
|
||||
<TaskList
|
||||
title="Upcoming"
|
||||
tasks={upcomingTasks ?? []}
|
||||
button={
|
||||
showAddButton &&
|
||||
!todayOrPreviousTasks?.length && <AddTaskButton entity={entity} />
|
||||
}
|
||||
/>
|
||||
<TaskList
|
||||
title="Unscheduled"
|
||||
tasks={unscheduledTasks ?? []}
|
||||
button={
|
||||
showAddButton &&
|
||||
!todayOrPreviousTasks?.length &&
|
||||
!upcomingTasks?.length && <AddTaskButton entity={entity} />
|
||||
}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
@ -1,12 +1,14 @@
|
||||
import { ReactElement } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { TaskForList } from '../types/TaskForList';
|
||||
import { TaskForList } from '@/activities/types/TaskForList';
|
||||
|
||||
import { TaskRow } from './TaskRow';
|
||||
|
||||
type OwnProps = {
|
||||
title: string;
|
||||
tasks: TaskForList[];
|
||||
button?: ReactElement | false;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -18,11 +20,18 @@ const StyledContainer = styled.div`
|
||||
padding: 8px 24px;
|
||||
`;
|
||||
|
||||
const StyledTitleBar = styled.h3`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
place-items: center;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.h3`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
margin-top: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledCount = styled.span`
|
||||
@ -37,14 +46,17 @@ const StyledTaskRows = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export function TaskList({ title, tasks }: OwnProps) {
|
||||
export function TaskList({ title, tasks, button }: OwnProps) {
|
||||
return (
|
||||
<>
|
||||
{tasks && tasks.length > 0 && (
|
||||
<StyledContainer>
|
||||
<StyledTitleBar>
|
||||
<StyledTitle>
|
||||
{title} <StyledCount>{tasks.length}</StyledCount>
|
||||
</StyledTitle>
|
||||
{button}
|
||||
</StyledTitleBar>
|
||||
<StyledTaskRows>
|
||||
{tasks.map((task) => (
|
||||
<TaskRow key={task.id} task={task} />
|
@ -11,8 +11,8 @@ import {
|
||||
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
|
||||
import { beautifyExactDate, hasDatePassed } from '~/utils/date-utils';
|
||||
|
||||
import { TaskForList } from '../../types/TaskForList';
|
||||
import { useCompleteTask } from '../hooks/useCompleteTask';
|
||||
import { TaskForList } from '../types/TaskForList';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
@ -4,8 +4,8 @@ import { getOperationName } from '@apollo/client/utilities';
|
||||
|
||||
import { Activity, useUpdateActivityMutation } from '~/generated/graphql';
|
||||
|
||||
import { ACTIVITY_UPDATE_FRAGMENT } from '../graphql/fragments/activityUpdateFragment';
|
||||
import { GET_ACTIVITIES } from '../graphql/queries/getActivities';
|
||||
import { ACTIVITY_UPDATE_FRAGMENT } from '../../graphql/fragments/activityUpdateFragment';
|
||||
import { GET_ACTIVITIES } from '../../graphql/queries/getActivities';
|
||||
|
||||
type Task = Pick<Activity, 'id' | 'completedAt'>;
|
||||
|
@ -4,7 +4,7 @@ import { availableFiltersScopedState } from '@/ui/filter-n-sort/states/available
|
||||
import { FilterDefinition } from '@/ui/filter-n-sort/types/FilterDefinition';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
|
||||
import { TasksRecoilScopeContext } from '../states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||
import { TasksRecoilScopeContext } from '../../states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||
|
||||
export function useInitializeTasksFilters({
|
||||
availableFilters,
|
@ -2,6 +2,9 @@ import { useEffect } from 'react';
|
||||
import { DateTime } from 'luxon';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||
import { useInitializeTasksFilters } from '@/activities/tasks/hooks/useInitializeTasksFilters';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { filtersScopedState } from '@/ui/filter-n-sort/states/filtersScopedState';
|
||||
import { FilterOperand } from '@/ui/filter-n-sort/types/FilterOperand';
|
||||
@ -12,11 +15,7 @@ import { ActivityType, useGetActivitiesQuery } from '~/generated/graphql';
|
||||
import { tasksFilters } from '~/pages/tasks/tasks-filters';
|
||||
import { parseDate } from '~/utils/date-utils';
|
||||
|
||||
import { TasksRecoilScopeContext } from '../states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||
|
||||
import { useInitializeTasksFilters } from './useInitializeTasksFilters';
|
||||
|
||||
export function useTasks() {
|
||||
export function useTasks(entity?: ActivityTargetableEntity) {
|
||||
useInitializeTasksFilters({
|
||||
availableFilters: tasksFilters,
|
||||
});
|
||||
@ -35,7 +34,7 @@ export function useTasks() {
|
||||
const [currentUser] = useRecoilState(currentUserState);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentUser && !filters.length) {
|
||||
if (currentUser && !filters.length && !entity) {
|
||||
setFilters([
|
||||
{
|
||||
key: 'assigneeId',
|
||||
@ -47,9 +46,20 @@ export function useTasks() {
|
||||
},
|
||||
]);
|
||||
}
|
||||
}, [currentUser, filters, setFilters]);
|
||||
}, [currentUser, filters, setFilters, entity]);
|
||||
|
||||
const whereFilters = Object.assign(
|
||||
const whereFilters = entity
|
||||
? {
|
||||
activityTargets: {
|
||||
some: {
|
||||
OR: [
|
||||
{ companyId: { equals: entity.id } },
|
||||
{ personId: { equals: entity.id } },
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
: Object.assign(
|
||||
{},
|
||||
...filters.map((filter) => {
|
||||
return turnFilterIntoWhereClause(filter);
|
||||
@ -77,7 +87,7 @@ export function useTasks() {
|
||||
});
|
||||
|
||||
const tasksData =
|
||||
activeTabId === 'done' ? completeTasksData : incompleteTaskData;
|
||||
activeTabId === 'done' || !entity ? completeTasksData : incompleteTaskData;
|
||||
|
||||
const todayOrPreviousTasks = tasksData?.findManyActivities.filter((task) => {
|
||||
if (!task.dueAt) {
|
@ -51,24 +51,6 @@ const StyledEmptyTimelineSubTitle = styled.div`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledTopActionBar = styled.div`
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
backdrop-filter: ${() => (useIsMobile() ? 'none' : `blur(5px)`)};
|
||||
|
||||
border-bottom: ${({ theme }) =>
|
||||
useIsMobile() ? 'none' : `1px solid ${theme.border.color.medium}`};
|
||||
|
||||
border-top-right-radius: ${() => (useIsMobile() ? 'none' : `8px`)};
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
left: 0px;
|
||||
padding: 12px 16px 12px 16px;
|
||||
position: ${() => (useIsMobile() ? 'relative' : 'sticky')};
|
||||
top: 0px;
|
||||
`;
|
||||
|
||||
export function Timeline({ entity }: { entity: ActivityTargetableEntity }) {
|
||||
const { data: queryResult, loading } = useGetActivitiesByTargetsQuery({
|
||||
variables: {
|
||||
@ -104,12 +86,6 @@ export function Timeline({ entity }: { entity: ActivityTargetableEntity }) {
|
||||
|
||||
return (
|
||||
<StyledMainContainer>
|
||||
<StyledTopActionBar>
|
||||
<ActivityCreateButton
|
||||
onNoteClick={() => openCreateActivity(ActivityType.Note, [entity])}
|
||||
onTaskClick={() => openCreateActivity(ActivityType.Task, [entity])}
|
||||
/>
|
||||
</StyledTopActionBar>
|
||||
<TimelineItemsContainer activities={activities} />
|
||||
</StyledMainContainer>
|
||||
);
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { Tooltip } from 'react-tooltip';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useCompleteTask } from '@/activities/hooks/useCompleteTask';
|
||||
import { useOpenActivityRightDrawer } from '@/activities/hooks/useOpenActivityRightDrawer';
|
||||
import { useCompleteTask } from '@/activities/tasks/hooks/useCompleteTask';
|
||||
import { IconNotes } from '@/ui/icon';
|
||||
import { OverflowingTextWithTooltip } from '@/ui/tooltip/OverflowingTextWithTooltip';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
3
front/src/modules/activities/types/NoteForList.ts
Normal file
3
front/src/modules/activities/types/NoteForList.ts
Normal file
@ -0,0 +1,3 @@
|
||||
import { GetActivitiesQuery } from '~/generated/graphql';
|
||||
|
||||
export type NoteForList = GetActivitiesQuery['findManyActivities'][0];
|
@ -80,7 +80,7 @@ export function DropdownButton({
|
||||
dropdownHotkeyScope,
|
||||
dropdownButtonCustomHotkeyScope,
|
||||
]);
|
||||
|
||||
console.log(dropdownComponents, isDropdownButtonOpen);
|
||||
return (
|
||||
<StyledContainer ref={containerRef}>
|
||||
{hotkey && (
|
||||
|
@ -14,6 +14,7 @@ type OwnProps = {
|
||||
icon: ReactNode;
|
||||
onAddButtonClick?: () => void;
|
||||
onFavoriteButtonClick?: () => void;
|
||||
extraButtons?: ReactNode[];
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -30,6 +31,7 @@ export function WithTopBarContainer({
|
||||
icon,
|
||||
onAddButtonClick,
|
||||
onFavoriteButtonClick,
|
||||
extraButtons,
|
||||
}: OwnProps) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
@ -41,6 +43,7 @@ export function WithTopBarContainer({
|
||||
icon={icon}
|
||||
onAddButtonClick={onAddButtonClick}
|
||||
onFavoriteButtonClick={onFavoriteButtonClick}
|
||||
extraButtons={extraButtons}
|
||||
/>
|
||||
<RightDrawerContainer topMargin={PAGE_BAR_MIN_HEIGHT + 16 + 16}>
|
||||
{children}
|
||||
|
@ -4,8 +4,10 @@ import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { IconButton } from '@/ui/button/components/IconButton';
|
||||
import { DropdownRecoilScopeContext } from '@/ui/dropdown/states/recoil-scope-contexts/DropdownRecoilScopeContext';
|
||||
import { IconChevronLeft, IconHeart, IconPlus } from '@/ui/icon/index';
|
||||
import NavCollapseButton from '@/ui/navbar/components/NavCollapseButton';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
import { navbarIconSize } from '../../../navbar/constants';
|
||||
@ -70,6 +72,7 @@ type OwnProps = {
|
||||
icon: ReactNode;
|
||||
onAddButtonClick?: () => void;
|
||||
onFavoriteButtonClick?: () => void;
|
||||
extraButtons?: ReactNode[];
|
||||
};
|
||||
|
||||
export function PageBar({
|
||||
@ -79,6 +82,7 @@ export function PageBar({
|
||||
icon,
|
||||
onAddButtonClick,
|
||||
onFavoriteButtonClick,
|
||||
extraButtons,
|
||||
}: OwnProps) {
|
||||
const navigate = useNavigate();
|
||||
const navigateBack = useCallback(() => navigate(-1), [navigate]);
|
||||
@ -113,6 +117,7 @@ export function PageBar({
|
||||
</StyledTitleContainer>
|
||||
</StyledTopBarIconTitleContainer>
|
||||
</StyledLeftContainer>
|
||||
<RecoilScope SpecificContext={DropdownRecoilScopeContext}>
|
||||
<StyledActionButtonsContainer>
|
||||
{onFavoriteButtonClick && (
|
||||
<IconButton
|
||||
@ -134,7 +139,9 @@ export function PageBar({
|
||||
variant="border"
|
||||
/>
|
||||
)}
|
||||
{extraButtons}
|
||||
</StyledActionButtonsContainer>
|
||||
</RecoilScope>
|
||||
</StyledTopBarContainer>
|
||||
</>
|
||||
);
|
||||
|
@ -0,0 +1,77 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { IconButton } from '@/ui/button/components/IconButton';
|
||||
import { DropdownButton } from '@/ui/dropdown/components/DropdownButton';
|
||||
import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem';
|
||||
import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu';
|
||||
import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer';
|
||||
import { useDropdownButton } from '@/ui/dropdown/hooks/useDropdownButton';
|
||||
import { IconCheckbox, IconNotes, IconPlus } from '@/ui/icon/index';
|
||||
import { RelationPickerHotkeyScope } from '@/ui/input/relation-picker/types/RelationPickerHotkeyScope';
|
||||
import { ActivityType } from '~/generated/graphql';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
export function ShowPageAddButton({
|
||||
entity,
|
||||
}: {
|
||||
entity: ActivityTargetableEntity;
|
||||
}) {
|
||||
const { closeDropdownButton, toggleDropdownButton } = useDropdownButton({
|
||||
key: 'add-show-page',
|
||||
});
|
||||
const openCreateActivity = useOpenCreateActivityDrawer();
|
||||
|
||||
function handleSelect(type: ActivityType) {
|
||||
console.log(type, entity);
|
||||
openCreateActivity(type, [entity]);
|
||||
closeDropdownButton();
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
<DropdownButton
|
||||
dropdownKey="add-show-page"
|
||||
buttonComponents={
|
||||
<IconButton
|
||||
icon={<IconPlus size={16} />}
|
||||
size="large"
|
||||
data-testid="add-showpage-button"
|
||||
textColor={'secondary'}
|
||||
variant="border"
|
||||
onClick={toggleDropdownButton}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<StyledDropdownMenu>
|
||||
<StyledDropdownMenuItemsContainer
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleSelect(ActivityType.Note)}
|
||||
accent="regular"
|
||||
>
|
||||
<IconNotes size={16} />
|
||||
Note
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleSelect(ActivityType.Note)}
|
||||
accent="regular"
|
||||
>
|
||||
<IconCheckbox size={16} />
|
||||
Task
|
||||
</DropdownMenuItem>
|
||||
</StyledDropdownMenuItemsContainer>
|
||||
</StyledDropdownMenu>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: RelationPickerHotkeyScope.RelationPicker,
|
||||
}}
|
||||
/>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
@ -1,9 +1,24 @@
|
||||
import { ReactElement } from 'react';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { Notes } from '@/activities/notes/components/Notes';
|
||||
import { EntityTasks } from '@/activities/tasks/components/EntityTasks';
|
||||
import { Timeline } from '@/activities/timeline/components/Timeline';
|
||||
import { ActivityTargetableEntity } from '@/activities/types/ActivityTargetableEntity';
|
||||
import {
|
||||
IconCheckbox,
|
||||
IconMail,
|
||||
IconNotes,
|
||||
IconTimelineEvent,
|
||||
} from '@/ui/icon';
|
||||
import { TabList } from '@/ui/tab/components/TabList';
|
||||
import { activeTabIdScopedState } from '@/ui/tab/states/activeTabIdScopedState';
|
||||
import { useRecoilScopedState } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedState';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
|
||||
export const StyledShowPageRightContainer = styled.div`
|
||||
import { ShowPageRecoilScopeContext } from '../../states/ShowPageRecoilScopeContext';
|
||||
|
||||
const StyledShowPageRightContainer = styled.div`
|
||||
display: flex;
|
||||
flex: 1 0 0;
|
||||
flex-direction: column;
|
||||
@ -12,14 +27,75 @@ export const StyledShowPageRightContainer = styled.div`
|
||||
width: calc(100% + 4px);
|
||||
`;
|
||||
|
||||
export type ShowPageRightContainerProps = {
|
||||
children: ReactElement;
|
||||
const StyledTabListContainer = styled.div`
|
||||
align-items: end;
|
||||
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: 40px;
|
||||
padding-left: ${({ theme }) => theme.spacing(1)};
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
entity: ActivityTargetableEntity;
|
||||
timeline?: boolean;
|
||||
tasks?: boolean;
|
||||
notes?: boolean;
|
||||
emails?: boolean;
|
||||
};
|
||||
|
||||
export function ShowPageRightContainer({
|
||||
children,
|
||||
}: ShowPageRightContainerProps) {
|
||||
entity,
|
||||
timeline,
|
||||
tasks,
|
||||
notes,
|
||||
emails,
|
||||
}: OwnProps) {
|
||||
const theme = useTheme();
|
||||
|
||||
const TASK_TABS = [
|
||||
{
|
||||
id: 'timeline',
|
||||
title: 'Timeline',
|
||||
icon: <IconTimelineEvent size={theme.icon.size.md} />,
|
||||
hide: !timeline,
|
||||
},
|
||||
{
|
||||
id: 'tasks',
|
||||
title: 'Tasks',
|
||||
icon: <IconCheckbox size={theme.icon.size.md} />,
|
||||
hide: !tasks,
|
||||
},
|
||||
{
|
||||
id: 'notes',
|
||||
title: 'Notes',
|
||||
icon: <IconNotes size={theme.icon.size.md} />,
|
||||
hide: !notes,
|
||||
},
|
||||
{
|
||||
id: 'emails',
|
||||
title: 'Emails',
|
||||
icon: <IconMail size={theme.icon.size.md} />,
|
||||
hide: !emails,
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
const [activeTabId] = useRecoilScopedState(
|
||||
activeTabIdScopedState,
|
||||
ShowPageRecoilScopeContext,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledShowPageRightContainer>{children} </StyledShowPageRightContainer>
|
||||
<StyledShowPageRightContainer>
|
||||
<StyledTabListContainer>
|
||||
<TabList context={ShowPageRecoilScopeContext} tabs={TASK_TABS} />
|
||||
</StyledTabListContainer>
|
||||
{activeTabId === 'timeline' && <Timeline entity={entity} />}
|
||||
{activeTabId === 'tasks' && <EntityTasks entity={entity} />}
|
||||
{activeTabId === 'notes' && <Notes entity={entity} />}
|
||||
</StyledShowPageRightContainer>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const ShowPageRecoilScopeContext = createContext<string | null>(null);
|
@ -2,26 +2,33 @@ import * as React from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
type OwnProps = {
|
||||
id: string;
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
active?: boolean;
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const StyledTab = styled.div<{ active?: boolean }>`
|
||||
const StyledTab = styled.div<{ active?: boolean; disabled?: boolean }>`
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-color: ${({ theme, active }) =>
|
||||
active ? theme.border.color.inverted : 'transparent'};
|
||||
color: ${({ theme, active }) =>
|
||||
active ? theme.font.color.primary : theme.font.color.secondary};
|
||||
color: ${({ theme, active, disabled }) =>
|
||||
active
|
||||
? theme.font.color.primary
|
||||
: disabled
|
||||
? theme.font.color.light
|
||||
: theme.font.color.secondary};
|
||||
cursor: pointer;
|
||||
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
justify-content: center;
|
||||
padding: ${({ theme }) => theme.spacing(2) + ' ' + theme.spacing(2)};
|
||||
pointer-events: ${({ disabled }) => (disabled ? 'none' : '')};
|
||||
`;
|
||||
|
||||
const StyledHover = styled.span`
|
||||
@ -38,14 +45,22 @@ const StyledHover = styled.span`
|
||||
`;
|
||||
|
||||
export function Tab({
|
||||
id,
|
||||
title,
|
||||
icon,
|
||||
active = false,
|
||||
onClick,
|
||||
className,
|
||||
disabled,
|
||||
}: OwnProps) {
|
||||
return (
|
||||
<StyledTab onClick={onClick} active={active} className={className}>
|
||||
<StyledTab
|
||||
onClick={onClick}
|
||||
active={active}
|
||||
className={className}
|
||||
disabled={disabled}
|
||||
data-testid={'tab-' + id}
|
||||
>
|
||||
<StyledHover>
|
||||
{icon}
|
||||
{title}
|
||||
|
@ -10,6 +10,8 @@ type SingleTabProps = {
|
||||
title: string;
|
||||
icon?: React.ReactNode;
|
||||
id: string;
|
||||
hide?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
type OwnProps = {
|
||||
@ -31,8 +33,11 @@ export function TabList({ tabs, context }: OwnProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
{tabs.map((tab) => (
|
||||
{tabs
|
||||
.filter((tab) => !tab.hide)
|
||||
.map((tab) => (
|
||||
<Tab
|
||||
id={tab.id}
|
||||
key={tab.id}
|
||||
title={tab.title}
|
||||
icon={tab.icon}
|
||||
@ -40,6 +45,7 @@ export function TabList({ tabs, context }: OwnProps) {
|
||||
onClick={() => {
|
||||
setActiveTabId(tab.id);
|
||||
}}
|
||||
disabled={tab.disabled}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
|
@ -26,6 +26,7 @@ export const Catalog: Story = {
|
||||
args: { title: 'Tab title', icon: <IconCheckbox /> },
|
||||
argTypes: {
|
||||
active: { control: false },
|
||||
disabled: { control: false },
|
||||
onClick: { control: false },
|
||||
},
|
||||
parameters: {
|
||||
@ -43,6 +44,11 @@ export const Catalog: Story = {
|
||||
values: ['true', 'false'],
|
||||
props: (active: string) => ({ active: active === 'true' }),
|
||||
},
|
||||
{
|
||||
name: 'Disabled',
|
||||
values: ['true', 'false'],
|
||||
props: (disabled: string) => ({ disabled: disabled === 'true' }),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -0,0 +1,69 @@
|
||||
import { expect } from '@storybook/jest';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { within } from '@storybook/testing-library';
|
||||
import { IconCheckbox } from '@tabler/icons-react';
|
||||
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
|
||||
|
||||
import { TabList } from '../TabList';
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Tab1',
|
||||
icon: <IconCheckbox size={16} />,
|
||||
hide: true,
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Tab2',
|
||||
icon: <IconCheckbox size={16} />,
|
||||
hide: false,
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Tab3',
|
||||
icon: <IconCheckbox size={16} />,
|
||||
hide: false,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Tab4',
|
||||
icon: <IconCheckbox size={16} />,
|
||||
hide: false,
|
||||
disabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
const meta: Meta<typeof TabList> = {
|
||||
title: 'UI/Tab/TabList',
|
||||
component: TabList,
|
||||
args: {
|
||||
tabs: tabs,
|
||||
},
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<RecoilScope>
|
||||
<Story />
|
||||
</RecoilScope>
|
||||
),
|
||||
ComponentDecorator,
|
||||
],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof TabList>;
|
||||
|
||||
export const TabListDisplay: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const submitButton = canvas.queryByText('Tab1');
|
||||
expect(submitButton).toBeNull();
|
||||
expect(await canvas.findByText('Tab2')).toBeInTheDocument();
|
||||
expect(await canvas.findByText('Tab3')).toBeInTheDocument();
|
||||
expect(await canvas.findByText('Tab4')).toBeInTheDocument();
|
||||
},
|
||||
};
|
@ -1,7 +1,6 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import { Timeline } from '@/activities/timeline/components/Timeline';
|
||||
import { ActivityTargetableEntityType } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { CompanyTeam } from '@/companies/components/CompanyTeam';
|
||||
import { useCompanyQuery } from '@/companies/hooks/useCompanyQuery';
|
||||
@ -13,9 +12,12 @@ import { EditableFieldMutationContext } from '@/ui/editable-field/contexts/Edita
|
||||
import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox';
|
||||
import { IconBuildingSkyscraper } from '@/ui/icon';
|
||||
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
|
||||
import { ShowPageAddButton } from '@/ui/layout/show-page/components/ShowPageAddButton';
|
||||
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
|
||||
import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer';
|
||||
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
|
||||
import { ShowPageRecoilScopeContext } from '@/ui/layout/states/ShowPageRecoilScopeContext';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
import { useUpdateOneCompanyMutation } from '~/generated/graphql';
|
||||
import { getLogoUrlFromDomainName } from '~/utils';
|
||||
|
||||
@ -49,7 +51,17 @@ export function CompanyShow() {
|
||||
isFavorite={isFavorite}
|
||||
icon={<IconBuildingSkyscraper size={theme.icon.size.md} />}
|
||||
onFavoriteButtonClick={handleFavoriteButtonClick}
|
||||
extraButtons={[
|
||||
<ShowPageAddButton
|
||||
key="add"
|
||||
entity={{
|
||||
id: company.id,
|
||||
type: ActivityTargetableEntityType.Company,
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<RecoilScope SpecificContext={ShowPageRecoilScopeContext}>
|
||||
<ShowPageContainer>
|
||||
<ShowPageLeftContainer>
|
||||
<ShowPageSummaryCard
|
||||
@ -81,15 +93,18 @@ export function CompanyShow() {
|
||||
</PropertyBox>
|
||||
<CompanyTeam company={company}></CompanyTeam>
|
||||
</ShowPageLeftContainer>
|
||||
<ShowPageRightContainer>
|
||||
<Timeline
|
||||
<ShowPageRightContainer
|
||||
entity={{
|
||||
id: company.id,
|
||||
type: ActivityTargetableEntityType.Company,
|
||||
}}
|
||||
timeline
|
||||
tasks
|
||||
notes
|
||||
emails
|
||||
/>
|
||||
</ShowPageRightContainer>
|
||||
</ShowPageContainer>
|
||||
</RecoilScope>
|
||||
</WithTopBarContainer>
|
||||
);
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ import {
|
||||
type PageDecoratorArgs,
|
||||
} from '~/testing/decorators/PageDecorator';
|
||||
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||
import { mockedActivities } from '~/testing/mock-data/activities';
|
||||
import { mockedActivities, mockedTasks } from '~/testing/mock-data/activities';
|
||||
import { mockedCompaniesData } from '~/testing/mock-data/companies';
|
||||
|
||||
import { CompanyShow } from '../CompanyShow';
|
||||
@ -37,7 +37,10 @@ const meta: Meta<PageDecoratorArgs> = {
|
||||
(req, res, ctx) => {
|
||||
return res(
|
||||
ctx.data({
|
||||
findManyActivities: mockedActivities,
|
||||
findManyActivities:
|
||||
req?.variables?.where?.type?.equals === 'Task'
|
||||
? mockedTasks
|
||||
: mockedActivities,
|
||||
}),
|
||||
);
|
||||
},
|
||||
@ -59,7 +62,7 @@ export type Story = StoryObj<typeof CompanyShow>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const EditNote: Story = {
|
||||
export const EditNoteByAddButton: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const firstNoteTitle = await canvas.findByText('My very first note');
|
||||
@ -74,6 +77,9 @@ export const EditNote: Story = {
|
||||
|
||||
expect(await canvas.queryByDisplayValue('My very first note')).toBeNull();
|
||||
|
||||
const addDropdown = await canvas.findByTestId('add-showpage-button');
|
||||
await addDropdown.click();
|
||||
|
||||
const noteButton = await canvas.findByText('Note');
|
||||
await noteButton.click();
|
||||
|
||||
@ -114,3 +120,113 @@ export const EditNote: Story = {
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const NoteTab: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const noteTab = await canvas.findByTestId('tab-notes');
|
||||
await noteTab.click();
|
||||
|
||||
expect(await canvas.findByText('My very first note')).toBeInTheDocument();
|
||||
|
||||
const workspaceName = await canvas.findByText('Twenty');
|
||||
await fireEvent.click(workspaceName);
|
||||
|
||||
expect(await canvas.queryByDisplayValue('My very first note')).toBeNull();
|
||||
|
||||
const addButton = await canvas.findByText('Add note');
|
||||
await addButton.click();
|
||||
|
||||
const noteButton = await canvas.findByText('Note');
|
||||
await noteButton.click();
|
||||
|
||||
expect(await canvas.findByText('My very first note')).toBeInTheDocument();
|
||||
},
|
||||
parameters: {
|
||||
msw: [
|
||||
...meta.parameters?.msw,
|
||||
graphql.mutation(
|
||||
getOperationName(CREATE_ACTIVITY_WITH_COMMENT) ?? '',
|
||||
(req, res, ctx) => {
|
||||
return res(
|
||||
ctx.data({
|
||||
createOneActivity: mockedActivities[0],
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
graphql.query(getOperationName(GET_ACTIVITY) ?? '', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.data({
|
||||
findManyActivities: [mockedActivities[0]],
|
||||
}),
|
||||
);
|
||||
}),
|
||||
graphql.mutation(
|
||||
getOperationName(UPDATE_ONE_COMPANY) ?? '',
|
||||
(req, res, ctx) => {
|
||||
return res(
|
||||
ctx.data({
|
||||
updateOneCompany: [mockedCompaniesData[0]],
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const TaskTab: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const taskTab = await canvas.findByTestId('tab-tasks');
|
||||
await taskTab.click();
|
||||
|
||||
expect(await canvas.findByText('My very first task')).toBeInTheDocument();
|
||||
|
||||
const workspaceName = await canvas.findByText('Twenty');
|
||||
await fireEvent.click(workspaceName);
|
||||
|
||||
expect(await canvas.queryByDisplayValue('My very first task')).toBeNull();
|
||||
|
||||
const addButton = await canvas.findByText('Add task');
|
||||
await addButton.click();
|
||||
|
||||
const taskButton = await canvas.findByText('Task');
|
||||
await taskButton.click();
|
||||
|
||||
expect(await canvas.findByText('My very first task')).toBeInTheDocument();
|
||||
},
|
||||
parameters: {
|
||||
msw: [
|
||||
...meta.parameters?.msw,
|
||||
graphql.mutation(
|
||||
getOperationName(CREATE_ACTIVITY_WITH_COMMENT) ?? '',
|
||||
(req, res, ctx) => {
|
||||
return res(
|
||||
ctx.data({
|
||||
createOneActivity: mockedTasks[0],
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
graphql.query(getOperationName(GET_ACTIVITY) ?? '', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.data({
|
||||
findManyActivities: [mockedTasks[0]],
|
||||
}),
|
||||
);
|
||||
}),
|
||||
graphql.mutation(
|
||||
getOperationName(UPDATE_ONE_COMPANY) ?? '',
|
||||
(req, res, ctx) => {
|
||||
return res(
|
||||
ctx.data({
|
||||
updateOneCompany: [mockedCompaniesData[0]],
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -2,7 +2,6 @@ import { useParams } from 'react-router-dom';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
import { Timeline } from '@/activities/timeline/components/Timeline';
|
||||
import { ActivityTargetableEntityType } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { GET_PERSON } from '@/people/graphql/queries/getPerson';
|
||||
@ -14,9 +13,12 @@ import { EditableFieldMutationContext } from '@/ui/editable-field/contexts/Edita
|
||||
import { PropertyBox } from '@/ui/editable-field/property-box/components/PropertyBox';
|
||||
import { IconUser } from '@/ui/icon';
|
||||
import { WithTopBarContainer } from '@/ui/layout/components/WithTopBarContainer';
|
||||
import { ShowPageAddButton } from '@/ui/layout/show-page/components/ShowPageAddButton';
|
||||
import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer';
|
||||
import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer';
|
||||
import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard';
|
||||
import { ShowPageRecoilScopeContext } from '@/ui/layout/states/ShowPageRecoilScopeContext';
|
||||
import { RecoilScope } from '@/ui/utilities/recoil-scope/components/RecoilScope';
|
||||
import {
|
||||
useUpdateOnePersonMutation,
|
||||
useUploadPersonPictureMutation,
|
||||
@ -67,7 +69,17 @@ export function PersonShow() {
|
||||
hasBackButton
|
||||
isFavorite={isFavorite}
|
||||
onFavoriteButtonClick={handleFavoriteButtonClick}
|
||||
extraButtons={[
|
||||
<ShowPageAddButton
|
||||
key="add"
|
||||
entity={{
|
||||
id: person.id,
|
||||
type: ActivityTargetableEntityType.Person,
|
||||
}}
|
||||
/>,
|
||||
]}
|
||||
>
|
||||
<RecoilScope SpecificContext={ShowPageRecoilScopeContext}>
|
||||
<ShowPageContainer>
|
||||
<ShowPageLeftContainer>
|
||||
<ShowPageSummaryCard
|
||||
@ -99,15 +111,18 @@ export function PersonShow() {
|
||||
</EditableFieldMutationContext.Provider>
|
||||
</PropertyBox>
|
||||
</ShowPageLeftContainer>
|
||||
<ShowPageRightContainer>
|
||||
<Timeline
|
||||
<ShowPageRightContainer
|
||||
entity={{
|
||||
id: person.id ?? '',
|
||||
type: ActivityTargetableEntityType.Person,
|
||||
}}
|
||||
timeline
|
||||
tasks
|
||||
notes
|
||||
emails
|
||||
/>
|
||||
</ShowPageRightContainer>
|
||||
</ShowPageContainer>
|
||||
</RecoilScope>
|
||||
</WithTopBarContainer>
|
||||
);
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { TaskGroups } from '@/activities/components/TaskGroups';
|
||||
import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer';
|
||||
import { TasksRecoilScopeContext } from '@/activities/states/recoil-scope-contexts/TasksRecoilScopeContext';
|
||||
import { TaskGroups } from '@/activities/tasks/components/TaskGroups';
|
||||
import { FilterDropdownButton } from '@/ui/filter-n-sort/components/FilterDropdownButton';
|
||||
import { FiltersHotkeyScope } from '@/ui/filter-n-sort/types/FiltersHotkeyScope';
|
||||
import { IconArchive, IconCheck, IconCheckbox } from '@/ui/icon/index';
|
||||
|
@ -222,7 +222,10 @@ export const graphqlMocks = [
|
||||
graphql.query(getOperationName(GET_ACTIVITIES) ?? '', (req, res, ctx) => {
|
||||
return res(
|
||||
ctx.data({
|
||||
findManyActivities: mockedActivities,
|
||||
findManyActivities:
|
||||
req?.variables?.where?.type?.equals === 'Task'
|
||||
? mockedTasks
|
||||
: mockedActivities,
|
||||
}),
|
||||
);
|
||||
}),
|
||||
|
Loading…
Reference in New Issue
Block a user