diff --git a/packages/server/graphql/public/types/Comment.ts b/packages/server/graphql/public/types/Comment.ts index e1fa01a6b78..2d654dc414a 100644 --- a/packages/server/graphql/public/types/Comment.ts +++ b/packages/server/graphql/public/types/Comment.ts @@ -1,7 +1,5 @@ import {convertTipTapTaskContent} from '../../../../client/shared/tiptap/convertTipTapTaskContent' -import {isDraftJSContent} from '../../../../client/shared/tiptap/isDraftJSContent' import {getUserId} from '../../../utils/authorization' -import {convertKnownDraftToTipTap} from '../../../utils/convertToTipTap' import resolveReactjis from '../../resolvers/resolveReactjis' import {CommentResolvers} from '../resolverTypes' @@ -10,8 +8,7 @@ const TOMBSTONE = convertTipTapTaskContent('[deleted]') const Comment: CommentResolvers = { content: ({isActive, content}) => { if (!isActive) return TOMBSTONE - const validContent = isDraftJSContent(content) ? convertKnownDraftToTipTap(content) : content - return JSON.stringify(validContent) + return JSON.stringify(content) }, createdBy: ({createdBy, isAnonymous}) => { diff --git a/packages/server/postgres/migrations/2024-12-13T23:03:51.369Z_commentsToTipTap.ts b/packages/server/postgres/migrations/2024-12-13T23:03:51.369Z_commentsToTipTap.ts new file mode 100644 index 00000000000..01e324b92f9 --- /dev/null +++ b/packages/server/postgres/migrations/2024-12-13T23:03:51.369Z_commentsToTipTap.ts @@ -0,0 +1,127 @@ +import {mergeAttributes} from '@tiptap/core' +import BaseLink from '@tiptap/extension-link' +import Mention from '@tiptap/extension-mention' +import {generateJSON} from '@tiptap/html' +import StarterKit from '@tiptap/starter-kit' +import {convertFromRaw, RawDraftContentState} from 'draft-js' +import {Options, stateToHTML} from 'draft-js-export-html' +import type {Kysely} from 'kysely' + +export const serverTipTapExtensions = [ + StarterKit, + Mention.configure({ + renderText({node}) { + return node.attrs.label + }, + renderHTML({options, node}) { + return ['span', options.HTMLAttributes, `${node.attrs.label ?? node.attrs.id}`] + } + }), + Mention.extend({name: 'taskTag'}).configure({ + renderHTML({options, node}) { + return ['span', options.HTMLAttributes, `#${node.attrs.id}`] + } + }), + 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] + } + }) +] + +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) + } + console.log('found unknown for', userId, JSON.stringify(content)) + return 'Unknown User' +} + +export const convertKnownDraftToTipTap = (content: RawDraftContentState) => { + const contentState = convertFromRaw(content) + 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' + } + } + } + if (entityType === 'mention') { + const label = getNameFromEntity(content, data.userId) + return { + element: 'span', + attributes: { + 'data-id': data.userId.toWellFormed(), + 'data-label': label.toWellFormed(), + 'data-type': 'mention' + } + } + } + return + } + } + const html = stateToHTML(contentState, options) + const json = generateJSON(html, serverTipTapExtensions) + return json +} + +export async function up(db: Kysely): Promise { + let lastId = '' + + for (let i = 0; i < 1e6; i++) { + const comments = await db + .selectFrom('Comment') + .select(['id', 'content']) + .where('id', '>', lastId) + .orderBy('id asc') + .limit(1000) + .execute() + console.log('converting comments', i * 1000) + if (comments.length === 0) break + const updatePromises = [] as Promise[] + for (const task of comments) { + const {id, content} = task + if ('blocks' in content) { + // this is draftjs + const tipTapContent = convertKnownDraftToTipTap(content) + const contentStr = JSON.stringify(tipTapContent) + const doPromise = async () => { + try { + return await db + .updateTable('Comment') + .set({content: contentStr}) + .where('id', '=', id) + .execute() + } catch (e) { + console.log('GOT ERR', id, contentStr, e) + throw e + } + } + updatePromises.push(doPromise()) + } + } + await Promise.all(updatePromises) + lastId = comments.at(-1)!.id + } +} + +export async function down(db: Kysely): Promise {}