Enable Task creation (#688)

This commit is contained in:
Charles Bochet
2023-07-16 09:39:52 -07:00
committed by GitHub
parent 098cd038bd
commit 037628ab1d
126 changed files with 3032 additions and 253 deletions

View File

@ -9,7 +9,7 @@ import { BlockEditor } from '@/ui/components/editor/BlockEditor';
import { debounce } from '@/utils/debounce';
import {
CommentThread,
useUpdateCommentThreadBodyMutation,
useUpdateCommentThreadMutation,
} from '~/generated/graphql';
const BlockNoteStyledContainer = styled.div`
@ -22,8 +22,7 @@ type OwnProps = {
};
export function CommentThreadBodyEditor({ commentThread, onChange }: OwnProps) {
const [updateCommentThreadBodyMutation] =
useUpdateCommentThreadBodyMutation();
const [updateCommentThreadMutation] = useUpdateCommentThreadMutation();
const [body, setBody] = useState<string | null>(null);
@ -36,10 +35,10 @@ export function CommentThreadBodyEditor({ commentThread, onChange }: OwnProps) {
const debounceOnChange = useMemo(() => {
function onInternalChange(commentThreadBody: string) {
setBody(commentThreadBody);
updateCommentThreadBodyMutation({
updateCommentThreadMutation({
variables: {
commentThreadId: commentThread.id,
commentThreadBody: commentThreadBody,
id: commentThread.id,
body: commentThreadBody,
},
refetchQueries: [
getOperationName(GET_COMMENT_THREADS_BY_TARGETS) ?? '',
@ -48,7 +47,7 @@ export function CommentThreadBodyEditor({ commentThread, onChange }: OwnProps) {
}
return debounce(onInternalChange, 200);
}, [commentThread, updateCommentThreadBodyMutation, setBody]);
}, [commentThread, updateCommentThreadMutation, setBody]);
const editor: BlockNoteEditor | null = useBlockNote({
initialContent: commentThread.body

View File

@ -26,7 +26,6 @@ export function CommentThreadCreateButton({
<Button
icon={<IconCheckbox size={theme.icon.size.sm} />}
title="Task"
soon={true}
onClick={onTaskClick}
/>
<Button

View File

@ -2,17 +2,59 @@ import {
DropdownButton,
DropdownOptionType,
} from '@/ui/components/buttons/DropdownButton';
import { IconNotes } from '@/ui/icons/index';
import { IconCheck, IconNotes } from '@/ui/icons/index';
import {
ActivityType,
CommentThread,
useUpdateCommentThreadMutation,
} from '~/generated/graphql';
export function CommentThreadTypeDropdown() {
type OwnProps = {
commentThread: Pick<CommentThread, 'id' | 'type'>;
};
export function CommentThreadTypeDropdown({ commentThread }: OwnProps) {
const [updateCommentThreadMutation] = useUpdateCommentThreadMutation();
const options: DropdownOptionType[] = [
{ label: 'Note', icon: <IconNotes /> },
// { label: 'Call', icon: <IconPhone /> },
{ label: 'Note', key: 'note', icon: <IconNotes /> },
{ label: 'Task', key: 'task', icon: <IconCheck /> },
];
const handleSelect = (selectedOption: DropdownOptionType) => {
// console.log(`You selected: ${selectedOption.label}`);
function getSelectedOptionKey() {
if (commentThread.type === ActivityType.Note) {
return 'note';
} else if (commentThread.type === ActivityType.Task) {
return 'task';
} else {
return undefined;
}
}
const convertSelectionOptionKeyToActivityType = (key: string) => {
switch (key) {
case 'note':
return ActivityType.Note;
case 'task':
return ActivityType.Task;
default:
return undefined;
}
};
return <DropdownButton options={options} onSelection={handleSelect} />;
const handleSelect = (selectedOption: DropdownOptionType) => {
updateCommentThreadMutation({
variables: {
id: commentThread.id,
type: convertSelectionOptionKeyToActivityType(selectedOption.key),
},
});
};
return (
<DropdownButton
options={options}
onSelection={handleSelect}
selectedOptionKey={getSelectedOptionKey()}
/>
);
}

View File

@ -9,7 +9,7 @@ import { IconArrowUpRight } from '@/ui/icons/index';
import { debounce } from '@/utils/debounce';
import {
useGetCommentThreadQuery,
useUpdateCommentThreadTitleMutation,
useUpdateCommentThreadMutation,
} from '~/generated/graphql';
import { CommentThreadBodyEditor } from '../comment-thread/CommentThreadBodyEditor';
@ -105,21 +105,20 @@ export function CommentThread({
const [hasUserManuallySetTitle, setHasUserManuallySetTitle] =
useState<boolean>(false);
const [updateCommentThreadTitleMutation] =
useUpdateCommentThreadTitleMutation();
const [updateCommentThreadMutation] = useUpdateCommentThreadMutation();
const debounceUpdateTitle = useMemo(() => {
function updateTitle(title: string) {
updateCommentThreadTitleMutation({
updateCommentThreadMutation({
variables: {
commentThreadId: commentThreadId,
commentThreadTitle: title ?? '',
id: commentThreadId,
title: title ?? '',
},
refetchQueries: [getOperationName(GET_COMMENT_THREAD) ?? ''],
});
}
return debounce(updateTitle, 200);
}, [commentThreadId, updateCommentThreadTitleMutation]);
}, [commentThreadId, updateCommentThreadMutation]);
function updateTitleFromBody(body: string) {
const parsedTitle = JSON.parse(body)[0]?.content[0]?.text;
@ -144,11 +143,11 @@ export function CommentThread({
<StyledUpperPartContainer>
<StyledTopContainer>
<StyledTopActionsContainer>
<CommentThreadTypeDropdown />
<CommentThreadTypeDropdown commentThread={commentThread} />
<CommentThreadActionBar commentThreadId={commentThread?.id ?? ''} />
</StyledTopActionsContainer>
<StyledEditableTitleInput
placeholder="Note title (optional)"
placeholder={`${commentThread.type} title (optional)`}
onChange={(event) => {
setHasUserManuallySetTitle(true);
setTitle(event.target.value);

View File

@ -12,7 +12,7 @@ export function RightDrawerCreateCommentThread() {
return (
<RightDrawerPage>
<RightDrawerTopBar title="New note" />
<RightDrawerTopBar />
<RightDrawerBody>
{commentThreadId && (
<CommentThread

View File

@ -12,7 +12,7 @@ export function RightDrawerEditCommentThread() {
return (
<RightDrawerPage>
<RightDrawerTopBar title="" />
<RightDrawerTopBar />
<RightDrawerBody>
{commentThreadId && <CommentThread commentThreadId={commentThreadId} />}
</RightDrawerBody>

View File

@ -12,6 +12,7 @@ import {
beautifyPastDateRelativeToNow,
} from '@/utils/datetime/date-utils';
import {
ActivityType,
SortOrder,
useGetCommentThreadsByTargetsQuery,
} from '~/generated/graphql';
@ -187,7 +188,7 @@ const StyledTopActionBar = styled.div`
`;
export function Timeline({ entity }: { entity: CommentableEntity }) {
const { data: queryResult } = useGetCommentThreadsByTargetsQuery({
const { data: queryResult, loading } = useGetCommentThreadsByTargetsQuery({
variables: {
commentThreadTargetIds: [entity.id],
orderBy: [
@ -205,13 +206,18 @@ export function Timeline({ entity }: { entity: CommentableEntity }) {
const commentThreads: CommentThreadForDrawer[] =
queryResult?.findManyCommentThreads ?? [];
if (loading) {
return <></>;
}
if (!commentThreads.length) {
return (
<StyledTimelineEmptyContainer>
<StyledEmptyTimelineTitle>No activity yet</StyledEmptyTimelineTitle>
<StyledEmptyTimelineSubTitle>Create one:</StyledEmptyTimelineSubTitle>
<CommentThreadCreateButton
onNoteClick={() => openCreateCommandThread(entity)}
onNoteClick={() => openCreateCommandThread(entity, ActivityType.Note)}
onTaskClick={() => openCreateCommandThread(entity, ActivityType.Task)}
/>
</StyledTimelineEmptyContainer>
);
@ -222,7 +228,12 @@ export function Timeline({ entity }: { entity: CommentableEntity }) {
<StyledTopActionBar>
<StyledTimelineItemContainer>
<CommentThreadCreateButton
onNoteClick={() => openCreateCommandThread(entity)}
onNoteClick={() =>
openCreateCommandThread(entity, ActivityType.Note)
}
onTaskClick={() =>
openCreateCommandThread(entity, ActivityType.Task)
}
/>
</StyledTimelineItemContainer>
</StyledTopActionBar>

View File

@ -10,6 +10,7 @@ import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDraw
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { selectedRowIdsSelector } from '@/ui/tables/states/selectedRowIdsSelector';
import {
ActivityType,
CommentableType,
useCreateCommentThreadMutation,
} from '~/generated/graphql';
@ -53,6 +54,7 @@ export function useOpenCreateCommentThreadDrawerForSelectedRowIds() {
authorId: currentUser?.id ?? '',
commentThreadId: v4(),
createdAt: new Date().toISOString(),
type: ActivityType.Note,
commentThreadTargetArray: commentableEntityArray.map((entity) => ({
commentableId: entity.id,
commentableType: entity.type,

View File

@ -8,7 +8,10 @@ import { useSetHotkeyScope } from '@/lib/hotkeys/hooks/useSetHotkeyScope';
import { GET_PEOPLE } from '@/people/services';
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
import { useCreateCommentThreadMutation } from '~/generated/graphql';
import {
ActivityType,
useCreateCommentThreadMutation,
} from '~/generated/graphql';
import { useOpenRightDrawer } from '../../ui/layout/right-drawer/hooks/useOpenRightDrawer';
import {
@ -32,12 +35,16 @@ export function useOpenCreateCommentThreadDrawer() {
viewableCommentThreadIdState,
);
return function openCreateCommentThreadDrawer(entity: CommentableEntity) {
return function openCreateCommentThreadDrawer(
entity: CommentableEntity,
type: ActivityType,
) {
createCommentThreadMutation({
variables: {
authorId: currentUser?.id ?? '',
commentThreadId: v4(),
createdAt: new Date().toISOString(),
type: type,
commentThreadTargetArray: [
{
commentableId: entity.id,

View File

@ -37,6 +37,7 @@ export const CREATE_COMMENT_THREAD_WITH_COMMENT = gql`
$commentThreadId: String!
$body: String
$title: String
$type: ActivityType!
$authorId: String!
$createdAt: DateTime!
$commentThreadTargetArray: [CommentThreadTargetCreateManyCommentThreadInput!]!
@ -49,6 +50,7 @@ export const CREATE_COMMENT_THREAD_WITH_COMMENT = gql`
author: { connect: { id: $authorId } }
body: $body
title: $title
type: $type
commentThreadTargets: {
createMany: { data: $commentThreadTargetArray, skipDuplicates: true }
}
@ -58,6 +60,7 @@ export const CREATE_COMMENT_THREAD_WITH_COMMENT = gql`
createdAt
updatedAt
authorId
type
commentThreadTargets {
id
createdAt

View File

@ -17,6 +17,7 @@ export const GET_COMMENT_THREADS_BY_TARGETS = gql`
createdAt
title
body
type
author {
id
firstName
@ -51,6 +52,7 @@ export const GET_COMMENT_THREAD = gql`
createdAt
body
title
type
author {
id
firstName

View File

@ -69,32 +69,25 @@ export const DELETE_COMMENT_THREAD = gql`
}
`;
export const UPDATE_COMMENT_THREAD_TITLE = gql`
mutation UpdateCommentThreadTitle(
$commentThreadId: String!
$commentThreadTitle: String
export const UPDATE_COMMENT_THREAD = gql`
mutation UpdateCommentThread(
$id: String!
$body: String
$title: String
$type: ActivityType
) {
updateOneCommentThread(
where: { id: $commentThreadId }
data: { title: { set: $commentThreadTitle } }
) {
id
title
}
}
`;
export const UPDATE_COMMENT_THREAD_BODY = gql`
mutation UpdateCommentThreadBody(
$commentThreadId: String!
$commentThreadBody: String
) {
updateOneCommentThread(
where: { id: $commentThreadId }
data: { body: { set: $commentThreadBody } }
where: { id: $id }
data: {
body: { set: $body }
title: { set: $title }
type: { set: $type }
}
) {
id
body
title
type
}
}
`;

View File

@ -1,6 +1,8 @@
import { useState } from 'react';
import { getOperationName } from '@apollo/client/utilities';
import { v4 } from 'uuid';
import { GET_COMPANIES } from '@/companies/services';
import { useRecoilScopedState } from '@/recoil-scope/hooks/useRecoilScopedState';
import { relationPickerSearchFilterScopedState } from '@/relation-picker/states/relationPickerSearchFilterScopedState';
import { isCreateModeScopedState } from '@/ui/components/editable-cell/states/isCreateModeScopedState';
@ -49,6 +51,7 @@ export function PeopleCompanyCreateCell({ people }: OwnProps) {
address: '',
createdAt: new Date().toISOString(),
},
refetchQueries: [getOperationName(GET_COMPANIES) || ''],
});
await updatePeople({

View File

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

View File

@ -5,7 +5,7 @@ const StyledPropertyBoxItem = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
height: 32px;
padding: ${({ theme }) => theme.spacing(1)};
`;
const StyledIconContainer = styled.div`

View File

@ -1,7 +1,5 @@
import styled from '@emotion/styled';
import { Button } from '@/ui/components/buttons/Button';
import { RightDrawerTopBarCloseButton } from './RightDrawerTopBarCloseButton';
const StyledRightDrawerTopBar = styled.div`
@ -18,26 +16,14 @@ const StyledRightDrawerTopBar = styled.div`
padding-right: 8px;
`;
const StyledTopBarTitle = styled.div`
align-items: center;
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-right: ${({ theme }) => theme.spacing(1)};
`;
type OwnProps = {
title?: string | null | undefined;
onSave?: () => void;
};
export function RightDrawerTopBar({ title, onSave }: OwnProps) {
function handleOnClick() {
onSave?.();
}
export function RightDrawerTopBar({ title }: OwnProps) {
return (
<StyledRightDrawerTopBar>
<RightDrawerTopBarCloseButton />
<StyledTopBarTitle>{title}</StyledTopBarTitle>
{onSave ? <Button title="Save" onClick={handleOnClick} /> : <div></div>}
</StyledRightDrawerTopBar>
);
}