Skip to content

Commit

Permalink
feat: Upgrade Task Editor to TipTap (#10526)
Browse files Browse the repository at this point in the history
Signed-off-by: Matt Krick <[email protected]>
  • Loading branch information
mattkrick authored Nov 26, 2024
1 parent 1c1d17c commit 6a05e4b
Show file tree
Hide file tree
Showing 89 changed files with 981 additions and 1,386 deletions.
9 changes: 6 additions & 3 deletions packages/client/components/MentionDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@ import TypeAheadLabel from './TypeAheadLabel'

export default forwardRef(
(
props: SuggestionProps<{id: string; preferredName: string; picture: string}, MentionNodeAttrs>,
props: SuggestionProps<
{userId: string; preferredName: string; picture: string},
MentionNodeAttrs
>,
ref
) => {
const {command, items, query} = props
const [selectedIndex, setSelectedIndex] = useState(0)
const selectItem = (idx: number) => {
const item = items[idx]
if (!item) return
command({id: item.id, label: item.preferredName})
command({id: item.userId, label: item.preferredName})
}

const upHandler = () => {
Expand Down Expand Up @@ -60,7 +63,7 @@ export default forwardRef(
className={
'flex w-full cursor-pointer items-center rounded-md px-4 py-1 text-sm leading-8 text-slate-700 outline-none hover:!bg-slate-200 hover:text-slate-900 focus:bg-slate-200 data-highlighted:bg-slate-100 data-highlighted:text-slate-900'
}
key={item.id}
key={item.userId}
onClick={() => selectItem(idx)}
>
<Avatar picture={item.picture} className='h-6 w-6' />
Expand Down
4 changes: 2 additions & 2 deletions packages/client/components/NewAzureIssueInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import useForm from '../hooks/useForm'
import {PortalStatus} from '../hooks/usePortal'
import useTimedState from '../hooks/useTimedState'
import UpdatePokerScopeMutation from '../mutations/UpdatePokerScopeMutation'
import {convertTipTapTaskContent} from '../shared/tiptap/convertTipTapTaskContent'
import {CompletedHandler} from '../types/relayMutations'
import convertToTaskContent from '../utils/draftjs/convertToTaskContent'
import Legitity from '../validation/Legitity'
import Checkbox from './Checkbox'
import NewAzureIssueMenu from './NewAzureIssueMenu'
Expand Down Expand Up @@ -187,7 +187,7 @@ const NewAzureIssueInput = (props: Props) => {
teamId,
userId,
meetingId,
content: convertToTaskContent(`${newIssueTitle} #archived`),
content: convertTipTapTaskContent(newIssueTitle, ['archived']),
plaintextContent: newIssueTitle,
status: 'active' as const,
integration: {
Expand Down
4 changes: 2 additions & 2 deletions packages/client/components/NewGitHubIssueInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import useTimedState from '../hooks/useTimedState'
import CreateTaskMutation from '../mutations/CreateTaskMutation'
import UpdatePokerScopeMutation from '../mutations/UpdatePokerScopeMutation'
import GitHubIssueId from '../shared/gqlIds/GitHubIssueId'
import {convertTipTapTaskContent} from '../shared/tiptap/convertTipTapTaskContent'
import {CompletedHandler} from '../types/relayMutations'
import convertToTaskContent from '../utils/draftjs/convertToTaskContent'
import Legitity from '../validation/Legitity'
import Checkbox from './Checkbox'
import NewGitHubIssueMenu from './NewGitHubIssueMenu'
Expand Down Expand Up @@ -187,7 +187,7 @@ const NewGitHubIssueInput = (props: Props) => {
teamId,
userId,
meetingId,
content: convertToTaskContent(`${newIssueTitle} #archived`),
content: convertTipTapTaskContent(newIssueTitle, ['archived']),
plaintextContent: newIssueTitle,
status: 'active' as const,
integration: {
Expand Down
4 changes: 2 additions & 2 deletions packages/client/components/NewGitLabIssueInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import {PortalStatus} from '../hooks/usePortal'
import useTimedState from '../hooks/useTimedState'
import CreateTaskMutation from '../mutations/CreateTaskMutation'
import UpdatePokerScopeMutation from '../mutations/UpdatePokerScopeMutation'
import {convertTipTapTaskContent} from '../shared/tiptap/convertTipTapTaskContent'
import {CompletedHandler} from '../types/relayMutations'
import convertToTaskContent from '../utils/draftjs/convertToTaskContent'
import Legitity from '../validation/Legitity'
import Checkbox from './Checkbox'
import NewGitLabIssueMenu from './NewGitLabIssueMenu'
Expand Down Expand Up @@ -201,7 +201,7 @@ const NewGitLabIssueInput = (props: Props) => {
teamId,
userId,
meetingId,
content: convertToTaskContent(`${newIssueTitle} #archived`),
content: convertTipTapTaskContent(newIssueTitle, ['archived']),
plaintextContent: newIssueTitle,
status: 'active' as const,
integration: {
Expand Down
4 changes: 2 additions & 2 deletions packages/client/components/NewJiraIssueInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ import CreateTaskMutation from '../mutations/CreateTaskMutation'
import UpdatePokerScopeMutation from '../mutations/UpdatePokerScopeMutation'
import JiraIssueId from '../shared/gqlIds/JiraIssueId'
import JiraProjectId from '../shared/gqlIds/JiraProjectId'
import {convertTipTapTaskContent} from '../shared/tiptap/convertTipTapTaskContent'
import {CompletedHandler} from '../types/relayMutations'
import convertToTaskContent from '../utils/draftjs/convertToTaskContent'
import Legitity from '../validation/Legitity'
import Checkbox from './Checkbox'
import NewJiraIssueMenu from './NewJiraIssueMenu'
Expand Down Expand Up @@ -199,7 +199,7 @@ const NewJiraIssueInput = (props: Props) => {
teamId,
userId,
meetingId,
content: convertToTaskContent(`${newIssueTitle} #archived`),
content: convertTipTapTaskContent(newIssueTitle, ['archived']),
plaintextContent: newIssueTitle,
status: 'active' as const,
integration: {
Expand Down
34 changes: 20 additions & 14 deletions packages/client/components/NullableTask/NullableTask.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import graphql from 'babel-plugin-relay/macro'
import {convertFromRaw} from 'draft-js'
import {useMemo} from 'react'
import {useFragment} from 'react-relay'
import {AreaEnum, TaskStatusEnum} from '~/__generated__/UpdateTaskMutation.graphql'
import {NullableTask_task$key} from '../../__generated__/NullableTask_task.graphql'
import useAtmosphere from '../../hooks/useAtmosphere'
import {useTipTapTaskEditor} from '../../hooks/useTipTapTaskEditor'
import OutcomeCardContainer from '../../modules/outcomeCard/containers/OutcomeCard/OutcomeCardContainer'
import makeEmptyStr from '../../utils/draftjs/makeEmptyStr'
import isTaskArchived from '../../utils/isTaskArchived'
import isTempId from '../../utils/relay/isTempId'
import NullCard from '../NullCard/NullCard'

interface Props {
Expand Down Expand Up @@ -36,6 +36,7 @@ const NullableTask = (props: Props) => {
# from this place upward the tree, the task components are also used outside of meetings, thus we default to null here
fragment NullableTask_task on Task
@argumentDefinitions(meetingId: {type: "ID", defaultValue: null}) {
id
content
createdBy
createdByUser {
Expand All @@ -45,30 +46,35 @@ const NullableTask = (props: Props) => {
__typename
}
status
teamId
tags
...OutcomeCardContainer_task @arguments(meetingId: $meetingId)
}
`,
taskRef
)
const {content, createdBy, createdByUser, integration} = task
const {content, createdBy, createdByUser, integration, teamId, id: taskId, tags} = task
const isIntegration = !!integration?.__typename
const {preferredName} = createdByUser
const contentState = useMemo(() => {
try {
return convertFromRaw(JSON.parse(content))
} catch (e) {
return convertFromRaw(JSON.parse(makeEmptyStr()))
}
}, [content])

const atmosphere = useAtmosphere()
const isArchived = isTaskArchived(tags)
const readOnly = isTempId(taskId) || isArchived || !!isDraggingOver || isIntegration
const {editor, linkState, setLinkState} = useTipTapTaskEditor(content, {
atmosphere,
teamId,
readOnly
})

const showOutcome = contentState.hasText() || createdBy === atmosphere.viewerId || integration
const showOutcome =
editor && (!editor.isEmpty || createdBy === atmosphere.viewerId || isIntegration)
return showOutcome ? (
<OutcomeCardContainer
dataCy={`${dataCy}`}
area={area}
className={className}
contentState={contentState}
editor={editor}
linkState={linkState}
setLinkState={setLinkState}
isDraggingOver={isDraggingOver}
isAgenda={isAgenda}
task={task}
Expand Down
74 changes: 20 additions & 54 deletions packages/client/components/ParabolScopingSearchResultItem.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import styled from '@emotion/styled'
import graphql from 'babel-plugin-relay/macro'
import {convertToRaw} from 'draft-js'
import {useRef} from 'react'
import {useFragment} from 'react-relay'
import useAtmosphere from '~/hooks/useAtmosphere'
import useEditorState from '~/hooks/useEditorState'
import useMutationProps from '~/hooks/useMutationProps'
import useScrollIntoView from '~/hooks/useScrollIntoVIew'
import useTaskChildFocus from '~/hooks/useTaskChildFocus'
import DeleteTaskMutation from '~/mutations/DeleteTaskMutation'
import UpdatePokerScopeMutation from '~/mutations/UpdatePokerScopeMutation'
import UpdateTaskMutation from '~/mutations/UpdateTaskMutation'
import {PALETTE} from '~/styles/paletteV3'
import convertToTaskContent from '~/utils/draftjs/convertToTaskContent'
import isAndroid from '~/utils/draftjs/isAndroid'
import {ParabolScopingSearchResultItem_task$key} from '../__generated__/ParabolScopingSearchResultItem_task.graphql'
import {UpdatePokerScopeMutation as TUpdatePokerScopeMutation} from '../__generated__/UpdatePokerScopeMutation.graphql'
import {AreaEnum} from '../__generated__/UpdateTaskMutation.graphql'
import {useTipTapTaskEditor} from '../hooks/useTipTapTaskEditor'
import {Threshold} from '../types/constEnums'
import Checkbox from './Checkbox'
import TaskEditor from './TaskEditor/TaskEditor'
import {TipTapEditor} from './promptResponse/TipTapEditor'

const Item = styled('div')<{isEditingThisItem: boolean}>(({isEditingThisItem}) => ({
backgroundColor: isEditingThisItem ? PALETTE.SLATE_100 : 'transparent',
Expand All @@ -34,14 +30,6 @@ const Task = styled('div')({
width: '100%'
})

const StyledTaskEditor = styled(TaskEditor)({
width: '100%',
paddingTop: 4,
fontSize: '16px',
lineHeight: 'normal',
height: 'auto'
})

interface Props {
meetingId: string
usedServiceTaskIds: Set<string>
Expand Down Expand Up @@ -80,8 +68,7 @@ const ParabolScopingSearchResultItem = (props: Props) => {
const disabled = !isSelected && usedServiceTaskIds.size >= Threshold.MAX_POKER_STORIES
const atmosphere = useAtmosphere()
const {onCompleted, onError, submitMutation, submitting} = useMutationProps()
const [editorState, setEditorState] = useEditorState(content)
const editorRef = useRef<HTMLTextAreaElement>(null)
const {editor, linkState, setLinkState} = useTipTapTaskEditor(content, {atmosphere, teamId})
const {useTaskChild, addTaskChild, removeTaskChild, isTaskFocused} =
useTaskChildFocus(serviceTaskId)
const isEditingThisItem = !plaintextContent
Expand All @@ -107,44 +94,26 @@ const ParabolScopingSearchResultItem = (props: Props) => {
}

const handleTaskUpdate = () => {
if (!editor) return
const isFocused = isTaskFocused()
const area: AreaEnum = 'meeting'
if (isAndroid) {
const editorEl = editorRef.current
if (!editorEl || editorEl.type !== 'textarea') return
const {value} = editorEl
if (!value && !isFocused) {
DeleteTaskMutation(atmosphere, {taskId: serviceTaskId})
} else {
const initialContentState = editorState.getCurrentContent()
const initialText = initialContentState.getPlainText()
if (initialText === value) return
const updatedTask = {
id: serviceTaskId,
content: convertToTaskContent(value)
}
UpdateTaskMutation(atmosphere, {updatedTask, area}, {onCompleted: updatePokerScope})
}
if (editor.isEmpty && !isFocused) {
DeleteTaskMutation(atmosphere, {taskId: serviceTaskId})
return
}
const nextContentState = editorState.getCurrentContent()
const hasText = nextContentState.hasText()
if (!hasText && !isFocused) {
DeleteTaskMutation(atmosphere, {taskId: serviceTaskId})
} else {
const nextContent = JSON.stringify(convertToRaw(nextContentState))
if (nextContent === content) return
const updatedTask = {
id: serviceTaskId,
content: nextContent
}
UpdateTaskMutation(atmosphere, {updatedTask, area}, {onCompleted: updatePokerScope})
const nextContent = JSON.stringify(editor.getJSON())
if (content === nextContent) {
return
}
const updatedTask = {
id: serviceTaskId,
content: nextContent
}
UpdateTaskMutation(atmosphere, {updatedTask}, {})
}

const ref = useRef<HTMLDivElement>(null)
useScrollIntoView(ref, isEditingThisItem)

if (!editor) return null
return (
<Item
onClick={() => {
Expand All @@ -167,14 +136,11 @@ const ParabolScopingSearchResultItem = (props: Props) => {
addTaskChild('root')
}}
>
<StyledTaskEditor
dataCy={`task`}
editorRef={editorRef}
readOnly={!isEditingThisItem}
editorState={editorState}
setEditorState={setEditorState}
teamId={teamId}
useTaskChild={useTaskChild}
<TipTapEditor
editor={editor}
linkState={linkState}
setLinkState={setLinkState}
useLinkEditor={() => useTaskChild('editor-link-changer')}
/>
</Task>
</Item>
Expand Down
Loading

0 comments on commit 6a05e4b

Please sign in to comment.