Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Upgrade Task Editor to TipTap #10526

Merged
merged 6 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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