(null)
+ editorRef.current = useEditor(
+ {
+ content: contentJSON,
+ extensions: [
+ StarterKit,
+ LoomExtension,
+ Placeholder.configure({
+ showOnlyWhenEditable: false,
+ placeholder: 'Describe what “Done” looks like'
+ }),
+ Mention.extend({name: 'taskTag'}).configure(tiptapTagConfig),
+ Mention.configure(
+ atmosphere && teamId ? tiptapMentionConfig(atmosphere, teamId) : mentionConfig
+ ),
+ Mention.extend({name: 'emojiMention'}).configure(tiptapEmojiConfig),
+ TiptapLinkExtension.configure({
+ openOnClick: false,
+ popover: {
+ setLinkState
+ }
+ })
+ ],
+ editable: !readOnly,
+ autofocus: generateText(contentJSON, serverTipTapExtensions).length === 0
+ },
+ [contentJSON, readOnly]
+ )
+ return {editor: editorRef.current, linkState, setLinkState}
+}
diff --git a/packages/client/modules/demo/ClientGraphQLServer.ts b/packages/client/modules/demo/ClientGraphQLServer.ts
index 0351e4381dc..9a3810b6839 100644
--- a/packages/client/modules/demo/ClientGraphQLServer.ts
+++ b/packages/client/modules/demo/ClientGraphQLServer.ts
@@ -1,4 +1,4 @@
-import {stateToHTML} from 'draft-js-export-html'
+import {generateHTML, generateJSON, generateText} from '@tiptap/core'
import EventEmitter from 'eventemitter3'
import {parse, stringify} from 'flatted'
import ms from 'ms'
@@ -17,6 +17,9 @@ import {
NewMeetingPhase
} from '../../../server/postgres/types/NewMeetingPhase'
import {Task as ITask} from '../../../server/postgres/types/index.d'
+import {getTagsFromTipTapTask} from '../../shared/tiptap/getTagsFromTipTapTask'
+import {serverTipTapExtensions} from '../../shared/tiptap/serverTipTapExtensions'
+import {splitTipTapContent} from '../../shared/tiptap/splitTipTapContent'
import {
ExternalLinks,
MeetingSettingsThreshold,
@@ -26,9 +29,6 @@ import {
import {DISCUSS, GROUP, REFLECT, VOTE} from '../../utils/constants'
import dndNoise from '../../utils/dndNoise'
import extractTextFromDraftString from '../../utils/draftjs/extractTextFromDraftString'
-import getTagsFromEntityMap from '../../utils/draftjs/getTagsFromEntityMap'
-import makeEmptyStr from '../../utils/draftjs/makeEmptyStr'
-import splitDraftContent from '../../utils/draftjs/splitDraftContent'
import findStageById from '../../utils/meetings/findStageById'
import sleep from '../../utils/sleep'
import getGroupSmartTitle from '../../utils/smartGroup/getGroupSmartTitle'
@@ -295,6 +295,17 @@ class ClientGraphQLServer extends (EventEmitter as GQLDemoEmitter) {
}
}
},
+ tiptapMentionConfigQuery: () => {
+ return {
+ viewer: {
+ ...this.db.users[0],
+ team: {
+ ...this.db.team,
+ teamMembers: this.db.teamMembers
+ }
+ }
+ }
+ },
NewMeetingSummaryQuery: () => {
return {
viewer: {
@@ -464,8 +475,9 @@ class ClientGraphQLServer extends (EventEmitter as GQLDemoEmitter) {
// if the human deleted the task, exit fast
if (!task) return null
const {content} = task
- const {title, contentState} = splitDraftContent(content)
- const bodyHTML = stateToHTML(contentState)
+ const doc = JSON.parse(content)
+ const {title, bodyContent} = splitTipTapContent(doc)
+ const bodyHTML = generateHTML(bodyContent, serverTipTapExtensions)
if (integrationProviderService === 'github') {
Object.assign(task, {
@@ -1269,11 +1281,13 @@ class ClientGraphQLServer extends (EventEmitter as GQLDemoEmitter) {
const now = new Date().toJSON()
const taskId = newTask.id || this.getTempId('task')
const {discussionId, threadParentId, threadSortOrder, sortOrder, status} = newTask
- const content = newTask.content || makeEmptyStr()
- const {entityMap} = JSON.parse(content)
- const tags = getTagsFromEntityMap(entityMap)
+ const content =
+ (newTask.content as string) ||
+ JSON.stringify(generateJSON('', serverTipTapExtensions))
+ const doc = JSON.parse(content)
+ const tags = getTagsFromTipTapTask(doc)
const user = this.db.users.find((user) => user.id === userId)
- const plaintextContent = extractTextFromDraftString(content)
+ const plaintextContent = generateText(doc, serverTipTapExtensions)
const task = {
__typename: 'Task',
__isThreadable: 'Task',
@@ -1420,7 +1434,7 @@ class ClientGraphQLServer extends (EventEmitter as GQLDemoEmitter) {
const taskUpdates = {
content,
status,
- tags: content ? getTagsFromEntityMap(JSON.parse(content).entityMap) : undefined,
+ tags: content ? getTagsFromTipTapTask(JSON.parse(content)) : undefined,
teamId: demoTeamId,
sortOrder,
userId: updatedTask.userId || task.userId
diff --git a/packages/client/modules/demo/taskLookup.ts b/packages/client/modules/demo/taskLookup.ts
index 4de3f578ed8..4febb6dbd2c 100644
--- a/packages/client/modules/demo/taskLookup.ts
+++ b/packages/client/modules/demo/taskLookup.ts
@@ -1,26 +1,61 @@
+import {generateJSON} from '@tiptap/core'
+import {serverTipTapExtensions} from '../../shared/tiptap/serverTipTapExtensions'
+
const taskLookup = {
botRef1: [
- `{"blocks":[{"key":"2t991","text":"Create a process for making a collective decision, together","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}`
+ JSON.stringify(
+ generateJSON(
+ 'Create a process for making a collective decision, together
',
+ serverTipTapExtensions
+ )
+ )
],
botRef2: [
- `{"blocks":[{"key":"2t992","text":"Document our testing process","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}`,
- `{"blocks":[{"key":"2t902","text":"When onboarding new employees, have them document our processes as they learn them","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}`
+ JSON.stringify(generateJSON('Document our testing process
', serverTipTapExtensions)),
+ JSON.stringify(
+ generateJSON(
+ 'When onboarding new employees, have them document our processes as they learn them
',
+ serverTipTapExtensions
+ )
+ )
],
botRef3: [
- `{"blocks":[{"key":"2t993","text":"Set a timer for speakers during meetings","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}`
+ JSON.stringify(
+ generateJSON('Set a timer for speakers during meetings
', serverTipTapExtensions)
+ )
],
botRef4: [
- `{"blocks":[{"key":"2t994","text":"Propose which kind of decisions need to be made by the whole group","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}`
+ JSON.stringify(
+ generateJSON(
+ 'Propose which kind of decisions need to be made by the whole group
',
+ serverTipTapExtensions
+ )
+ )
],
botRef5: [
- `{"blocks":[{"key":"2t995","text":"Create a policy for when to decide in-person vs. when to decide over Slack, and who needs to be involved for each type","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}`
+ JSON.stringify(
+ generateJSON(
+ 'Create a policy for when to decide in-person vs. when to decide over Slack, and who needs to be involved for each type
',
+ serverTipTapExtensions
+ )
+ )
],
botRef6: [
- `{"blocks":[{"key":"2t996","text":"Try no-meeting Thursdays","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}`,
- `{"blocks":[{"key":"2t906","text":"Use our planning meetings to discover which meetings and attendees to schedule","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}`
+ JSON.stringify(generateJSON('Try no-meeting Thursdays
', serverTipTapExtensions)),
+ JSON.stringify(
+ generateJSON(
+ 'Use our planning meetings to discover which meetings and attendees to schedule
',
+ serverTipTapExtensions
+ )
+ )
],
botRef7: [
- `{"blocks":[{"key":"2t997","text":"Research reputable methods for prioritizing work that the team can review together","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}],"entityMap":{}}`
+ JSON.stringify(
+ generateJSON(
+ 'Research reputable methods for prioritizing work that the team can review together
',
+ serverTipTapExtensions
+ )
+ )
],
botRef8: []
} as const
diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/EmailTaskCard.tsx b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/EmailTaskCard.tsx
index 9d54764f9c7..f2a50fa3500 100644
--- a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/EmailTaskCard.tsx
+++ b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/EmailTaskCard.tsx
@@ -1,15 +1,14 @@
+import {generateHTML} from '@tiptap/html'
import graphql from 'babel-plugin-relay/macro'
-import {convertFromRaw, Editor, EditorState} from 'draft-js'
import {EmailTaskCard_task$key} from 'parabol-client/__generated__/EmailTaskCard_task.graphql'
-import editorDecorators from 'parabol-client/components/TaskEditor/decorators'
import {PALETTE} from 'parabol-client/styles/paletteV3'
import {FONT_FAMILY} from 'parabol-client/styles/typographyV2'
import {taskStatusColors} from 'parabol-client/utils/taskStatus'
import * as React from 'react'
-import {useMemo, useRef} from 'react'
import {useFragment} from 'react-relay'
import {TaskStatusEnum} from '../../../../../__generated__/EmailTaskCard_task.graphql'
-import convertToTaskContent from '../../../../../utils/draftjs/convertToTaskContent'
+import {convertTipTapTaskContent} from '../../../../../shared/tiptap/convertTipTapTaskContent'
+import {serverTipTapExtensions} from '../../../../../shared/tiptap/serverTipTapExtensions'
interface Props {
task: EmailTaskCard_task$key | null
@@ -45,7 +44,7 @@ const statusStyle = (status: TaskStatusEnum) => ({
})
const deletedTask = {
- content: convertToTaskContent('<>'),
+ content: convertTipTapTaskContent('<>'),
status: 'done',
tags: [] as string[],
user: {
@@ -66,15 +65,7 @@ const EmailTaskCard = (props: Props) => {
taskRef
)
const {content, status} = task || deletedTask
- const contentState = useMemo(() => convertFromRaw(JSON.parse(content)), [content])
- const editorStateRef = useRef()
- const getEditorState = () => {
- return editorStateRef.current
- }
- editorStateRef.current = EditorState.createWithContent(
- contentState,
- editorDecorators(getEditorState)
- )
+ const htmlContent = generateHTML(JSON.parse(content), serverTipTapExtensions)
return (
@@ -97,13 +88,7 @@ const EmailTaskCard = (props: Props) => {
|
- {
- /**/
- }}
- />
+
|
diff --git a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/TeamPromptResponseSummaryCard.tsx b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/TeamPromptResponseSummaryCard.tsx
index 9ba97448509..296979cd630 100644
--- a/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/TeamPromptResponseSummaryCard.tsx
+++ b/packages/client/modules/email/components/SummaryEmail/MeetingSummaryEmail/TeamPromptResponseSummaryCard.tsx
@@ -5,7 +5,7 @@ import {TeamPromptResponseSummaryCard_stage$key} from 'parabol-client/__generate
import * as React from 'react'
import {useFragment} from 'react-relay'
import {PALETTE} from '~/styles/paletteV3'
-import {serverTipTapExtensions} from '../../../../../../server/utils/serverTipTapExtensions'
+import {serverTipTapExtensions} from '../../../../../shared/tiptap/serverTipTapExtensions'
const responseSummaryCardStyles: React.CSSProperties = {
padding: '12px',
diff --git a/packages/client/modules/outcomeCard/components/OutcomeCard/OutcomeCard.tsx b/packages/client/modules/outcomeCard/components/OutcomeCard/OutcomeCard.tsx
index f7d03e9f753..e714856a2d2 100644
--- a/packages/client/modules/outcomeCard/components/OutcomeCard/OutcomeCard.tsx
+++ b/packages/client/modules/outcomeCard/components/OutcomeCard/OutcomeCard.tsx
@@ -1,24 +1,25 @@
import styled from '@emotion/styled'
+import {Editor} from '@tiptap/core'
import graphql from 'babel-plugin-relay/macro'
-import {EditorState} from 'draft-js'
-import {memo, RefObject} from 'react'
+import {memo} from 'react'
import {useFragment} from 'react-relay'
import {OutcomeCard_task$key} from '~/__generated__/OutcomeCard_task.graphql'
import {AreaEnum, TaskStatusEnum} from '~/__generated__/UpdateTaskMutation.graphql'
import EditingStatus from '~/components/EditingStatus/EditingStatus'
import {PALETTE} from '~/styles/paletteV3'
import IntegratedTaskContent from '../../../../components/IntegratedTaskContent'
-import TaskEditor from '../../../../components/TaskEditor/TaskEditor'
+import {TipTapEditor} from '../../../../components/promptResponse/TipTapEditor'
+import {LinkMenuState} from '../../../../components/promptResponse/TipTapLinkMenu'
import TaskIntegrationLink from '../../../../components/TaskIntegrationLink'
import TaskWatermark from '../../../../components/TaskWatermark'
import useAtmosphere from '../../../../hooks/useAtmosphere'
import useTaskChildFocus, {UseTaskChild} from '../../../../hooks/useTaskChildFocus'
+import UpdateTaskMutation from '../../../../mutations/UpdateTaskMutation'
import {cardFocusShadow, cardHoverShadow, cardShadow, Elevation} from '../../../../styles/elevation'
import cardRootStyles from '../../../../styles/helpers/cardRootStyles'
import {Card} from '../../../../types/constEnums'
import isTaskArchived from '../../../../utils/isTaskArchived'
import isTaskPrivate from '../../../../utils/isTaskPrivate'
-import isTempId from '../../../../utils/relay/isTempId'
import {taskStatusLabels} from '../../../../utils/taskStatus'
import TaskFooter from '../OutcomeCardFooter/TaskFooter'
import OutcomeCardStatusIndicator from '../OutcomeCardStatusIndicator/OutcomeCardStatusIndicator'
@@ -60,13 +61,13 @@ interface Props {
area: AreaEnum
isTaskFocused: boolean
isTaskHovered: boolean
- editorRef: RefObject
- editorState: EditorState
+ editor: Editor
+ linkState: LinkMenuState
+ setLinkState: (linkState: LinkMenuState) => void
handleCardUpdate: () => void
isAgenda: boolean
isDraggingOver: TaskStatusEnum | undefined
task: OutcomeCard_task$key
- setEditorState: (newEditorState: EditorState) => void
useTaskChild: UseTaskChild
dataCy: string
}
@@ -76,13 +77,13 @@ const OutcomeCard = memo((props: Props) => {
area,
isTaskFocused,
isTaskHovered,
- editorRef,
- editorState,
+ editor,
+ linkState,
+ setLinkState,
handleCardUpdate,
isAgenda,
isDraggingOver,
task: taskRef,
- setEditorState,
useTaskChild,
dataCy
} = props
@@ -100,9 +101,6 @@ const OutcomeCard = memo((props: Props) => {
}
status
tags
- team {
- id
- }
# grab userId to ensure sorting on connections works
userId
isHighlighted(meetingId: $meetingId)
@@ -114,13 +112,35 @@ const OutcomeCard = memo((props: Props) => {
)
const isPrivate = isTaskPrivate(task.tags)
const isArchived = isTaskArchived(task.tags)
- const {integration, status, id: taskId, team, isHighlighted, editors} = task
+ const toggleTag = (tagId: string) => {
+ const {state, view} = editor
+ const {doc, schema, tr} = state
+ if (task.tags.includes(tagId)) {
+ doc.descendants((node, pos) => {
+ if (node.type.name === 'taskTag' && node.attrs.id === tagId) {
+ tr.delete(pos, pos + node.nodeSize)
+ }
+ })
+ view.dispatch(tr)
+ const nextContent = JSON.stringify(editor.getJSON())
+ UpdateTaskMutation(atmosphere, {updatedTask: {id: taskId, content: nextContent}}, {})
+ return
+ }
+ // Create the mention node
+ const mentionNode = schema.nodes.taskTag!.create({id: tagId})
+
+ // Insert the mention node at the end
+ const transaction = tr.insert(doc.content.size, mentionNode)
+ view.dispatch(transaction)
+ const nextContent = JSON.stringify(editor.getJSON())
+ UpdateTaskMutation(atmosphere, {updatedTask: {id: taskId, content: nextContent}}, {})
+ }
+ const {integration, status, id: taskId, isHighlighted, editors} = task
const atmosphere = useAtmosphere()
const {viewerId} = atmosphere
const otherEditors = editors.filter((editor) => editor.userId !== viewerId)
const isEditing = editors.length > otherEditors.length
const {addTaskChild, removeTaskChild} = useTaskChildFocus(taskId)
- const {id: teamId} = team
const type = integration?.__typename
const statusTitle = `Card status: ${taskStatusLabels[status]}`
const privateTitle = ', marked as #private'
@@ -158,14 +178,11 @@ const OutcomeCard = memo((props: Props) => {
}}
onFocus={() => addTaskChild('root')}
>
- useTaskChild('editor-link-changer')}
/>
)}
@@ -174,7 +191,7 @@ const OutcomeCard = memo((props: Props) => {
dataCy={`${dataCy}`}
area={area}
cardIsActive={isTaskFocused || isTaskHovered || isEditing}
- editorState={editorState}
+ toggleTag={toggleTag}
isAgenda={isAgenda}
task={task}
useTaskChild={useTaskChild}
diff --git a/packages/client/modules/outcomeCard/components/OutcomeCardFooter/TaskFooter.tsx b/packages/client/modules/outcomeCard/components/OutcomeCardFooter/TaskFooter.tsx
index f9c1423555b..ab7cf06a0ac 100644
--- a/packages/client/modules/outcomeCard/components/OutcomeCardFooter/TaskFooter.tsx
+++ b/packages/client/modules/outcomeCard/components/OutcomeCardFooter/TaskFooter.tsx
@@ -1,6 +1,5 @@
import styled from '@emotion/styled'
import graphql from 'babel-plugin-relay/macro'
-import {EditorState} from 'draft-js'
import {Fragment} from 'react'
import {useFragment} from 'react-relay'
import {AreaEnum} from '~/__generated__/UpdateTaskMutation.graphql'
@@ -13,7 +12,6 @@ import {UseTaskChild} from '../../../../hooks/useTaskChildFocus'
import {Card} from '../../../../types/constEnums'
import {CompletedHandler} from '../../../../types/relayMutations'
import {USER_DASH} from '../../../../utils/constants'
-import removeContentTag from '../../../../utils/draftjs/removeContentTag'
import isTaskArchived from '../../../../utils/isTaskArchived'
import setLocalTaskError from '../../../../utils/relay/setLocalTaskError'
import OutcomeCardMessage from '../OutcomeCardMessage/OutcomeCardMessage'
@@ -52,7 +50,7 @@ const AvatarBlock = styled('div')({
interface Props {
area: AreaEnum
cardIsActive: boolean
- editorState: EditorState
+ toggleTag: (tag: string) => void
isAgenda: boolean
task: TaskFooter_task$key
useTaskChild: UseTaskChild
@@ -60,12 +58,11 @@ interface Props {
}
const TaskFooter = (props: Props) => {
- const {area, cardIsActive, editorState, isAgenda, task: taskRef, useTaskChild, dataCy} = props
+ const {area, cardIsActive, toggleTag, isAgenda, task: taskRef, useTaskChild, dataCy} = props
const task = useFragment(
graphql`
fragment TaskFooter_task on Task {
id
- content
error
integration {
__typename
@@ -105,7 +102,7 @@ const TaskFooter = (props: Props) => {
}
const atmosphere = useAtmosphere()
const showTeam = area === USER_DASH
- const {content, id: taskId, error, integration, tags, userId} = task
+ const {id: taskId, error, integration, tags, userId} = task
const isArchived = isTaskArchived(tags)
const canAssignUser = !integration && !isArchived
const canAssignTeam = !isArchived
@@ -141,16 +138,14 @@ const TaskFooter = (props: Props) => {
/>
)}
{isArchived ? (
- removeContentTag('archived', atmosphere, taskId, content, area)}
- >
+ toggleTag('archived')}>
) : (
void
isAgenda: boolean
task: any
useTaskChild: UseTaskChild
@@ -25,7 +24,7 @@ const TaskFooterTagMenu = lazyPreload(
)
const TaskFooterTagMenuToggle = (props: Props) => {
- const {area, editorState, isAgenda, mutationProps, task, useTaskChild, dataCy} = props
+ const {area, toggleTag, isAgenda, mutationProps, task, useTaskChild, dataCy} = props
const {togglePortal, originRef, menuPortal, menuProps} = useMenu(MenuPosition.UPPER_RIGHT)
const {
tooltipPortal,
@@ -52,7 +51,7 @@ const TaskFooterTagMenuToggle = (props: Props) => {
{menuPortal(
void
// TODO make area enum more fine grained to get rid of isAgenda
isAgenda: boolean
mutationProps: MenuMutationProps
@@ -34,13 +31,12 @@ interface Props {
}
const TaskFooterTagMenu = (props: Props) => {
- const {area, menuProps, editorState, isAgenda, task: taskRef, useTaskChild} = props
+ const {area, menuProps, toggleTag, isAgenda, task: taskRef, useTaskChild} = props
const task = useFragment(
graphql`
fragment TaskFooterTagMenu_task on Task {
...TaskFooterTagMenuStatusItem_task
id
- content
status
tags
}
@@ -49,13 +45,8 @@ const TaskFooterTagMenu = (props: Props) => {
)
useTaskChild('tag')
const atmosphere = useAtmosphere()
- const {id: taskId, status: taskStatus, tags, content} = task
+ const {id: taskId, status: taskStatus, tags} = task
const isPrivate = isTaskPrivate(tags)
- const handlePrivate = () => {
- isPrivate
- ? removeContentTag('private', atmosphere, taskId, content, area)
- : addContentTag('#private', atmosphere, taskId, editorState.getCurrentContent(), area)
- }
return (
diff --git a/packages/client/modules/outcomeCard/containers/OutcomeCard/OutcomeCardContainer.tsx b/packages/client/modules/outcomeCard/containers/OutcomeCard/OutcomeCardContainer.tsx
index de54a30f7ab..c9216412a4e 100644
--- a/packages/client/modules/outcomeCard/containers/OutcomeCard/OutcomeCardContainer.tsx
+++ b/packages/client/modules/outcomeCard/containers/OutcomeCard/OutcomeCardContainer.tsx
@@ -1,6 +1,6 @@
import styled from '@emotion/styled'
+import {Editor} from '@tiptap/core'
import graphql from 'babel-plugin-relay/macro'
-import {ContentState, convertToRaw} from 'draft-js'
import {memo, useEffect, useRef, useState} from 'react'
import {useFragment} from 'react-relay'
import {OutcomeCardContainer_task$key} from '~/__generated__/OutcomeCardContainer_task.graphql'
@@ -8,13 +8,11 @@ import {AreaEnum, TaskStatusEnum} from '~/__generated__/UpdateTaskMutation.graph
import useClickAway from '~/hooks/useClickAway'
import useScrollIntoView from '~/hooks/useScrollIntoVIew'
import SetTaskHighlightMutation from '~/mutations/SetTaskHighlightMutation'
+import {LinkMenuState} from '../../../../components/promptResponse/TipTapLinkMenu'
import useAtmosphere from '../../../../hooks/useAtmosphere'
-import useEditorState from '../../../../hooks/useEditorState'
import useTaskChildFocus from '../../../../hooks/useTaskChildFocus'
import DeleteTaskMutation from '../../../../mutations/DeleteTaskMutation'
import UpdateTaskMutation from '../../../../mutations/UpdateTaskMutation'
-import convertToTaskContent from '../../../../utils/draftjs/convertToTaskContent'
-import isAndroid from '../../../../utils/draftjs/isAndroid'
import OutcomeCard from '../../components/OutcomeCard/OutcomeCard'
const Wrapper = styled('div')({
@@ -23,7 +21,9 @@ const Wrapper = styled('div')({
interface Props {
area: AreaEnum
- contentState: ContentState
+ editor: Editor
+ linkState: LinkMenuState
+ setLinkState: (linkState: LinkMenuState) => void
className?: string
isAgenda: boolean | undefined
isDraggingOver: TaskStatusEnum | undefined
@@ -36,7 +36,9 @@ interface Props {
const OutcomeCardContainer = memo((props: Props) => {
const {
- contentState,
+ editor,
+ linkState,
+ setLinkState,
className,
isDraggingOver,
task: taskRef,
@@ -63,9 +65,7 @@ const OutcomeCardContainer = memo((props: Props) => {
const atmosphere = useAtmosphere()
const ref = useRef(null)
const [isTaskHovered, setIsTaskHovered] = useState(false)
- const editorRef = useRef(null)
- const [editorState, setEditorState] = useEditorState(content)
const {useTaskChild, isTaskFocused} = useTaskChildFocus(taskId)
const isHighlighted = isTaskHovered || !!isDraggingOver
@@ -81,40 +81,20 @@ const OutcomeCardContainer = memo((props: Props) => {
const handleCardUpdate = () => {
const isFocused = isTaskFocused()
- if (isAndroid) {
- const editorEl = editorRef.current
- if (!editorEl || editorEl.type !== 'textarea') return
- const {value} = editorEl
- if (!value.trim() && !isFocused) {
- DeleteTaskMutation(atmosphere, {taskId})
- } else {
- const initialContentState = editorState.getCurrentContent()
- const initialText = initialContentState.getPlainText()
- if (initialText === value) return
- const updatedTask = {
- id: taskId,
- content: convertToTaskContent(value)
- }
- UpdateTaskMutation(atmosphere, {updatedTask, area}, {})
- }
+ if (editor.isEmpty && !isFocused) {
+ DeleteTaskMutation(atmosphere, {taskId})
return
}
- const nextContentState = editorState.getCurrentContent()
- const hasText = nextContentState.getPlainText().trim().length > 0
- if (!hasText && !isFocused) {
- DeleteTaskMutation(atmosphere, {taskId})
- } else {
- const content = JSON.stringify(convertToRaw(nextContentState))
- const initialContent = JSON.stringify(convertToRaw(contentState))
- if (content === initialContent) return
- const updatedTask = {
- id: taskId,
- content
- }
- UpdateTaskMutation(atmosphere, {updatedTask, area}, {})
+ const nextContent = JSON.stringify(editor.getJSON())
+ if (content === nextContent) return
+ const updatedTask = {
+ id: taskId,
+ content: nextContent
}
+ UpdateTaskMutation(atmosphere, {updatedTask, area}, {})
}
- useScrollIntoView(ref, !contentState.hasText())
+
+ useScrollIntoView(ref, editor.isEmpty)
useClickAway(ref, () => setIsTaskHovered(false))
return (
{
diff --git a/packages/client/modules/teamDashboard/components/TeamArchive/TeamArchive.tsx b/packages/client/modules/teamDashboard/components/TeamArchive/TeamArchive.tsx
index 12719f7227e..d2a575ae26c 100644
--- a/packages/client/modules/teamDashboard/components/TeamArchive/TeamArchive.tsx
+++ b/packages/client/modules/teamDashboard/components/TeamArchive/TeamArchive.tsx
@@ -12,7 +12,6 @@ import {
} from 'react-virtualized'
import {GridCellRenderer, GridCoreProps} from 'react-virtualized/dist/es/Grid'
import {TeamArchive_team$key} from '~/__generated__/TeamArchive_team.graphql'
-import extractTextFromDraftString from '~/utils/draftjs/extractTextFromDraftString'
import getSafeRegex from '~/utils/getSafeRegex'
import toTeamMemberId from '~/utils/relay/toTeamMemberId'
import {TeamArchiveArchivedTasksQuery} from '../../../../__generated__/TeamArchiveArchivedTasksQuery.graphql'
@@ -140,6 +139,7 @@ const TeamArchive = (props: Props) => {
teamId
userId
content
+ plaintextContent
...NullableTask_task
}
}
@@ -191,7 +191,7 @@ const TeamArchive = (props: Props) => {
if (!dashSearch) return teamMemberFilteredTasks
const dashSearchRegex = getSafeRegex(dashSearch, 'i')
const filteredEdges = teamMemberFilteredTasks.edges.filter((edge) =>
- extractTextFromDraftString(edge.node.content).match(dashSearchRegex)
+ edge.node.plaintextContent.match(dashSearchRegex)
)
return {...teamMemberFilteredTasks, edges: filteredEdges}
}, [dashSearch, teamMemberFilteredTasks])
diff --git a/packages/client/mutations/BatchArchiveTasksMutation.ts b/packages/client/mutations/BatchArchiveTasksMutation.ts
index f95935aa9ec..42c871314f0 100644
--- a/packages/client/mutations/BatchArchiveTasksMutation.ts
+++ b/packages/client/mutations/BatchArchiveTasksMutation.ts
@@ -3,8 +3,8 @@ import {commitMutation} from 'react-relay'
import {BatchArchiveTasksMutation_tasks$data} from '~/__generated__/BatchArchiveTasksMutation_tasks.graphql'
import {Task as ITask} from '../../server/postgres/types/index.d'
import {BatchArchiveTasksMutation as TBatchArchiveTasksMutation} from '../__generated__/BatchArchiveTasksMutation.graphql'
+import {getTagsFromTipTapTask} from '../shared/tiptap/getTagsFromTipTapTask'
import {SharedUpdater, StandardMutation} from '../types/relayMutations'
-import getTagsFromEntityMap from '../utils/draftjs/getTagsFromEntityMap'
import handleRemoveTasks from './handlers/handleRemoveTasks'
import handleUpsertTasks from './handlers/handleUpsertTasks'
@@ -38,8 +38,8 @@ export const batchArchiveTasksTaskUpdater: SharedUpdater {
const content = archivedTask.getValue('content')
- const {entityMap} = JSON.parse(content)
- const nextTags = getTagsFromEntityMap(entityMap)
+ const doc = JSON.parse(content)
+ const nextTags = getTagsFromTipTapTask(doc)
archivedTask.setValue(nextTags, 'tags')
handleUpsertTasks(archivedTask as any, store)
handleRemoveTasks(archivedTask as any, store)
diff --git a/packages/client/mutations/CreateTaskIntegrationMutation.ts b/packages/client/mutations/CreateTaskIntegrationMutation.ts
index d87e8d15b2f..30fbd710a71 100644
--- a/packages/client/mutations/CreateTaskIntegrationMutation.ts
+++ b/packages/client/mutations/CreateTaskIntegrationMutation.ts
@@ -1,12 +1,13 @@
+import {generateHTML} from '@tiptap/core'
import graphql from 'babel-plugin-relay/macro'
-import {stateToHTML} from 'draft-js-export-html'
import {commitMutation} from 'react-relay'
import {RecordSourceSelectorProxy} from 'relay-runtime'
import JiraProjectId from '~/shared/gqlIds/JiraProjectId'
import {CreateTaskIntegrationMutation as TCreateTaskIntegrationMutation} from '../__generated__/CreateTaskIntegrationMutation.graphql'
+import {serverTipTapExtensions} from '../shared/tiptap/serverTipTapExtensions'
+import {splitTipTapContent} from '../shared/tiptap/splitTipTapContent'
import {StandardMutation} from '../types/relayMutations'
import SendClientSideEvent from '../utils/SendClientSideEvent'
-import splitDraftContent from '../utils/draftjs/splitDraftContent'
import getMeetingPathParams from '../utils/meetings/getMeetingPathParams'
import createProxyRecord from '../utils/relay/createProxyRecord'
@@ -99,8 +100,8 @@ const jiraTaskIntegrationOptimisticUpdater = (
if (!task) return
const contentStr = task.getValue('content') as string
if (!contentStr) return
- const {title: summary, contentState} = splitDraftContent(contentStr)
- const descriptionHTML = stateToHTML(contentState)
+ const {title: summary, bodyContent} = splitTipTapContent(JSON.parse(contentStr))
+ const descriptionHTML = generateHTML(bodyContent, serverTipTapExtensions)
const optimisticIntegration = {
summary,
descriptionHTML,
@@ -127,8 +128,8 @@ const githubTaskIntegrationOptimisitcUpdater = (
})
const contentStr = task.getValue('content') as string
if (!contentStr) return
- const {title, contentState} = splitDraftContent(contentStr)
- const bodyHTML = stateToHTML(contentState)
+ const {title, bodyContent} = splitTipTapContent(JSON.parse(contentStr))
+ const bodyHTML = generateHTML(bodyContent, serverTipTapExtensions)
const optimisticIntegration = {
title,
bodyHTML,
@@ -153,8 +154,8 @@ const gitlabTaskIntegrationOptimisitcUpdater = (
})
const contentStr = task.getValue('content') as string
if (!contentStr) return
- const {title, contentState} = splitDraftContent(contentStr)
- const descriptionHtml = stateToHTML(contentState)
+ const {title, bodyContent} = splitTipTapContent(JSON.parse(contentStr))
+ const descriptionHtml = generateHTML(bodyContent, serverTipTapExtensions)
const webPath = `${fullPath}/-/issues/0`
const optimisticIntegration = {
title,
@@ -179,8 +180,8 @@ const jiraServerTaskIntegrationOptimisticUpdater = (
if (!task) return
const contentStr = task.getValue('content') as string
if (!contentStr) return
- const {title: summary, contentState} = splitDraftContent(contentStr)
- const descriptionHTML = stateToHTML(contentState)
+ const {title: summary, bodyContent} = splitTipTapContent(JSON.parse(contentStr))
+ const descriptionHTML = generateHTML(bodyContent, serverTipTapExtensions)
const optimisticIntegration = {
summary,
descriptionHTML,
@@ -201,8 +202,8 @@ const azureTaskIntegrationOptimisitcUpdater = (
if (!task) return
const contentStr = task.getValue('content') as string
if (!contentStr) return
- const {title, contentState} = splitDraftContent(contentStr)
- const descriptionHTML = stateToHTML(contentState)
+ const {title, bodyContent} = splitTipTapContent(JSON.parse(contentStr))
+ const descriptionHTML = generateHTML(bodyContent, serverTipTapExtensions)
const optimisticIntegration = {
id: '?',
title,
diff --git a/packages/client/mutations/CreateTaskMutation.ts b/packages/client/mutations/CreateTaskMutation.ts
index 0ff880cdf4d..4e238bd6647 100644
--- a/packages/client/mutations/CreateTaskMutation.ts
+++ b/packages/client/mutations/CreateTaskMutation.ts
@@ -1,13 +1,14 @@
+import {generateJSON, generateText, JSONContent} from '@tiptap/core'
import graphql from 'babel-plugin-relay/macro'
import {commitMutation} from 'react-relay'
import AzureDevOpsProjectId from '~/shared/gqlIds/AzureDevOpsProjectId'
-import extractTextFromDraftString from '~/utils/draftjs/extractTextFromDraftString'
import Atmosphere from '../Atmosphere'
import {CreateTaskMutation as TCreateTaskMutation} from '../__generated__/CreateTaskMutation.graphql'
import {CreateTaskMutation_notification$data} from '../__generated__/CreateTaskMutation_notification.graphql'
import {CreateTaskMutation_task$data} from '../__generated__/CreateTaskMutation_task.graphql'
import GitHubIssueId from '../shared/gqlIds/GitHubIssueId'
import JiraProjectId from '../shared/gqlIds/JiraProjectId'
+import {serverTipTapExtensions} from '../shared/tiptap/serverTipTapExtensions'
import {
OnNextHandler,
OnNextHistoryContext,
@@ -15,7 +16,6 @@ import {
SharedUpdater,
StandardMutation
} from '../types/relayMutations'
-import makeEmptyStr from '../utils/draftjs/makeEmptyStr'
import clientTempId from '../utils/relay/clientTempId'
import createProxyRecord from '../utils/relay/createProxyRecord'
import getOptimisticTaskEditor from '../utils/relay/getOptimisticTaskEditor'
@@ -108,9 +108,11 @@ export const createTaskTaskUpdater: SharedUpdater
if (!task) return
const taskId = task.getValue('id')
const content = task.getValue('content')
- const rawContent = JSON.parse(content)
- const {blocks} = rawContent
- const isEditing = blocks.length === 0 || (blocks.length === 1 && blocks[0].text === '')
+ const rawContent = JSON.parse(content) as JSONContent
+ const isEditing =
+ !rawContent.content ||
+ rawContent.content.length === 0 ||
+ (rawContent.content.length === 1 && rawContent.content[0]?.text === '')
const editorPayload = getOptimisticTaskEditor(store, taskId, isEditing)
handleEditTask(editorPayload, store)
handleUpsertTasks(task, store)
@@ -161,7 +163,7 @@ const CreateTaskMutation: StandardMutation', serverTipTapExtensions)),
title: plaintextContent,
plaintextContent
}
diff --git a/packages/client/mutations/UpdatePokerScopeMutation.ts b/packages/client/mutations/UpdatePokerScopeMutation.ts
index 6f641fdcaa3..96473c3d08c 100644
--- a/packages/client/mutations/UpdatePokerScopeMutation.ts
+++ b/packages/client/mutations/UpdatePokerScopeMutation.ts
@@ -1,19 +1,20 @@
import graphql from 'babel-plugin-relay/macro'
-import {stateToHTML} from 'draft-js-export-html'
import {commitMutation} from 'react-relay'
import GitLabIssueId from '~/shared/gqlIds/GitLabIssueId'
//import AzureDevOpsIssueId from '~/shared/gqlIds/AzureDevOpsIssueId'
+import {generateHTML} from '@tiptap/core'
import {UpdatePokerScopeMutation as TUpdatePokerScopeMutation} from '../__generated__/UpdatePokerScopeMutation.graphql'
import GitHubIssueId from '../shared/gqlIds/GitHubIssueId'
import JiraIssueId from '../shared/gqlIds/JiraIssueId'
+import {convertTipTapTaskContent} from '../shared/tiptap/convertTipTapTaskContent'
+import {serverTipTapExtensions} from '../shared/tiptap/serverTipTapExtensions'
+import {splitTipTapContent} from '../shared/tiptap/splitTipTapContent'
import {PALETTE} from '../styles/paletteV3'
import {BaseLocalHandlers, StandardMutation} from '../types/relayMutations'
-import SendClientSideEvent from '../utils/SendClientSideEvent'
-import convertToTaskContent from '../utils/draftjs/convertToTaskContent'
-import splitDraftContent from '../utils/draftjs/splitDraftContent'
import getSearchQueryFromMeeting from '../utils/getSearchQueryFromMeeting'
import clientTempId from '../utils/relay/clientTempId'
import createProxyRecord from '../utils/relay/createProxyRecord'
+import SendClientSideEvent from '../utils/SendClientSideEvent'
graphql`
fragment UpdatePokerScopeMutation_meeting on UpdatePokerScopeSuccess {
@@ -169,8 +170,8 @@ const UpdatePokerScopeMutation: StandardMutation {
+ content.content!.push({
+ type: 'paragraph',
+ content: [
+ {
+ type: 'taskTag',
+ attrs: {
+ id: tag,
+ label: null
+ }
+ }
+ ]
+ })
+ return content
+}
+
+export default addTagToTask
diff --git a/packages/client/shared/tiptap/convertTipTapTaskContent.ts b/packages/client/shared/tiptap/convertTipTapTaskContent.ts
new file mode 100644
index 00000000000..b93c0f01e13
--- /dev/null
+++ b/packages/client/shared/tiptap/convertTipTapTaskContent.ts
@@ -0,0 +1,13 @@
+import {generateJSON} from '@tiptap/html'
+import {TaskTag} from 'parabol-server/postgres/types'
+import {serverTipTapExtensions} from '~/shared/tiptap/serverTipTapExtensions'
+
+export const convertTipTapTaskContent = (text: string, tags?: TaskTag[]) => {
+ let body = `${text}
`
+ if (tags) {
+ tags.forEach((tag) => {
+ body = body + `#${tag}`
+ })
+ }
+ return JSON.stringify(generateJSON(body, serverTipTapExtensions))
+}
diff --git a/packages/client/shared/tiptap/getAllNodesAttributesByType.ts b/packages/client/shared/tiptap/getAllNodesAttributesByType.ts
new file mode 100644
index 00000000000..8bf272f8383
--- /dev/null
+++ b/packages/client/shared/tiptap/getAllNodesAttributesByType.ts
@@ -0,0 +1,31 @@
+import {JSONContent} from '@tiptap/core'
+import {Attrs} from '@tiptap/pm/model'
+
+export const getAllNodesAttributesByType = (
+ doc: JSONContent,
+ nodeType: string
+) => {
+ let mentions: T[] = []
+
+ // Handle arrays
+ if (Array.isArray(doc)) {
+ for (const item of doc) {
+ mentions = mentions.concat(getAllNodesAttributesByType(item, nodeType))
+ }
+ }
+
+ // Handle objects
+ if (doc && typeof doc === 'object') {
+ // Check if current object is a mention
+ if (doc.type === nodeType) {
+ mentions.push(doc.attrs as T)
+ }
+
+ // Recursively search content if it exists
+ if (doc.content) {
+ mentions = mentions.concat(getAllNodesAttributesByType(doc.content, nodeType))
+ }
+ }
+
+ return mentions
+}
diff --git a/packages/client/shared/tiptap/getTagsFromTipTapTask.ts b/packages/client/shared/tiptap/getTagsFromTipTapTask.ts
new file mode 100644
index 00000000000..5336c4207f6
--- /dev/null
+++ b/packages/client/shared/tiptap/getTagsFromTipTapTask.ts
@@ -0,0 +1,8 @@
+import {JSONContent} from '@tiptap/core'
+import {TaskTag} from '../../../server/postgres/types'
+import {getAllNodesAttributesByType} from './getAllNodesAttributesByType'
+
+export const getTagsFromTipTapTask = (content: JSONContent) => {
+ const tagAttributes = getAllNodesAttributesByType<{id: TaskTag}>(content, 'taskTag')
+ return [...new Set(tagAttributes.map(({id}) => id))]
+}
diff --git a/packages/client/shared/tiptap/isDraftJSContent.ts b/packages/client/shared/tiptap/isDraftJSContent.ts
new file mode 100644
index 00000000000..f135181ddd5
--- /dev/null
+++ b/packages/client/shared/tiptap/isDraftJSContent.ts
@@ -0,0 +1,8 @@
+import {JSONContent} from '@tiptap/core'
+import {RawDraftContentState} from 'draft-js'
+
+export const isDraftJSContent = (
+ content: JSONContent | RawDraftContentState
+): content is RawDraftContentState => {
+ return 'blocks' in content
+}
diff --git a/packages/client/shared/tiptap/removeMentionKeepText.ts b/packages/client/shared/tiptap/removeMentionKeepText.ts
new file mode 100644
index 00000000000..066cb2f08aa
--- /dev/null
+++ b/packages/client/shared/tiptap/removeMentionKeepText.ts
@@ -0,0 +1,29 @@
+import {JSONContent} from '@tiptap/core'
+
+export const removeMentionKeepText = (
+ node: JSONContent,
+ eqFn: (userId: string) => boolean
+): JSONContent => {
+ // Base case: if the node is a 'mention', replace it
+ if (node.type === 'mention' && node.attrs && eqFn(node.attrs.id)) {
+ return {
+ type: 'span',
+ content: [
+ {
+ text: node.attrs.label,
+ type: 'text'
+ }
+ ]
+ }
+ }
+
+ // If the node has content, recursively process each child node
+ if (Array.isArray(node.content)) {
+ return {
+ ...node,
+ content: node.content.map((obj) => removeMentionKeepText(obj, eqFn))
+ }
+ }
+ // If the node is not a 'mention' and has no content, return it as is
+ return node
+}
diff --git a/packages/client/shared/tiptap/removeNodeByType.ts b/packages/client/shared/tiptap/removeNodeByType.ts
new file mode 100644
index 00000000000..220cdfe7067
--- /dev/null
+++ b/packages/client/shared/tiptap/removeNodeByType.ts
@@ -0,0 +1,21 @@
+import {JSONContent} from '@tiptap/core'
+
+export const removeNodeByType = (json: JSONContent, nodeTypeToRemove: string): JSONContent => {
+ if (Array.isArray(json)) {
+ return json
+ .map((node) => removeNodeByType(node, nodeTypeToRemove))
+ .filter((item) => item.type !== nodeTypeToRemove)
+ }
+
+ if (json && typeof json === 'object') {
+ const newObj = {...json}
+
+ // Recursively process content if it exists
+ if (newObj.content) {
+ newObj.content = removeNodeByType(newObj.content, nodeTypeToRemove) as JSONContent[]
+ }
+ return newObj
+ }
+ // Return primitive values as-is
+ return json
+}
diff --git a/packages/client/shared/tiptap/serverTipTapExtensions.ts b/packages/client/shared/tiptap/serverTipTapExtensions.ts
new file mode 100644
index 00000000000..8c019d992b0
--- /dev/null
+++ b/packages/client/shared/tiptap/serverTipTapExtensions.ts
@@ -0,0 +1,30 @@
+import {mergeAttributes} from '@tiptap/core'
+import BaseLink from '@tiptap/extension-link'
+import Mention, {MentionNodeAttrs, MentionOptions} from '@tiptap/extension-mention'
+import StarterKit from '@tiptap/starter-kit'
+import {LoomExtension} from '../../components/promptResponse/loomExtension'
+import {tiptapTagConfig} from '../../utils/tiptapTagConfig'
+
+export const mentionConfig: Partial> = {
+ renderText({node}) {
+ return node.attrs.label
+ },
+ renderHTML({options, node}) {
+ return ['span', options.HTMLAttributes, `${node.attrs.label ?? node.attrs.id}`]
+ }
+}
+export const serverTipTapExtensions = [
+ StarterKit,
+ LoomExtension,
+ Mention.configure(mentionConfig),
+ Mention.extend({name: 'taskTag'}).configure(tiptapTagConfig),
+ BaseLink.extend({
+ parseHTML() {
+ return [{tag: 'a[href]:not([data-type="button"]):not([href *= "javascript:" i])'}]
+ },
+
+ renderHTML({HTMLAttributes}) {
+ return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {class: 'link'}), 0]
+ }
+ })
+]
diff --git a/packages/client/shared/tiptap/splitTipTapContent.ts b/packages/client/shared/tiptap/splitTipTapContent.ts
new file mode 100644
index 00000000000..fd57bac164d
--- /dev/null
+++ b/packages/client/shared/tiptap/splitTipTapContent.ts
@@ -0,0 +1,17 @@
+import {generateText, JSONContent} from '@tiptap/core'
+import {serverTipTapExtensions} from './serverTipTapExtensions'
+
+export const splitTipTapContent = (doc: JSONContent, maxLength = 256) => {
+ const [firstBlock, ...bodyBlocks] = doc.content!
+ const fullTitle = generateText({...doc, content: [firstBlock!]}, serverTipTapExtensions)
+ if (fullTitle.length < maxLength) {
+ const bodyText = generateText({...doc, content: bodyBlocks}, serverTipTapExtensions)
+ const content = bodyText.trim().length > 0 ? bodyBlocks : doc.content!
+ return {title: fullTitle, bodyContent: {...doc, content}}
+ }
+ return {
+ title: fullTitle.slice(0, maxLength),
+ // repeat the full title in the body since we had to truncate it
+ bodyContent: doc
+ }
+}
diff --git a/packages/client/types/modules.d.ts b/packages/client/types/modules.d.ts
index 33ce7803049..dbb21cd9384 100644
--- a/packages/client/types/modules.d.ts
+++ b/packages/client/types/modules.d.ts
@@ -14,7 +14,6 @@ declare module 'string-score'
declare module 'babel-plugin-relay/macro' {
export {graphql as default} from 'react-relay'
}
-declare module 'unicode-substring'
declare module 'react-textarea-autosize'
declare module 'react-copy-to-clipboard'
declare module 'tayden-clusterfck'
diff --git a/packages/client/utils/draftjs/addContentTag.ts b/packages/client/utils/draftjs/addContentTag.ts
deleted file mode 100644
index 24d113c8823..00000000000
--- a/packages/client/utils/draftjs/addContentTag.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import {ContentState, convertToRaw} from 'draft-js'
-import {AreaEnum} from '~/__generated__/UpdateTaskMutation.graphql'
-import Atmosphere from '../../Atmosphere'
-import UpdateTaskMutation from '../../mutations/UpdateTaskMutation'
-import addTagToTask from './addTagToTask'
-
-const addContentTag = (
- tag: string,
- atmosphere: Atmosphere,
- taskId: string,
- contentState: ContentState,
- area: AreaEnum
-) => {
- const newContent = addTagToTask(contentState, tag)
- const rawContentStr = JSON.stringify(convertToRaw(newContent))
- const updatedTask = {
- id: taskId,
- content: rawContentStr
- }
- UpdateTaskMutation(atmosphere, {updatedTask, area}, {})
-}
-
-export default addContentTag
diff --git a/packages/client/utils/draftjs/addTagToTask.ts b/packages/client/utils/draftjs/addTagToTask.ts
deleted file mode 100644
index 0ed854b265d..00000000000
--- a/packages/client/utils/draftjs/addTagToTask.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import {ContentState, Modifier, SelectionState} from 'draft-js'
-
-const addTagToTask = (contentState: ContentState, tag: string) => {
- const value = tag.slice(1)
- const lastBlock = contentState.getLastBlock()
- const selectionState = new SelectionState({
- anchorKey: lastBlock.getKey(),
- anchorOffset: lastBlock.getLength(),
- focusKey: lastBlock.getKey(),
- focusOffset: lastBlock.getLength(),
- isBackward: false,
- hasFocus: false
- })
- const contentStateWithNewBlock = Modifier.splitBlock(contentState, selectionState)
- const newBlock = contentStateWithNewBlock.getLastBlock()
- const lastSelection = selectionState.merge({
- anchorKey: newBlock.getKey(),
- focusKey: newBlock.getKey(),
- anchorOffset: 0,
- focusOffset: 0
- })
-
- const contentStateWithEntity = contentStateWithNewBlock.createEntity('TAG', 'IMMUTABLE', {value})
- const entityKey = contentStateWithEntity.getLastCreatedEntityKey()
-
- return Modifier.replaceText(contentStateWithEntity, lastSelection, tag, undefined, entityKey)
-}
-
-export default addTagToTask
diff --git a/packages/client/utils/draftjs/getTagsFromEntityMap.ts b/packages/client/utils/draftjs/getTagsFromEntityMap.ts
deleted file mode 100644
index 7afcb1fd4e7..00000000000
--- a/packages/client/utils/draftjs/getTagsFromEntityMap.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import {RawDraftContentState} from 'draft-js'
-
-const getTagsFromEntityMap = (entityMap: RawDraftContentState['entityMap']) => {
- const tags = new Set()
- Object.values(entityMap).forEach((entity) => {
- if (entity.type === 'TAG') {
- tags.add(entity.data.value)
- }
- })
- return Array.from(tags)
-}
-
-export default getTagsFromEntityMap
diff --git a/packages/client/utils/draftjs/removeContentTag.ts b/packages/client/utils/draftjs/removeContentTag.ts
deleted file mode 100644
index 6856dda0214..00000000000
--- a/packages/client/utils/draftjs/removeContentTag.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import {AreaEnum} from '~/__generated__/UpdateTaskMutation.graphql'
-import Atmosphere from '../../Atmosphere'
-import UpdateTaskMutation from '../../mutations/UpdateTaskMutation'
-import removeRangesForEntity from './removeRangesForEntity'
-
-const removeContentTag = (
- tagValue: string,
- atmosphere: Atmosphere,
- taskId: string,
- content: string,
- area: AreaEnum
-) => {
- const eqFn = (data: {value: any}) => data.value === tagValue
- const nextContent = removeRangesForEntity(content, 'TAG', eqFn)
- if (!nextContent) return
- const updatedTask = {
- id: taskId,
- content: nextContent
- }
- UpdateTaskMutation(atmosphere, {updatedTask, area}, {})
-}
-
-export default removeContentTag
diff --git a/packages/client/utils/draftjs/removeEntityKeepText.ts b/packages/client/utils/draftjs/removeEntityKeepText.ts
deleted file mode 100644
index d9c7a0ea0fe..00000000000
--- a/packages/client/utils/draftjs/removeEntityKeepText.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import {
- RawDraftContentBlock,
- RawDraftContentState,
- RawDraftEntity,
- RawDraftEntityRange
-} from 'draft-js'
-
-const updateBlockEntityRanges = (blocks: RawDraftContentBlock[], updatedKeyMap: any) => {
- const nextBlocks = [] as RawDraftContentBlock[]
- blocks.forEach((block) => {
- const {entityRanges} = block
- const nextEntityRanges = [] as RawDraftEntityRange[]
- entityRanges.forEach((entityRange) => {
- const nextKey = updatedKeyMap[entityRange.key]
- if (nextKey !== null) {
- nextEntityRanges.push({...entityRange, key: nextKey})
- }
- })
- nextBlocks.push({...block, entityRanges: nextEntityRanges})
- })
- return nextBlocks
-}
-
-/*
- * Removes the underlying entity but keeps the text in place
- * Useful for e.g. removing a mention but keeping the name
- */
-const removeEntityKeepText = (rawContent: RawDraftContentState, eqFn: (entity: any) => boolean) => {
- const {blocks, entityMap} = rawContent
- const nextEntityMap = {} as RawDraftContentState['entityMap']
- // oldKey: newKey. null is a remove sentinel
- const updatedKeyMap = {} as {[key: string]: string | null}
- const removedEntities = [] as RawDraftEntity[]
- // I'm not really sure how draft-js assigns keys, so I just reuse what they give me FIFO
- const releasedKeys = [] as string[]
-
- for (const [key, entity] of Object.entries(entityMap)) {
- if (eqFn(entity)) {
- removedEntities.push(entity)
- updatedKeyMap[key] = null
- releasedKeys.push(key)
- } else {
- const nextKey = releasedKeys.length ? releasedKeys.shift()! : key
- nextEntityMap[nextKey] = entity
- updatedKeyMap[key] = nextKey
- }
- }
-
- return {
- rawContent:
- removedEntities.length === 0
- ? rawContent
- : {
- blocks: updateBlockEntityRanges(blocks, updatedKeyMap),
- entityMap: nextEntityMap
- },
- removedEntities
- }
-}
-
-export default removeEntityKeepText
diff --git a/packages/client/utils/draftjs/removeRangesForEntity.ts b/packages/client/utils/draftjs/removeRangesForEntity.ts
deleted file mode 100644
index 2e04966a8da..00000000000
--- a/packages/client/utils/draftjs/removeRangesForEntity.ts
+++ /dev/null
@@ -1,89 +0,0 @@
-import {
- ContentState,
- convertFromRaw,
- convertToRaw,
- EditorState,
- Entity,
- Modifier,
- RawDraftContentState,
- RawDraftEntityRange,
- SelectionState
-} from 'draft-js'
-import unicodeSubstring from 'unicode-substring'
-
-const getUTF16Range = (text: string, range: RawDraftEntityRange) => {
- const offset = unicodeSubstring(text, 0, range.offset).length
- return {
- key: range.key,
- offset,
- length: offset + unicodeSubstring(text, offset, range.length).length
- }
-}
-
-const getEntities = (
- entityMap: RawDraftContentState['entityMap'],
- entityType: string,
- eqFn: (entityData: any) => boolean
-) => {
- const entities = [] as string[]
- for (const [key, entity] of Object.entries(entityMap)) {
- if (entity.type === entityType && eqFn(entity.data)) {
- entities.push(key)
- }
- }
- return entities
-}
-
-const getRemovalRanges = (
- entities: Entity[],
- entityRanges: RawDraftEntityRange[],
- text: string
-) => {
- const removalRanges = [] as {start: any; end: any}[]
- entityRanges.forEach((utf8Range) => {
- const entityKey = String(utf8Range.key)
- if (entities.includes(entityKey)) {
- const {offset, length} = getUTF16Range(text, utf8Range)
- const entityEnd = offset + length
- const end = offset === 0 && text[entityEnd] === ' ' ? entityEnd + 1 : entityEnd
- const start = text[offset - 1] === ' ' ? offset - 1 : offset
- removalRanges.push({start, end})
- }
- })
- removalRanges.sort((a, b) => (a.end < b.end ? 1 : -1))
- return removalRanges
-}
-
-const removeRangesForEntity = (content: string, entityType: string, eqFn: any) => {
- const rawContent = JSON.parse(content)
- const {blocks, entityMap} = rawContent
- const entities = getEntities(entityMap, entityType, eqFn)
- // it's an arduous task to update the next entities after removing 1, so use the removeRange helper
- const editorState = EditorState.createWithContent(convertFromRaw(rawContent))
- let contentState = editorState.getCurrentContent()
- const selectionState = editorState.getSelection()
- for (let i = blocks.length - 1; i >= 0; i--) {
- const block = blocks[i]
- const {entityRanges, key: blockKey, text} = block
- const removalRanges = getRemovalRanges(entities, entityRanges, text)
- removalRanges.forEach((range) => {
- const selectionToRemove = selectionState.merge({
- anchorKey: blockKey,
- focusKey: blockKey,
- anchorOffset: range.start,
- focusOffset: range.end
- }) as SelectionState
- contentState = Modifier.removeRange(contentState, selectionToRemove, 'backward')
- })
- if (contentState.getBlockForKey(blockKey).getText() === '') {
- contentState = contentState.merge({
- blockMap: contentState.getBlockMap().delete(blockKey)
- }) as ContentState
- }
- }
- return contentState === editorState.getCurrentContent()
- ? null
- : JSON.stringify(convertToRaw(contentState))
-}
-
-export default removeRangesForEntity
diff --git a/packages/client/utils/relay/isTempId.ts b/packages/client/utils/relay/isTempId.ts
index d5d6ae3279d..cee8566ca73 100644
--- a/packages/client/utils/relay/isTempId.ts
+++ b/packages/client/utils/relay/isTempId.ts
@@ -1,3 +1,3 @@
-const isTempId = (id: any) => (id ? id.endsWith('-tmp') : false)
+const isTempId = (id: string | null | undefined) => id?.endsWith('-tmp') ?? false
export default isTempId
diff --git a/packages/client/utils/tiptapMentionConfig.ts b/packages/client/utils/tiptapMentionConfig.ts
index bb807477792..74c79d424a7 100644
--- a/packages/client/utils/tiptapMentionConfig.ts
+++ b/packages/client/utils/tiptapMentionConfig.ts
@@ -6,6 +6,7 @@ import tippy, {Instance, Props} from 'tippy.js'
import {tiptapMentionConfigQuery} from '../__generated__/tiptapMentionConfigQuery.graphql'
import Atmosphere from '../Atmosphere'
import MentionDropdown from '../components/MentionDropdown'
+import {mentionConfig} from '../shared/tiptap/serverTipTapExtensions'
const queryNode = graphql`
query tiptapMentionConfigQuery($teamId: ID!) {
@@ -13,6 +14,7 @@ const queryNode = graphql`
team(teamId: $teamId) {
teamMembers {
id
+ userId
picture
preferredName
}
@@ -25,10 +27,7 @@ export const tiptapMentionConfig = (
atmosphere: Atmosphere,
teamId: string
): Partial> => ({
- // renderText does not fire, bug in TipTap? Fallback to using more verbose renderHTML
- renderHTML({options, node}) {
- return ['span', options.HTMLAttributes, `${node.attrs.label ?? node.attrs.id}`]
- },
+ ...mentionConfig,
suggestion: {
// some users have first & last name
allowSpaces: true,
diff --git a/packages/integration-tests/tests/retrospective-demo/step1-reflect.test.ts b/packages/integration-tests/tests/retrospective-demo/step1-reflect.test.ts
index 9e03b3d8e92..92c0687bcd4 100644
--- a/packages/integration-tests/tests/retrospective-demo/step1-reflect.test.ts
+++ b/packages/integration-tests/tests/retrospective-demo/step1-reflect.test.ts
@@ -48,7 +48,7 @@ test.describe('retrospective-demo / reflect page', () => {
const continueTextbox = '[data-cy=reflection-column-Continue] [role=textbox]'
await page.click(continueTextbox)
- await page.type(continueTextbox, 'Continue doing this')
+ await page.fill(continueTextbox, 'Continue doing this')
await page.press(continueTextbox, 'Enter')
await expect(
@@ -61,7 +61,7 @@ test.describe('retrospective-demo / reflect page', () => {
const startTextbox = '[data-cy=reflection-column-Start] [role=textbox]'
await page.click(startTextbox)
- await page.type(startTextbox, 'Start doing this')
+ await page.fill(startTextbox, 'Start doing this')
await page.press(startTextbox, 'Enter')
await expect(
@@ -233,9 +233,7 @@ test.describe('retrospective-demo / reflect page', () => {
)
).toBeVisible()
- await expect(
- page.locator('button :text("1 / 2 Ready")')
- ).toBeVisible()
+ await expect(page.locator('button :text("1 / 2 Ready")')).toBeVisible()
await expect(
page.locator(
diff --git a/packages/integration-tests/tests/retrospective-demo/step2-group.test.ts b/packages/integration-tests/tests/retrospective-demo/step2-group.test.ts
index 401cd3277d3..1a7f6523d60 100644
--- a/packages/integration-tests/tests/retrospective-demo/step2-group.test.ts
+++ b/packages/integration-tests/tests/retrospective-demo/step2-group.test.ts
@@ -93,7 +93,7 @@ test.describe('retrospective-demo / group page', () => {
const startTextbox = '[data-cy=reflection-column-Start] [role=textbox]'
await page.click(startTextbox)
- await page.type(startTextbox, 'Documenting things in Notion')
+ await page.fill(startTextbox, 'Documenting things in Notion')
await page.press(startTextbox, 'Enter')
await expect(
page.locator('[data-cy="reflection-column-Start"] :text("Documenting things in Notion")')
@@ -101,7 +101,7 @@ test.describe('retrospective-demo / group page', () => {
const stopTextbox = '[data-cy=reflection-column-Stop] [role=textbox]'
await page.click(stopTextbox)
- await page.type(stopTextbox, 'Making decisions in one-on-one meetings')
+ await page.fill(stopTextbox, 'Making decisions in one-on-one meetings')
await page.press(stopTextbox, 'Enter')
await expect(
page.locator(
diff --git a/packages/integration-tests/tests/retrospective-demo/step4-discuss.test.ts b/packages/integration-tests/tests/retrospective-demo/step4-discuss.test.ts
index 2b8ef42d5f2..c6fdc078694 100644
--- a/packages/integration-tests/tests/retrospective-demo/step4-discuss.test.ts
+++ b/packages/integration-tests/tests/retrospective-demo/step4-discuss.test.ts
@@ -142,7 +142,7 @@ test.describe('retrospective-demo / discuss page', () => {
}
for await (const task of tasks || []) {
- await expect(page.locator(`[data-cy=task-wrapper] :text('${task}')`)).toBeVisible({
+ await expect(page.locator(`:text('${task}')`)).toBeVisible({
timeout: 30_000
})
}
diff --git a/packages/server/graphql/mutations/changeTaskTeam.ts b/packages/server/graphql/mutations/changeTaskTeam.ts
index 0fc63d0302a..b6491536ab9 100644
--- a/packages/server/graphql/mutations/changeTaskTeam.ts
+++ b/packages/server/graphql/mutations/changeTaskTeam.ts
@@ -1,12 +1,13 @@
import {GraphQLID, GraphQLNonNull} from 'graphql'
import {SubscriptionChannel} from 'parabol-client/types/constEnums'
-import removeEntityKeepText from 'parabol-client/utils/draftjs/removeEntityKeepText'
+import {removeMentionKeepText} from '../../../client/shared/tiptap/removeMentionKeepText'
import getKysely from '../../postgres/getKysely'
import {AtlassianAuth} from '../../postgres/queries/getAtlassianAuthByUserIdTeamId'
import {GitHubAuth} from '../../postgres/queries/getGitHubAuthByUserIdTeamId'
import upsertAtlassianAuths from '../../postgres/queries/upsertAtlassianAuths'
import upsertGitHubAuth from '../../postgres/queries/upsertGitHubAuth'
import {getUserId, isTeamMember} from '../../utils/authorization'
+import {convertToTipTap} from '../../utils/convertToTipTap'
import publish from '../../utils/publish'
import standardError from '../../utils/standardError'
import {GQLContext} from '../graphql'
@@ -126,11 +127,9 @@ export default {
const userIdsOnlyOnOldTeam = oldTeamUserIds.filter((oldTeamUserId) => {
return !newTeamUserIds.find((newTeamUserId) => newTeamUserId === oldTeamUserId)
})
- const rawContent = JSON.parse(content)
- const eqFn = (entity: {type: string; data: {userId?: string}}) =>
- entity.type === 'MENTION' &&
- Boolean(userIdsOnlyOnOldTeam.find((userId) => userId === entity.data.userId))
- const {rawContent: nextRawContent} = removeEntityKeepText(rawContent, eqFn)
+ const rawContent = convertToTipTap(content)
+ const eqFn = (userId: string) => userIdsOnlyOnOldTeam.includes(userId)
+ const {rawContent: nextRawContent} = removeMentionKeepText(rawContent, eqFn)
// If there is a task with the same integration hash in the new team, then delete it first.
// This is done so there are no duplicates and also solves the issue of the conflicting task being
diff --git a/packages/server/graphql/mutations/createTask.ts b/packages/server/graphql/mutations/createTask.ts
index 0299b2075ea..ae62f285935 100644
--- a/packages/server/graphql/mutations/createTask.ts
+++ b/packages/server/graphql/mutations/createTask.ts
@@ -1,21 +1,22 @@
+import {generateText} from '@tiptap/core'
import {GraphQLNonNull, GraphQLObjectType, GraphQLResolveInfo} from 'graphql'
import {Insertable} from 'kysely'
import {SubscriptionChannel} from 'parabol-client/types/constEnums'
-import getTypeFromEntityMap from 'parabol-client/utils/draftjs/getTypeFromEntityMap'
import toTeamMemberId from 'parabol-client/utils/relay/toTeamMemberId'
import MeetingMemberId from '../../../client/shared/gqlIds/MeetingMemberId'
+import {getAllNodesAttributesByType} from '../../../client/shared/tiptap/getAllNodesAttributesByType'
+import {getTagsFromTipTapTask} from '../../../client/shared/tiptap/getTagsFromTipTapTask'
+import {serverTipTapExtensions} from '../../../client/shared/tiptap/serverTipTapExtensions'
import dndNoise from '../../../client/utils/dndNoise'
-import extractTextFromDraftString from '../../../client/utils/draftjs/extractTextFromDraftString'
-import getTagsFromEntityMap from '../../../client/utils/draftjs/getTagsFromEntityMap'
-import normalizeRawDraftJS from '../../../client/validation/normalizeRawDraftJS'
import generateUID from '../../generateUID'
import updatePrevUsedRepoIntegrationsCache from '../../integrations/updatePrevUsedRepoIntegrationsCache'
import getKysely from '../../postgres/getKysely'
-import {Task, TaskTag} from '../../postgres/types/index.d'
+import {Task} from '../../postgres/types/index.d'
import {Notification} from '../../postgres/types/pg'
import {TaskServiceEnum} from '../../postgres/types/TaskIntegration'
import {analytics} from '../../utils/analytics/analytics'
import {getUserId, isTeamMember} from '../../utils/authorization'
+import {convertToTipTap} from '../../utils/convertToTipTap'
import publish, {SubOptions} from '../../utils/publish'
import standardError from '../../utils/standardError'
import {DataLoaderWorker, GQLContext} from '../graphql'
@@ -88,16 +89,17 @@ const handleAddTaskNotifications = async (
})
}
- const {entityMap} = JSON.parse(content)
- getTypeFromEntityMap('MENTION', entityMap)
+ const jsonContent = JSON.parse(content)
+ getAllNodesAttributesByType<{id: string; label: string}>(jsonContent, 'mention')
.filter(
- (mention) => mention !== viewerId && mention !== userId && !usersIdsToIgnore.includes(mention)
+ (mention) =>
+ mention.id !== viewerId && mention.id !== userId && !usersIdsToIgnore.includes(mention.id)
)
- .forEach((mentioneeUserId) => {
+ .forEach((mentionee) => {
notificationsToAdd.push({
id: generateUID(),
type: 'TASK_INVOLVES' as const,
- userId: mentioneeUserId,
+ userId: mentionee.id,
involvement: 'MENTIONEE',
taskId,
changeAuthorId,
@@ -187,15 +189,13 @@ export default {
return standardError(new Error(firstError), {userId: viewerId})
}
- const content = normalizeRawDraftJS(newTask.content)
- // const content = convertToTipTap(newTask.content)
- // const plaintextContent = generateText(content, serverTipTapExtensions)
+ const content = convertToTipTap(newTask.content)
+ const plaintextContent = generateText(content, serverTipTapExtensions)
// see if the task already exists
const integrationRes = await createTaskInService(
newTask.integration,
- // TODO: FIX ME
- content as any,
+ content,
viewerId,
teamId,
context,
@@ -213,8 +213,8 @@ export default {
}
const task = {
id: generateUID(),
- content,
- plaintextContent: extractTextFromDraftString(content),
+ content: JSON.stringify(content),
+ plaintextContent,
createdBy: viewerId,
meetingId,
sortOrder: sortOrder || dndNoise(),
@@ -226,14 +226,13 @@ export default {
threadSortOrder,
threadParentId,
userId: userId || null,
- // FIXME
- tags: getTagsFromEntityMap(JSON.parse(content as any).entityMap)
+ tags: getTagsFromTipTapTask(content)
}
const {id: taskId} = task
const teamMembers = await dataLoader.get('teamMembersByTeamId').load(teamId)
await pg.insertInto('Task').values(task).execute()
// FIXME
- handleAddTaskNotifications(teamMembers, task as any, viewerId, teamId, {
+ handleAddTaskNotifications(teamMembers, task, viewerId, teamId, {
operationId,
mutatorId
}).catch()
diff --git a/packages/server/graphql/mutations/createTaskIntegration.ts b/packages/server/graphql/mutations/createTaskIntegration.ts
index 108ab8ba6d2..ed9d1d2fa30 100644
--- a/packages/server/graphql/mutations/createTaskIntegration.ts
+++ b/packages/server/graphql/mutations/createTaskIntegration.ts
@@ -54,7 +54,7 @@ export default {
if (!task) {
return standardError(new Error('Task not found'), {userId: viewerId})
}
- const {content: rawContentStr, teamId, userId} = task
+ const {content: rawContentJSON, teamId, userId} = task
if (!isTeamMember(authToken, teamId)) {
return standardError(new Error('Team not found'), {userId: viewerId})
}
@@ -124,7 +124,7 @@ export default {
const teamDashboardUrl = makeAppURL(appOrigin, `team/${teamId}`)
const createTaskResponse = await taskIntegrationManager.createTask({
- rawContentStr,
+ rawContentJSON: JSON.parse(rawContentJSON),
integrationRepoId
})
diff --git a/packages/server/graphql/mutations/helpers/addSeedTasks.ts b/packages/server/graphql/mutations/helpers/addSeedTasks.ts
index 25ade85b187..bac1034ae9f 100644
--- a/packages/server/graphql/mutations/helpers/addSeedTasks.ts
+++ b/packages/server/graphql/mutations/helpers/addSeedTasks.ts
@@ -1,13 +1,13 @@
-import convertToTaskContent from 'parabol-client/utils/draftjs/convertToTaskContent'
-import getTagsFromEntityMap from 'parabol-client/utils/draftjs/getTagsFromEntityMap'
+import {generateJSON} from '@tiptap/html'
import makeAppURL from 'parabol-client/utils/makeAppURL'
+import {getTagsFromTipTapTask} from '../../../../client/shared/tiptap/getTagsFromTipTapTask'
+import {serverTipTapExtensions} from '../../../../client/shared/tiptap/serverTipTapExtensions'
import appOrigin from '../../../appOrigin'
import generateUID from '../../../generateUID'
import getKysely from '../../../postgres/getKysely'
-import {convertHtmlToTaskContent} from '../../../utils/draftjs/convertHtmlToTaskContent'
const NORMAL_TASK_STRING = `This is a task card. They can be created here, in a meeting, or via an integration`
-const INTEGRATIONS_TASK_STRING = `Parabol supports integrations for Jira, GitHub, GitLab, Slack and Mattermost. Connect your tools on Settings > Integrations.`
+const INTEGRATIONS_TASK_STRING = `Parabol supports integrations for Jira, GitHub, GitLab, Slack and Mattermost. Connect your tools in Settings > Integrations.`
function getSeedTasks(teamId: string) {
const searchParams = {
@@ -17,19 +17,22 @@ function getSeedTasks(teamId: string) {
}
const options = {searchParams}
const integrationURL = makeAppURL(appOrigin, `team/${teamId}/integrations`, options)
- const integrationTaskHTML = `Parabol supports integrations for Jira, GitHub, GitLab, Slack and Mattermost. Connect your tools in Integrations.`
-
return [
{
status: 'active' as const,
sortOrder: 1,
- content: convertToTaskContent(NORMAL_TASK_STRING),
+ content: JSON.stringify(generateJSON(`${NORMAL_TASK_STRING}
`, serverTipTapExtensions)),
plaintextContent: NORMAL_TASK_STRING
},
{
status: 'active' as const,
sortOrder: 0,
- content: convertHtmlToTaskContent(integrationTaskHTML),
+ content: JSON.stringify(
+ generateJSON(
+ `Parabol supports integrations for Jira, GitHub, GitLab, Slack and Mattermost. Connect your tools in Integrations.
`,
+ serverTipTapExtensions
+ )
+ ),
plaintextContent: INTEGRATIONS_TASK_STRING
}
]
@@ -44,7 +47,7 @@ export default async (userId: string, teamId: string) => {
id: `${teamId}::${generateUID()}`,
createdAt: now,
createdBy: userId,
- tags: getTagsFromEntityMap(JSON.parse(proj.content).entityMap),
+ tags: getTagsFromTipTapTask(JSON.parse(proj.content)),
teamId,
userId,
updatedAt: now
diff --git a/packages/server/graphql/mutations/helpers/createAzureTask.ts b/packages/server/graphql/mutations/helpers/createAzureTask.ts
index a8e8c8e06df..8046f7c0789 100644
--- a/packages/server/graphql/mutations/helpers/createAzureTask.ts
+++ b/packages/server/graphql/mutations/helpers/createAzureTask.ts
@@ -1,10 +1,11 @@
+import {JSONContent} from '@tiptap/core'
import {DataLoaderWorker} from '../../../graphql/graphql'
import {IntegrationProviderAzureDevOps} from '../../../postgres/queries/getIntegrationProvidersByIds'
import {TeamMemberIntegrationAuth} from '../../../postgres/types'
import AzureDevOpsServerManager from '../../../utils/AzureDevOpsServerManager'
const createAzureTask = async (
- rawContentStr: string,
+ rawContentJSON: JSONContent,
serviceProjectHash: string,
azureAuth: TeamMemberIntegrationAuth,
dataLoader: DataLoaderWorker
@@ -15,7 +16,7 @@ const createAzureTask = async (
provider as IntegrationProviderAzureDevOps
)
- return manager.createTask({rawContentStr, integrationRepoId: serviceProjectHash})
+ return manager.createTask({rawContentJSON, integrationRepoId: serviceProjectHash})
}
export default createAzureTask
diff --git a/packages/server/graphql/mutations/helpers/createGitHubTask.ts b/packages/server/graphql/mutations/helpers/createGitHubTask.ts
index e9a59535e77..9ebb7a33d98 100644
--- a/packages/server/graphql/mutations/helpers/createGitHubTask.ts
+++ b/packages/server/graphql/mutations/helpers/createGitHubTask.ts
@@ -1,6 +1,6 @@
-import {stateToMarkdown} from 'draft-js-export-markdown'
+import {JSONContent} from '@tiptap/core'
import {GraphQLResolveInfo} from 'graphql'
-import splitDraftContent from '../../../../client/utils/draftjs/splitDraftContent'
+import {splitTipTapContent} from 'parabol-client/shared/tiptap/splitTipTapContent'
import {GitHubAuth} from '../../../postgres/queries/getGitHubAuthByUserIdTeamId'
import {
CreateIssueMutation,
@@ -8,13 +8,14 @@ import {
GetRepoInfoQuery,
GetRepoInfoQueryVariables
} from '../../../types/githubTypes'
+import {convertTipTapToMarkdown} from '../../../utils/convertTipTapToMarkdown'
import getGitHubRequest from '../../../utils/getGitHubRequest'
import createIssueMutation from '../../../utils/githubQueries/createIssue.graphql'
import getRepoInfo from '../../../utils/githubQueries/getRepoInfo.graphql'
import {GQLContext} from '../../graphql'
const createGitHubTask = async (
- rawContent: string,
+ rawContent: JSONContent,
repoOwner: string,
repoName: string,
githubAuth: GitHubAuth,
@@ -22,8 +23,8 @@ const createGitHubTask = async (
info: GraphQLResolveInfo
) => {
const {accessToken, login} = githubAuth
- const {title, contentState} = splitDraftContent(rawContent)
- const body = stateToMarkdown(contentState) as string
+ const {title, bodyContent} = splitTipTapContent(rawContent)
+ const body = convertTipTapToMarkdown(bodyContent)
const githubRequest = getGitHubRequest(info, context, {
accessToken
})
diff --git a/packages/server/graphql/mutations/helpers/createGitLabTask.ts b/packages/server/graphql/mutations/helpers/createGitLabTask.ts
index 69c5ce4d93c..40a7837577e 100644
--- a/packages/server/graphql/mutations/helpers/createGitLabTask.ts
+++ b/packages/server/graphql/mutations/helpers/createGitLabTask.ts
@@ -1,12 +1,13 @@
-import {stateToMarkdown} from 'draft-js-export-markdown'
+import {JSONContent} from '@tiptap/core'
import {GraphQLResolveInfo} from 'graphql'
-import splitDraftContent from 'parabol-client/utils/draftjs/splitDraftContent'
+import {splitTipTapContent} from '../../../../client/shared/tiptap/splitTipTapContent'
import GitLabServerManager from '../../../integrations/gitlab/GitLabServerManager'
import {TeamMemberIntegrationAuth} from '../../../postgres/types'
+import {convertTipTapToMarkdown} from '../../../utils/convertTipTapToMarkdown'
import {DataLoaderWorker, GQLContext} from '../../graphql'
const createGitLabTask = async (
- rawContent: string,
+ rawContent: JSONContent,
fullPath: string,
gitlabAuth: TeamMemberIntegrationAuth,
context: GQLContext,
@@ -15,8 +16,8 @@ const createGitLabTask = async (
) => {
const {accessToken, providerId} = gitlabAuth
if (!accessToken) return {error: new Error('Invalid GitLab auth')}
- const {title, contentState} = splitDraftContent(rawContent)
- const body = stateToMarkdown(contentState)
+ const {title, bodyContent} = splitTipTapContent(rawContent)
+ const body = convertTipTapToMarkdown(bodyContent)
const provider = await dataLoader.get('integrationProviders').load(providerId)
const manager = new GitLabServerManager(gitlabAuth, context, info, provider!.serverBaseUrl!)
const [createIssueData, createIssueError] = await manager.createIssue({
diff --git a/packages/server/graphql/mutations/helpers/createJiraTask.ts b/packages/server/graphql/mutations/helpers/createJiraTask.ts
index ffcf6800add..aa75145f9fa 100644
--- a/packages/server/graphql/mutations/helpers/createJiraTask.ts
+++ b/packages/server/graphql/mutations/helpers/createJiraTask.ts
@@ -1,17 +1,18 @@
+import {JSONContent} from '@tiptap/core'
+import {splitTipTapContent} from 'parabol-client/shared/tiptap/splitTipTapContent'
import {RateLimitError} from 'parabol-client/utils/AtlassianManager'
-import splitDraftContent from 'parabol-client/utils/draftjs/splitDraftContent'
import {AtlassianAuth} from '../../../postgres/queries/getAtlassianAuthByUserIdTeamId'
import AtlassianServerManager from '../../../utils/AtlassianServerManager'
-import convertContentStateToADF from '../../../utils/convertContentStateToADF'
+import {convertTipTapToADF} from '../../../utils/convertTipTapToADF'
const createJiraTask = async (
- rawContent: string,
+ rawContent: JSONContent,
cloudId: string,
projectKey: string,
atlassianAuth: AtlassianAuth
) => {
- const {title: summary, contentState} = splitDraftContent(rawContent)
- const description = convertContentStateToADF(contentState)
+ const {title: summary, bodyContent} = splitTipTapContent(rawContent)
+ const description = convertTipTapToADF(bodyContent)
const {accessToken, accountId} = atlassianAuth
const manager = new AtlassianServerManager(accessToken)
diff --git a/packages/server/graphql/mutations/helpers/createTaskInService.ts b/packages/server/graphql/mutations/helpers/createTaskInService.ts
index e972d7fb08e..e7dffd86f66 100644
--- a/packages/server/graphql/mutations/helpers/createTaskInService.ts
+++ b/packages/server/graphql/mutations/helpers/createTaskInService.ts
@@ -1,3 +1,4 @@
+import {JSONContent} from '@tiptap/core'
import {GraphQLResolveInfo} from 'graphql'
import AzureDevOpsIssueId from 'parabol-client/shared/gqlIds/AzureDevOpsIssueId'
import GitLabIssueId from 'parabol-client/shared/gqlIds/GitLabIssueId'
@@ -8,7 +9,7 @@ import IntegrationRepoId from '../../../../client/shared/gqlIds/IntegrationRepoI
import JiraIssueId from '../../../../client/shared/gqlIds/JiraIssueId'
import JiraProjectId from '../../../../client/shared/gqlIds/JiraProjectId'
import JiraProjectKeyId from '../../../../client/shared/gqlIds/JiraProjectKeyId'
-import removeRangesForEntity from '../../../../client/utils/draftjs/removeRangesForEntity'
+import {removeNodeByType} from '../../../../client/shared/tiptap/removeNodeByType'
import {GQLContext} from '../../graphql'
import {CreateTaskIntegrationInput} from '../createTask'
import createAzureTask from './createAzureTask'
@@ -18,7 +19,7 @@ import createJiraTask from './createJiraTask'
const createTaskInService = async (
integrationInput: CreateTaskIntegrationInput | null | undefined,
- rawContent: string,
+ rawContent: JSONContent,
accessUserId: string,
teamId: string,
context: GQLContext,
@@ -27,8 +28,7 @@ const createTaskInService = async (
if (!integrationInput) return {integrationHash: undefined, integration: undefined}
const {dataLoader} = context
const {service, serviceProjectHash} = integrationInput
- const eqFn = (data: {value: string}) => ['archived', 'private'].includes(data.value)
- const taglessContentJSON = removeRangesForEntity(rawContent, 'TAG', eqFn) || rawContent
+ const taglessContentJSON = removeNodeByType(rawContent, 'taskTag')
if (service === 'jira') {
const atlassianAuth = await dataLoader
.get('freshAtlassianAuth')
diff --git a/packages/server/graphql/mutations/helpers/importTasksForPoker.ts b/packages/server/graphql/mutations/helpers/importTasksForPoker.ts
index 951dbf52354..bb0bf03dab2 100644
--- a/packages/server/graphql/mutations/helpers/importTasksForPoker.ts
+++ b/packages/server/graphql/mutations/helpers/importTasksForPoker.ts
@@ -1,8 +1,8 @@
import IntegrationHash from 'parabol-client/shared/gqlIds/IntegrationHash'
import {isNotNull} from 'parabol-client/utils/predicates'
+import {convertTipTapTaskContent} from '../../../../client/shared/tiptap/convertTipTapTaskContent'
+import {getTagsFromTipTapTask} from '../../../../client/shared/tiptap/getTagsFromTipTapTask'
import dndNoise from '../../../../client/utils/dndNoise'
-import convertToTaskContent from '../../../../client/utils/draftjs/convertToTaskContent'
-import getTagsFromEntityMap from '../../../../client/utils/draftjs/getTagsFromEntityMap'
import generateUID from '../../../generateUID'
import getKysely from '../../../postgres/getKysely'
import {selectTasks} from '../../../postgres/select'
@@ -45,7 +45,7 @@ const importTasksForPoker = async (
}
const integrationHash = IntegrationHash.join(integration)
const plaintextContent = `Task imported from ${integration.service} #archived`
- const content = convertToTaskContent(plaintextContent)
+ const content = convertTipTapTaskContent(plaintextContent)
return {
id: generateUID(),
content,
@@ -57,7 +57,7 @@ const importTasksForPoker = async (
integrationHash,
integration: JSON.stringify(integration),
meetingId,
- tags: getTagsFromEntityMap(JSON.parse(content).entityMap)
+ tags: getTagsFromTipTapTask(JSON.parse(content))
}
})
.filter(isNotNull)
diff --git a/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts b/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts
index eb52b52f02a..20d0876ae93 100644
--- a/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts
+++ b/packages/server/graphql/mutations/helpers/publishChangeNotifications.ts
@@ -1,4 +1,4 @@
-import getTypeFromEntityMap from 'parabol-client/utils/draftjs/getTypeFromEntityMap'
+import {getAllNodesAttributesByType} from '../../../../client/shared/tiptap/getAllNodesAttributesByType'
import generateUID from '../../../generateUID'
import getKysely from '../../../postgres/getKysely'
import {Task} from '../../../postgres/types'
@@ -13,12 +13,16 @@ const publishChangeNotifications = async (
) => {
const pg = getKysely()
const changeAuthorId = `${changeUser.id}::${task.teamId}`
- const {entityMap: oldEntityMap} = JSON.parse(oldTask.content)
- const {entityMap} = JSON.parse(task.content)
+ const oldContentJSON = JSON.parse(oldTask.content)
+ const contentJSON = JSON.parse(task.content)
const wasPrivate = oldTask.tags.includes('private')
const isPrivate = task.tags.includes('private')
- const oldMentions = wasPrivate ? [] : getTypeFromEntityMap('MENTION', oldEntityMap)
- const mentions = isPrivate ? [] : getTypeFromEntityMap('MENTION', entityMap)
+ const oldMentions = wasPrivate
+ ? []
+ : getAllNodesAttributesByType<{id: string}>(oldContentJSON, 'mention').map(({id}) => id)
+ const mentions = isPrivate
+ ? []
+ : getAllNodesAttributesByType<{id: string}>(contentJSON, 'mention').map(({id}) => id)
// intersect the mentions to get the ones to add and remove
const userIdsToRemove = oldMentions.filter((userId) => !mentions.includes(userId))
const notificationsToAdd = mentions
diff --git a/packages/server/graphql/mutations/updateTask.ts b/packages/server/graphql/mutations/updateTask.ts
index b9ac62a1415..db4a0aad1f4 100644
--- a/packages/server/graphql/mutations/updateTask.ts
+++ b/packages/server/graphql/mutations/updateTask.ts
@@ -1,11 +1,12 @@
+import {generateText} from '@tiptap/core'
import {GraphQLNonNull, GraphQLObjectType} from 'graphql'
import {SubscriptionChannel} from 'parabol-client/types/constEnums'
-import extractTextFromDraftString from 'parabol-client/utils/draftjs/extractTextFromDraftString'
-import normalizeRawDraftJS from 'parabol-client/validation/normalizeRawDraftJS'
-import getTagsFromEntityMap from '../../../client/utils/draftjs/getTagsFromEntityMap'
+import {getTagsFromTipTapTask} from '../../../client/shared/tiptap/getTagsFromTipTapTask'
+import {serverTipTapExtensions} from '../../../client/shared/tiptap/serverTipTapExtensions'
import getKysely from '../../postgres/getKysely'
import {Task} from '../../postgres/types/index'
import {getUserId, isTeamMember} from '../../utils/authorization'
+import {convertToTipTap} from '../../utils/convertToTipTap'
import publish from '../../utils/publish'
import standardError from '../../utils/standardError'
import {GQLContext} from '../graphql'
@@ -54,7 +55,12 @@ export default {
// VALIDATION
const {id: taskId, userId: inputUserId, status, sortOrder, content} = updatedTask
- const validContent = normalizeRawDraftJS(content)
+
+ const validContent = convertToTipTap(content)
+ const plaintextContent = content
+ ? generateText(validContent, serverTipTapExtensions)
+ : undefined
+
const [task, viewer] = await Promise.all([
dataLoader.get('tasks').load(taskId),
dataLoader.get('users').loadNonNull(viewerId)
@@ -79,11 +85,11 @@ export default {
.updateTable('Task')
.set({
content: content ? validContent : undefined,
- plaintextContent: content ? extractTextFromDraftString(validContent) : undefined,
+ plaintextContent,
sortOrder: sortOrder || undefined,
status: status || undefined,
userId: inputUserId || undefined,
- tags: content ? getTagsFromEntityMap(JSON.parse(validContent).entityMap) : undefined
+ tags: content ? getTagsFromTipTapTask(validContent) : undefined
})
.where('id', '=', taskId)
.executeTakeFirst()
diff --git a/packages/server/graphql/public/mutations/upsertTeamPromptResponse.ts b/packages/server/graphql/public/mutations/upsertTeamPromptResponse.ts
index cd5d32de2a0..b3b95c58e03 100644
--- a/packages/server/graphql/public/mutations/upsertTeamPromptResponse.ts
+++ b/packages/server/graphql/public/mutations/upsertTeamPromptResponse.ts
@@ -1,12 +1,12 @@
import {generateText, JSONContent} from '@tiptap/core'
import TeamPromptResponseId from 'parabol-client/shared/gqlIds/TeamPromptResponseId'
import {SubscriptionChannel} from 'parabol-client/types/constEnums'
+import {serverTipTapExtensions} from '../../../../client/shared/tiptap/serverTipTapExtensions'
import {upsertTeamPromptResponse as upsertTeamPromptResponseQuery} from '../../../postgres/queries/upsertTeamPromptResponses'
import {TeamPromptResponse} from '../../../postgres/types'
import {analytics} from '../../../utils/analytics/analytics'
import {getUserId, isTeamMember} from '../../../utils/authorization'
import publish from '../../../utils/publish'
-import {serverTipTapExtensions} from '../../../utils/serverTipTapExtensions'
import standardError from '../../../utils/standardError'
import {IntegrationNotifier} from '../../mutations/helpers/notifications/IntegrationNotifier'
import {MutationResolvers} from '../resolverTypes'
diff --git a/packages/server/graphql/public/types/Task.ts b/packages/server/graphql/public/types/Task.ts
index 645ea20187b..e8868952397 100644
--- a/packages/server/graphql/public/types/Task.ts
+++ b/packages/server/graphql/public/types/Task.ts
@@ -1,12 +1,14 @@
import AzureDevOpsIssueId from 'parabol-client/shared/gqlIds/AzureDevOpsIssueId'
import JiraServerIssueId from '~/shared/gqlIds/JiraServerIssueId'
import GitHubRepoId from '../../../../client/shared/gqlIds/GitHubRepoId'
+import {isDraftJSContent} from '../../../../client/shared/tiptap/isDraftJSContent'
import GitLabServerManager from '../../../integrations/gitlab/GitLabServerManager'
import {IGetLatestTaskEstimatesQueryResult} from '../../../postgres/queries/generated/getLatestTaskEstimatesQuery'
import getSimilarTaskEstimate from '../../../postgres/queries/getSimilarTaskEstimate'
import insertTaskEstimate from '../../../postgres/queries/insertTaskEstimate'
import {GetIssueLabelsQuery, GetIssueLabelsQueryVariables} from '../../../types/githubTypes'
import {getUserId} from '../../../utils/authorization'
+import {convertKnownDraftToTipTap} from '../../../utils/convertToTipTap'
import getGitHubRequest from '../../../utils/getGitHubRequest'
import getIssueLabels from '../../../utils/githubQueries/getIssueLabels.graphql'
import sendToSentry from '../../../utils/sendToSentry'
@@ -28,6 +30,30 @@ const Task: Omit, 'replies'> = {
return integration?.service ?? null
},
+ content: async ({content}) => {
+ // cheaply check to see if it might be draft-js content
+ if (!content.includes('entityMap')) return content
+
+ // actually check if it's draft-js content
+ const contentJSON = JSON.parse(content)
+ if (!isDraftJSContent(contentJSON)) return content
+
+ // this is Draft-JS Content. convert it, save it, send it down
+ const tipTapContent = convertKnownDraftToTipTap(contentJSON)
+ const contentStr = JSON.stringify(tipTapContent)
+
+ // HACK we shouldn't be writing to the DB in a query,
+ // but we're doing it here just until we can migrate all tasks over to TipTap
+ // const pg = getKysely()
+ // await pg
+ // .updateTable('Task')
+ // .set({
+ // content: contentStr
+ // })
+ // .where('id', '=', taskId)
+ // .execute()
+ return contentStr
+ },
createdByUser: ({createdBy}, _args, {dataLoader}) => {
return dataLoader.get('users').loadNonNull(createdBy)
},
diff --git a/packages/server/integrations/TaskIntegrationManagerFactory.ts b/packages/server/integrations/TaskIntegrationManagerFactory.ts
index 44f67180886..a535c686931 100644
--- a/packages/server/integrations/TaskIntegrationManagerFactory.ts
+++ b/packages/server/integrations/TaskIntegrationManagerFactory.ts
@@ -1,3 +1,4 @@
+import {JSONContent} from '@tiptap/core'
import {GraphQLResolveInfo} from 'graphql'
import {DataLoaderWorker, GQLContext} from '../graphql/graphql'
import {IntegrationProviderServiceEnumType} from '../graphql/types/IntegrationProviderServiceEnum'
@@ -26,7 +27,7 @@ export interface TaskIntegrationManager {
title: string
createTask(params: {
- rawContentStr: string
+ rawContentJSON: JSONContent
integrationRepoId: string
context?: GQLContext
info?: GraphQLResolveInfo
diff --git a/packages/server/integrations/github/GitHubServerManager.ts b/packages/server/integrations/github/GitHubServerManager.ts
index 19a165492fd..edb19e07a11 100644
--- a/packages/server/integrations/github/GitHubServerManager.ts
+++ b/packages/server/integrations/github/GitHubServerManager.ts
@@ -1,3 +1,4 @@
+import {JSONContent} from '@tiptap/core'
import {GraphQLResolveInfo} from 'graphql'
import GitHubIssueId from '../../../client/shared/gqlIds/GitHubIssueId'
import GitHubRepoId from '../../../client/shared/gqlIds/GitHubRepoId'
@@ -60,16 +61,16 @@ export default class GitHubServerManager implements TaskIntegrationManager {
}
async createTask({
- rawContentStr,
+ rawContentJSON,
integrationRepoId
}: {
- rawContentStr: string
+ rawContentJSON: JSONContent
integrationRepoId: string
}): Promise {
const {repoOwner, repoName} = GitHubRepoId.split(integrationRepoId)
const res = await createGitHubTask(
- rawContentStr,
+ rawContentJSON,
repoOwner,
repoName,
this.auth,
diff --git a/packages/server/integrations/gitlab/GitLabServerManager.ts b/packages/server/integrations/gitlab/GitLabServerManager.ts
index 5a1aa1ca578..b8a9a4dbe86 100644
--- a/packages/server/integrations/gitlab/GitLabServerManager.ts
+++ b/packages/server/integrations/gitlab/GitLabServerManager.ts
@@ -1,7 +1,7 @@
-import {stateToMarkdown} from 'draft-js-export-markdown'
+import {JSONContent} from '@tiptap/core'
import {GraphQLResolveInfo} from 'graphql'
import GitLabIssueId from 'parabol-client/shared/gqlIds/GitLabIssueId'
-import splitDraftContent from 'parabol-client/utils/draftjs/splitDraftContent'
+import {splitTipTapContent} from 'parabol-client/shared/tiptap/splitTipTapContent'
import {GQLContext} from '../../graphql/graphql'
import createIssueMutation from '../../graphql/nestedSchema/GitLab/mutations/createIssue.graphql'
import createNote from '../../graphql/nestedSchema/GitLab/mutations/createNote.graphql'
@@ -20,6 +20,7 @@ import {
GetProjectIssuesQueryVariables,
GetProjectsQuery
} from '../../types/gitlabTypes'
+import {convertTipTapToMarkdown} from '../../utils/convertTipTapToMarkdown'
import makeCreateGitLabTaskComment from '../../utils/makeCreateGitLabTaskComment'
import {CreateTaskResponse, TaskIntegrationManager} from '../TaskIntegrationManagerFactory'
@@ -84,14 +85,14 @@ class GitLabServerManager implements TaskIntegrationManager {
}
async createTask({
- rawContentStr,
+ rawContentJSON,
integrationRepoId
}: {
- rawContentStr: string
+ rawContentJSON: JSONContent
integrationRepoId: string
}): Promise {
- const {title, contentState} = splitDraftContent(rawContentStr)
- const description = stateToMarkdown(contentState) as string
+ const {title, bodyContent} = splitTipTapContent(rawContentJSON)
+ const description = convertTipTapToMarkdown(bodyContent)
const [createIssueData, createIssueError] = await this.createIssue({
title,
description,
diff --git a/packages/server/integrations/jira/JiraIntegrationManager.ts b/packages/server/integrations/jira/JiraIntegrationManager.ts
index 60f3c5f0d06..4f06df1fb46 100644
--- a/packages/server/integrations/jira/JiraIntegrationManager.ts
+++ b/packages/server/integrations/jira/JiraIntegrationManager.ts
@@ -1,3 +1,4 @@
+import {JSONContent} from '@tiptap/core'
import JiraIssueId from 'parabol-client/shared/gqlIds/JiraIssueId'
import JiraProjectId from 'parabol-client/shared/gqlIds/JiraProjectId'
import createJiraTask from '../../graphql/mutations/helpers/createJiraTask'
@@ -35,15 +36,15 @@ export default class JiraIntegrationManager
}
async createTask({
- rawContentStr,
+ rawContentJSON,
integrationRepoId
}: {
- rawContentStr: string
+ rawContentJSON: JSONContent
integrationRepoId: string
}): Promise {
const {cloudId, projectKey} = JiraProjectId.split(integrationRepoId)
- const res = await createJiraTask(rawContentStr, cloudId, projectKey, this.auth)
+ const res = await createJiraTask(rawContentJSON, cloudId, projectKey, this.auth)
if (res.error) return res.error
diff --git a/packages/server/integrations/jiraServer/JiraServerRestManager.ts b/packages/server/integrations/jiraServer/JiraServerRestManager.ts
index daae367d1ed..40c1ed9cbb2 100644
--- a/packages/server/integrations/jiraServer/JiraServerRestManager.ts
+++ b/packages/server/integrations/jiraServer/JiraServerRestManager.ts
@@ -1,10 +1,12 @@
+import {generateText, JSONContent} from '@tiptap/core'
import crypto from 'crypto'
import OAuth from 'oauth-1.0a'
+import {serverTipTapExtensions} from 'parabol-client/shared/tiptap/serverTipTapExtensions'
+import {splitTipTapContent} from 'parabol-client/shared/tiptap/splitTipTapContent'
import IntegrationRepoId from '~/shared/gqlIds/IntegrationRepoId'
import JiraServerIssueId from '~/shared/gqlIds/JiraServerIssueId'
import {ExternalLinks} from '~/types/constEnums'
import composeJQL from '~/utils/composeJQL'
-import splitDraftContent from '~/utils/draftjs/splitDraftContent'
import {IntegrationProviderJiraServer} from '../../postgres/queries/getIntegrationProvidersByIds'
import {TeamMemberIntegrationAuth} from '../../postgres/types'
import {CreateTaskResponse, TaskIntegrationManager} from '../TaskIntegrationManagerFactory'
@@ -285,15 +287,16 @@ export default class JiraServerRestManager implements TaskIntegrationManager {
}
async createTask({
- rawContentStr,
+ rawContentJSON,
integrationRepoId
}: {
- rawContentStr: string
+ rawContentJSON: JSONContent
integrationRepoId: string
}): Promise {
- const {title: summary, contentState} = splitDraftContent(rawContentStr)
+ const {title: summary, bodyContent} = splitTipTapContent(rawContentJSON)
+
// TODO: implement stateToJiraServerFormat
- const description = contentState.getPlainText()
+ const description = generateText(bodyContent, serverTipTapExtensions)
const {repositoryId} = IntegrationRepoId.split(integrationRepoId)
diff --git a/packages/server/package.json b/packages/server/package.json
index a2728a15478..3c5ca289a2a 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -121,10 +121,12 @@
"mailcomposer": "^4.0.1",
"mailgun.js": "^9.3.0",
"markdown-draft-js": "^2.4.0",
+ "md-to-adf": "^0.6.4",
"mime-types": "^2.1.16",
"ms": "^2.0.0",
"nest-graphql-endpoint": "0.8.1",
"node-env-flag": "0.1.0",
+ "node-html-markdown": "^1.3.0",
"nodemailer": "^6.9.9",
"oauth-1.0a": "^2.2.6",
"openai": "^4.53.0",
diff --git a/packages/server/safeMutations/archiveTasksForDB.ts b/packages/server/safeMutations/archiveTasksForDB.ts
index 18b005cdf88..29bc5831980 100644
--- a/packages/server/safeMutations/archiveTasksForDB.ts
+++ b/packages/server/safeMutations/archiveTasksForDB.ts
@@ -1,18 +1,17 @@
-import {convertFromRaw, convertToRaw} from 'draft-js'
-import addTagToTask from 'parabol-client/utils/draftjs/addTagToTask'
-import getTagsFromEntityMap from 'parabol-client/utils/draftjs/getTagsFromEntityMap'
+import addTagToTask from '../../client/shared/tiptap/addTagToTask'
+import {getTagsFromTipTapTask} from '../../client/shared/tiptap/getTagsFromTipTapTask'
import getKysely from '../postgres/getKysely'
import {Task} from '../postgres/types/index.d'
+import {convertToTipTap} from '../utils/convertToTipTap'
const archiveTasksForDB = async (tasks: Task[], doneMeetingId?: string) => {
if (!tasks || tasks.length === 0) return []
const pg = getKysely()
const tasksToArchive = tasks.map((task) => {
- const contentState = convertFromRaw(JSON.parse(task.content))
- const nextContentState = addTagToTask(contentState, '#archived')
- const raw = convertToRaw(nextContentState)
- const nextTags = getTagsFromEntityMap(raw.entityMap)
- const nextContentStr = JSON.stringify(raw)
+ const content = convertToTipTap(task.content)
+ const nextContent = addTagToTask(content, 'archived')
+ const nextTags = getTagsFromTipTapTask(nextContent)
+ const nextContentStr = JSON.stringify(nextContent)
// update cache
task.content = nextContentStr
diff --git a/packages/server/types/modules.d.ts b/packages/server/types/modules.d.ts
index 9b16b604830..c7f7fb31c96 100644
--- a/packages/server/types/modules.d.ts
+++ b/packages/server/types/modules.d.ts
@@ -20,11 +20,11 @@ declare module '@authenio/samlify-node-xmllint'
declare module 'node-env-flag'
declare module '*getProjectRoot'
declare module 'tayden-clusterfck'
-declare module 'unicode-substring'
declare module 'jest-extended'
declare module 'json2csv/lib/JSON2CSVParser'
declare module 'object-hash'
declare module 'string-score'
+declare module 'md-to-adf'
declare const __APP_VERSION__: string
declare const __PRODUCTION__: boolean
diff --git a/packages/server/utils/AzureDevOpsServerManager.ts b/packages/server/utils/AzureDevOpsServerManager.ts
index 0436ff3b676..dace2f5440f 100644
--- a/packages/server/utils/AzureDevOpsServerManager.ts
+++ b/packages/server/utils/AzureDevOpsServerManager.ts
@@ -1,6 +1,7 @@
+import {JSONContent} from '@tiptap/core'
import AzureDevOpsIssueId from 'parabol-client/shared/gqlIds/AzureDevOpsIssueId'
import IntegrationHash from 'parabol-client/shared/gqlIds/IntegrationHash'
-import splitDraftContent from 'parabol-client/utils/draftjs/splitDraftContent'
+import {splitTipTapContent} from 'parabol-client/shared/tiptap/splitTipTapContent'
import makeAppURL from 'parabol-client/utils/makeAppURL'
import {isError} from 'util'
import {ExternalLinks} from '~/types/constEnums'
@@ -357,13 +358,13 @@ class AzureDevOpsServerManager implements TaskIntegrationManager {
}
async createTask({
- rawContentStr,
+ rawContentJSON,
integrationRepoId
}: {
- rawContentStr: string
+ rawContentJSON: JSONContent
integrationRepoId: string
}): Promise {
- const {title} = splitDraftContent(rawContentStr)
+ const {title} = splitTipTapContent(rawContentJSON)
const {instanceId, projectId} = AzureDevOpsProjectId.split(integrationRepoId)
const issueRes = await this.createIssue({title, instanceId, projectId})
if (issueRes instanceof Error) return issueRes
diff --git a/packages/server/utils/convertContentStateToADF.ts b/packages/server/utils/convertContentStateToADF.ts
deleted file mode 100644
index 90c4e988a93..00000000000
--- a/packages/server/utils/convertContentStateToADF.ts
+++ /dev/null
@@ -1,258 +0,0 @@
-import {ContentBlock, ContentState} from 'draft-js'
-import {Constants, getEntityRanges} from 'draft-js-utils'
-import {isNotNull} from '../../client/utils/predicates'
-
-const {INLINE_STYLE, BLOCK_TYPE, ENTITY_TYPE} = Constants
-
-interface Mark {
- type: string
- attrs?: Record
-}
-
-interface Text {
- type: 'text'
- text: string
- marks?: Mark[]
-}
-
-type InlineNode =
- | {
- type: 'emoji' | 'hardBreak' | 'inlineCard' | 'mention'
- }
- | Text
-
-type Content = (Node | InlineNode)[]
-
-interface Node {
- type: string
- attrs?: Record
- content?: Content
-}
-
-export interface Doc {
- version: 1
- type: 'doc'
- content: Content
-}
-
-class ContentStateToADFConverter {
- private contentState: ContentState
-
- private renderText = (text: string, marks?: Mark[]): Text => ({
- type: 'text',
- text,
- ...(marks?.length ? {marks} : {})
- })
-
- private renderContent = (block: ContentBlock): Node[] => {
- const text = block.getText()
- const charMetaList = block.getCharacterList()
- const entityPieces = getEntityRanges(text, charMetaList) as [
- [entityKey: string, stylePieces: [text: string, style: Set][]]
- ]
- const content = entityPieces.flatMap(([entityKey, stylePieces]) => {
- const contentPieces: Text[] = stylePieces
- .map(([text, style]) => {
- if (!text) {
- return null
- }
-
- if (style.has(INLINE_STYLE.CODE)) {
- return {
- type: 'text' as const,
- text,
- marks: [
- {
- type: 'code'
- }
- ]
- }
- }
- const marks: Mark[] = []
- if (style.has(INLINE_STYLE.BOLD)) {
- marks.push({
- type: 'strong'
- })
- }
- if (style.has(INLINE_STYLE.UNDERLINE)) {
- marks.push({
- type: 'underline'
- })
- }
- if (style.has(INLINE_STYLE.ITALIC)) {
- marks.push({
- type: 'em'
- })
- }
- if (style.has(INLINE_STYLE.STRIKETHROUGH)) {
- marks.push({
- type: 'strike'
- })
- }
- return this.renderText(text, marks)
- })
- .filter(isNotNull)
-
- const entity = entityKey ? this.contentState.getEntity(entityKey) : null
- if (entity && entity.getType() === ENTITY_TYPE.LINK) {
- const data = entity.getData()
- const mark = {
- type: 'link',
- attrs: {
- href: data.href || data.url || '',
- ...(data.title ? {title: data.title} : {})
- }
- }
- return contentPieces.map((piece) => ({
- ...piece,
- marks: [mark, ...(piece.marks ?? [])]
- }))
- }
- /*
- TODO: In order to show images or other embedded types, we need to upload those separately to Jira and then refer to them by id
- else if (entity?.getType() === ENTITY_TYPE.IMAGE) {
- const data = entity.getData();
- const src = data.src || '';
- const alt = data.alt ? `${escapeTitle(data.alt)}` : '';
- return ...
- } else if (entity?.getType() === ENTITY_TYPE.EMBED) {
- const data = entity.getData()
- const url = data.url || content;
- return ...
- }
- */
-
- return contentPieces
- })
- return content
- }
-
- private renderHeader = (block: ContentBlock, level: number): Node => ({
- type: 'heading',
- attrs: {
- level
- },
- content: this.renderContent(block)
- })
-
- constructor(contentState: ContentState) {
- this.contentState = contentState
- }
-
- generate = (): Doc => {
- const blocks = this.contentState.getBlocksAsArray()
-
- // list items need to be nested, so store them and add them to the list once a non-list item is found
- let currentListItems: {
- type: 'listItem'
- content: any[]
- }[] = []
- let currentListType: string | null = null
- const flushList = () => {
- const type = currentListType === BLOCK_TYPE.UNORDERED_LIST_ITEM ? 'bulletList' : 'orderedList'
- const content = currentListItems
- currentListItems = []
- currentListType = null
- return {
- type,
- content
- }
- }
-
- const content = blocks
- .map((block) => {
- const blockType = block.getType()
- const text = block.getText()
- if (!text) {
- return null
- }
-
- if (blockType === currentListType) {
- currentListItems.push({
- type: 'listItem',
- // Jira wants a top level element nested in the listItem
- content: [
- {
- type: 'paragraph',
- content: this.renderContent(block)
- }
- ]
- })
- // filter out later
- return null
- }
- if (currentListType) {
- return flushList()
- }
- if (
- blockType === BLOCK_TYPE.UNORDERED_LIST_ITEM ||
- blockType === BLOCK_TYPE.ORDERED_LIST_ITEM
- ) {
- currentListType = blockType
- currentListItems = [
- {
- type: 'listItem',
- // Jira wants a top level element nested in the listItem
- content: [
- {
- type: 'paragraph',
- content: this.renderContent(block)
- }
- ]
- }
- ]
- // filter out later
- return null
- }
-
- switch (blockType) {
- case BLOCK_TYPE.HEADER_ONE:
- return this.renderHeader(block, 1)
- case BLOCK_TYPE.HEADER_TWO:
- return this.renderHeader(block, 2)
- case BLOCK_TYPE.HEADER_THREE:
- return this.renderHeader(block, 3)
- case BLOCK_TYPE.HEADER_FOUR:
- return this.renderHeader(block, 4)
- case BLOCK_TYPE.HEADER_FIVE:
- return this.renderHeader(block, 5)
- case BLOCK_TYPE.HEADER_SIX:
- return this.renderHeader(block, 6)
- case BLOCK_TYPE.BLOCKQUOTE:
- return {
- type: 'blockquote',
- content: this.renderContent(block)
- }
- case BLOCK_TYPE.CODE:
- return {
- type: 'codeBlock',
- content: this.renderContent(block)
- }
- case BLOCK_TYPE.ATOMIC:
- case BLOCK_TYPE.UNSTYLED:
- default:
- return {
- type: 'paragraph',
- content: this.renderContent(block)
- }
- }
- })
- .filter((contentBlock) => contentBlock !== null) as Node[]
- // flush the list if the document stopped with one
- if (currentListType) {
- content.push(flushList())
- }
-
- return {
- version: 1,
- type: 'doc',
- content
- }
- }
-}
-
-const convertContentStateToADF = (contentState: ContentState) => {
- return new ContentStateToADFConverter(contentState).generate()
-}
-
-export default convertContentStateToADF
diff --git a/packages/server/utils/convertTipTapToADF.ts b/packages/server/utils/convertTipTapToADF.ts
new file mode 100644
index 00000000000..8bf5e6d29ed
--- /dev/null
+++ b/packages/server/utils/convertTipTapToADF.ts
@@ -0,0 +1,8 @@
+import {JSONContent} from '@tiptap/core'
+import fnTranslate from 'md-to-adf'
+import {convertTipTapToMarkdown} from './convertTipTapToMarkdown'
+export const convertTipTapToADF = (content: JSONContent) => {
+ const markdown = convertTipTapToMarkdown(content)
+ const adf = fnTranslate(markdown)
+ return adf
+}
diff --git a/packages/server/utils/convertTipTapToMarkdown.ts b/packages/server/utils/convertTipTapToMarkdown.ts
new file mode 100644
index 00000000000..4a5d4617971
--- /dev/null
+++ b/packages/server/utils/convertTipTapToMarkdown.ts
@@ -0,0 +1,9 @@
+import {JSONContent} from '@tiptap/core'
+import {generateHTML} from '@tiptap/html'
+import {NodeHtmlMarkdown} from 'node-html-markdown'
+import {serverTipTapExtensions} from 'parabol-client/shared/tiptap/serverTipTapExtensions'
+export const convertTipTapToMarkdown = (content: JSONContent) => {
+ const html = generateHTML(content, serverTipTapExtensions)
+ const markdown = NodeHtmlMarkdown.translate(html)
+ return markdown
+}
diff --git a/packages/server/utils/convertToTipTap.ts b/packages/server/utils/convertToTipTap.ts
index ef106a01d5b..5b772cdb015 100644
--- a/packages/server/utils/convertToTipTap.ts
+++ b/packages/server/utils/convertToTipTap.ts
@@ -5,30 +5,76 @@ import {Paragraph} from '@tiptap/extension-paragraph'
import {Text} from '@tiptap/extension-text'
import {generateJSON} from '@tiptap/html'
import {convertFromRaw, RawDraftContentState} from 'draft-js'
-import {stateToHTML} from 'draft-js-export-html'
+import {Options, stateToHTML} from 'draft-js-export-html'
+import {isDraftJSContent} from '../../client/shared/tiptap/isDraftJSContent'
+import {serverTipTapExtensions} from '../../client/shared/tiptap/serverTipTapExtensions'
+import {Logger} from './Logger'
-const isDraftJSContent = (
- content: JSONContent | RawDraftContentState
-): content is RawDraftContentState => {
- return 'blocks' in content
+const getNameFromEntity = (content: RawDraftContentState, userId: string) => {
+ const {blocks, entityMap} = content
+ const entityKey = Number(
+ Object.keys(entityMap).find((key) => entityMap[key]!.data?.userId === userId)
+ )
+ for (let i = 0; i < blocks.length; i++) {
+ const block = blocks[i]!
+ const {entityRanges, text} = block
+ const entityRange = entityRanges.find((range) => range.key === entityKey)
+ if (!entityRange) continue
+ const {length, offset} = entityRange
+ return text.slice(offset, offset + length)
+ }
+ Logger.log('found unknown for', userId, JSON.stringify(content))
+ return 'Unknown User'
}
+export const convertKnownDraftToTipTap = (content: RawDraftContentState) => {
+ const contentState = convertFromRaw(content)
+ // TODO: blockRenderers! convert entity map to match what tiptap expects
+ const options: Options = {
+ entityStyleFn: (entity) => {
+ const entityType = entity.getType().toLowerCase()
+ const data = entity.getData()
+ if (entityType === 'tag') {
+ return {
+ element: 'span',
+ attributes: {
+ 'data-id': data.value,
+ 'data-type': 'taskTag'
+ }
+ }
+ }
+ // TODO FIX ME WHEN WE DO THE CONVERSION
+ if (entityType === 'mention') {
+ const label = getNameFromEntity(content, data.userId)
+ return {
+ element: 'span',
+ attributes: {
+ 'data-id': data.userId,
+ 'data-label': label,
+ 'data-type': 'mention'
+ }
+ }
+ }
+ return
+ }
+ }
+ const html = stateToHTML(contentState, options)
+ const json = generateJSON(html, serverTipTapExtensions) as JSONContent
+ return json
+}
export const convertToTipTap = (contentStr: string | null | undefined) => {
// TODO: add extensions to handle tags and mentions
const tipTapExtensions = [Document, Paragraph, Text, Bold]
if (!contentStr) {
// return an empty str
- return generateJSON(``, tipTapExtensions)
+ return generateJSON(``, tipTapExtensions) as JSONContent
}
let parsedContent: JSONContent | RawDraftContentState
try {
parsedContent = JSON.parse(contentStr)
} catch (e) {
- return generateJSON(``, tipTapExtensions)
+ return generateJSON(``, tipTapExtensions) as JSONContent
}
if (!isDraftJSContent(parsedContent)) return parsedContent
- const contentState = convertFromRaw(parsedContent)
- // TODO: blockRenderers! convert entity map to match what tiptap expects
- const html = stateToHTML(contentState)
- return generateJSON(html, tipTapExtensions) as JSONContent
+ return convertKnownDraftToTipTap(parsedContent)
}
diff --git a/packages/server/utils/makeCreateJiraTaskComment.ts b/packages/server/utils/makeCreateJiraTaskComment.ts
index 76ca9e670d9..2d2e61184d0 100644
--- a/packages/server/utils/makeCreateJiraTaskComment.ts
+++ b/packages/server/utils/makeCreateJiraTaskComment.ts
@@ -1,5 +1,35 @@
import {ExternalLinks} from '../../client/types/constEnums'
-import {Doc} from './convertContentStateToADF'
+
+interface Mark {
+ type: string
+ attrs?: Record
+}
+
+interface Text {
+ type: 'text'
+ text: string
+ marks?: Mark[]
+}
+
+type InlineNode =
+ | {
+ type: 'emoji' | 'hardBreak' | 'inlineCard' | 'mention'
+ }
+ | Text
+
+type Content = (Node | InlineNode)[]
+
+interface Node {
+ type: string
+ attrs?: Record
+ content?: Content
+}
+
+export interface Doc {
+ version: 1
+ type: 'doc'
+ content: Content
+}
const makeCreateJiraTaskComment = (
creator: string,
diff --git a/packages/server/utils/makeScoreJiraComment.ts b/packages/server/utils/makeScoreJiraComment.ts
index 69c88ce86ad..494c9734443 100644
--- a/packages/server/utils/makeScoreJiraComment.ts
+++ b/packages/server/utils/makeScoreJiraComment.ts
@@ -1,5 +1,5 @@
import {ExternalLinks} from '../../client/types/constEnums'
-import {Doc} from './convertContentStateToADF'
+import {Doc} from './makeCreateJiraTaskComment'
const makeScoreJiraComment = (
dimensionName: string,
diff --git a/packages/server/utils/serverTipTapExtensions.ts b/packages/server/utils/serverTipTapExtensions.ts
deleted file mode 100644
index 8b57092a503..00000000000
--- a/packages/server/utils/serverTipTapExtensions.ts
+++ /dev/null
@@ -1,24 +0,0 @@
-import {mergeAttributes} from '@tiptap/core'
-import BaseLink from '@tiptap/extension-link'
-import Mention from '@tiptap/extension-mention'
-import StarterKit from '@tiptap/starter-kit'
-import {LoomExtension} from '../../client/components/promptResponse/loomExtension'
-
-export const serverTipTapExtensions = [
- StarterKit,
- LoomExtension,
- Mention.configure({
- renderText({node}) {
- return node.attrs.label
- }
- }),
- BaseLink.extend({
- parseHTML() {
- return [{tag: 'a[href]:not([data-type="button"]):not([href *= "javascript:" i])'}]
- },
-
- renderHTML({HTMLAttributes}) {
- return ['a', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {class: 'link'}), 0]
- }
- })
-]
diff --git a/scripts/webpack/dev.clientdll.config.js b/scripts/webpack/dev.clientdll.config.js
index ecb3715e706..b63ba59b9bb 100644
--- a/scripts/webpack/dev.clientdll.config.js
+++ b/scripts/webpack/dev.clientdll.config.js
@@ -49,8 +49,7 @@ module.exports = {
'string-score',
'tayden-clusterfck',
'tlds',
- 'tslib',
- 'unicode-substring'
+ 'tslib'
]
},
resolve: {
diff --git a/yarn.lock b/yarn.lock
index 6ee701b2afa..10c58f7e7bd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8690,6 +8690,11 @@ addressparser@1.0.1:
resolved "https://registry.yarnpkg.com/addressparser/-/addressparser-1.0.1.tgz#47afbe1a2a9262191db6838e4fd1d39b40821746"
integrity sha512-aQX7AISOMM7HFE0iZ3+YnD07oIeJqWGVnJ+ZIKaBZAk03ftmVYVqsGas/rbXKR21n4D/hKCSHypvcyOkds/xzg==
+adf-builder@^3.3.0:
+ version "3.3.0"
+ resolved "https://registry.yarnpkg.com/adf-builder/-/adf-builder-3.3.0.tgz#23d69266486b403727f130ae2217a262f7be78a5"
+ integrity sha512-zl5Sk2HtJk/E+gBnD0g51w5wIEEujaAbzsBhSsteO8Fnef7v+7pvas0FeQuLCAgOxrwy945HWAB1gQ4yjPI3/g==
+
agent-base@6, agent-base@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
@@ -13621,7 +13626,7 @@ hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2:
dependencies:
function-bind "^1.1.2"
-he@^1.2.0:
+he@1.2.0, he@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
@@ -16389,6 +16394,11 @@ markdown-it@^14.0.0, markdown-it@^14.1.0:
punycode.js "^2.3.1"
uc.micro "^2.1.0"
+marked@^0.8.2:
+ version "0.8.2"
+ resolved "https://registry.yarnpkg.com/marked/-/marked-0.8.2.tgz#4faad28d26ede351a7a1aaa5fec67915c869e355"
+ integrity sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw==
+
marked@^13.0.3:
version "13.0.3"
resolved "https://registry.yarnpkg.com/marked/-/marked-13.0.3.tgz#5c5b4a5d0198060c7c9bc6ef9420a7fed30f822d"
@@ -16399,6 +16409,14 @@ marked@^4.3.0:
resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3"
integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==
+md-to-adf@^0.6.4:
+ version "0.6.4"
+ resolved "https://registry.yarnpkg.com/md-to-adf/-/md-to-adf-0.6.4.tgz#c402d9a1ed570189a39d5ad200c1c88ecbdbd5e4"
+ integrity sha512-MynWm2w+adyzeaCJGXoOjgHAAhql0I9jR2Bq55hM7GQ05u6cGX7jThdWN6dgVSGnBmuAGmZGy2rDGBP+hJRckA==
+ dependencies:
+ adf-builder "^3.3.0"
+ marked "^0.8.2"
+
md5@^2.2.1:
version "2.3.0"
resolved "https://registry.yarnpkg.com/md5/-/md5-2.3.0.tgz#c3da9a6aae3a30b46b7b0c349b87b110dc3bda4f"
@@ -17045,6 +17063,21 @@ node-gyp@^8.4.1, node-gyp@^9.0.0:
tar "^6.1.2"
which "^2.0.2"
+node-html-markdown@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/node-html-markdown/-/node-html-markdown-1.3.0.tgz#ef0b19a3bbfc0f1a880abb9ff2a0c9aa6bbff2a9"
+ integrity sha512-OeFi3QwC/cPjvVKZ114tzzu+YoR+v9UXW5RwSXGUqGb0qCl0DvP406tzdL7SFn8pZrMyzXoisfG2zcuF9+zw4g==
+ dependencies:
+ node-html-parser "^6.1.1"
+
+node-html-parser@^6.1.1:
+ version "6.1.13"
+ resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-6.1.13.tgz#a1df799b83df5c6743fcd92740ba14682083b7e4"
+ integrity sha512-qIsTMOY4C/dAa5Q5vsobRpOOvPfC4pB61UVW2uSwZNUp0QU/jCekTal1vMmbO0DgdHeLUJpv/ARmDqErVxA3Sg==
+ dependencies:
+ css-select "^5.1.0"
+ he "1.2.0"
+
node-int64@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@@ -22179,11 +22212,6 @@ unicode-property-aliases-ecmascript@^2.0.0:
resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd"
integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==
-unicode-substring@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/unicode-substring/-/unicode-substring-1.0.0.tgz#659fb839078e7bee84b86c27210ac4db215bf885"
- integrity sha512-2acGIOTaqS/GWocwKdyL1Vk9MHglCss1mR0CL2o/YJTwKrAt6JbTrw4X187VkSDmFcpJ8n2i3/+gJSYEdvXJMg==
-
unique-filename@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-1.1.1.tgz#1d69769369ada0583103a1e6ae87681b56573230"