Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

chore: convert legacy tasks to TipTap #10533

Merged
merged 1 commit into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 0 additions & 26 deletions packages/server/graphql/public/types/Task.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -30,30 +28,6 @@ const Task: Omit<ReqResolvers<'Task'>, '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)
},
Expand Down
Original file line number Diff line number Diff line change
@@ -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<any>): Promise<void> {
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<any>[]
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<void> {
// noop
}