From e8bedffa6bba45d3af774f2c80849aded53c2e67 Mon Sep 17 00:00:00 2001 From: Matt Krick Date: Tue, 26 Nov 2024 18:43:43 -0800 Subject: [PATCH] chore: convert legacy tasks to TipTap Signed-off-by: Matt Krick --- packages/server/graphql/public/types/Task.ts | 26 ---- ...11-27T00:50:24.390Z_taskContentToTipTap.ts | 129 ++++++++++++++++++ 2 files changed, 129 insertions(+), 26 deletions(-) create mode 100644 packages/server/postgres/migrations/2024-11-27T00:50:24.390Z_taskContentToTipTap.ts diff --git a/packages/server/graphql/public/types/Task.ts b/packages/server/graphql/public/types/Task.ts index e8868952397..645ea20187b 100644 --- a/packages/server/graphql/public/types/Task.ts +++ b/packages/server/graphql/public/types/Task.ts @@ -1,14 +1,12 @@ 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' @@ -30,30 +28,6 @@ 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/postgres/migrations/2024-11-27T00:50:24.390Z_taskContentToTipTap.ts b/packages/server/postgres/migrations/2024-11-27T00:50:24.390Z_taskContentToTipTap.ts new file mode 100644 index 00000000000..527d4ec2c28 --- /dev/null +++ b/packages/server/postgres/migrations/2024-11-27T00:50:24.390Z_taskContentToTipTap.ts @@ -0,0 +1,129 @@ +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 tasks = await db + .selectFrom('Task') + .select(['id', 'content']) + .where('id', '>', lastId) + .orderBy('id asc') + .limit(1000) + .execute() + console.log('converting tasks', i * 1000) + if (tasks.length === 0) break + const updatePromises = [] as Promise[] + for (const task of tasks) { + 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('Task') + .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 = tasks.at(-1)!.id + } +} + +export async function down(): Promise { + // noop +}