UBERF-9724: Use updated accounts (#8452)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2025-04-14 09:36:20 +07:00
committed by GitHub
parent 143f0bc7b6
commit 5ca2a73fba
28 changed files with 1506 additions and 1153 deletions
+324 -326
View File
@@ -10,14 +10,15 @@
import activity from '@hcengineering/activity'
import { Analytics } from '@hcengineering/analytics'
import { CollaboratorClient } from '@hcengineering/collaborator-client'
import contact, { Person } from '@hcengineering/contact'
import core, {
PersonId,
AttachedDoc,
Class,
Doc,
DocumentUpdate,
Markup,
MeasureContext,
PersonId,
Ref,
Space,
Status,
@@ -29,7 +30,6 @@ import github, {
GithubFieldMapping,
GithubIntegrationRepository,
GithubIssue,
GithubIssue as GithubIssueP,
GithubMilestone,
GithubProject
} from '@hcengineering/github'
@@ -138,19 +138,22 @@ export abstract class IssueSyncManagerBase {
this.provider = provider
}
async getAssignees (issue: IssueExternalData): Promise<any[]> {
// TODO: FIXME
throw new Error('Not implemented')
async getAssignees (issue: IssueExternalData): Promise<Ref<Person>[]> {
// Find Assignees and reviewers
// const assignees: PersonAccount[] = []
const assignees: PersonId[] = []
// for (const o of issue.assignees.nodes) {
// const acc = await this.provider.getAccount(o)
// if (acc !== undefined) {
// assignees.push(acc)
// }
// }
// return assignees
for (const o of issue.assignees.nodes) {
const acc = await this.provider.getAccount(o)
if (acc !== undefined) {
assignees.push(acc)
}
}
return await this.getPersonsFromId(assignees)
}
async getPersonsFromId (assignees: PersonId[]): Promise<Ref<Person>[]> {
const socialIds = await this.client.findAll(contact.class.SocialIdentity, { _id: { $in: assignees as any } })
return socialIds.map((it) => it.attachedTo)
}
async processProjectV2Event (
@@ -159,7 +162,7 @@ export abstract class IssueSyncManagerBase {
derivedClient: TxOperations,
prj: GithubProject
): Promise<void> {
const account = (await this.provider.getAccountU(event.sender))?._id ?? core.account.System
const account = (await this.provider.getAccountU(event.sender)) ?? core.account.System
switch (event.action) {
case 'edited': {
const itemId = event.projects_v2_item.node_id
@@ -708,263 +711,261 @@ export abstract class IssueSyncManagerBase {
accountGH: PersonId,
syncToProject: boolean
): Promise<DocumentUpdate<DocSyncInfo>> {
// TODO: FIXME
throw new Error('Not implemented')
// let needUpdate = false
// if (!this.client.getHierarchy().hasMixin(existing, github.mixin.GithubIssue)) {
// await this.ctx.withLog(
// 'create mixin issue: GithubIssue',
// {},
// async () => {
// await this.client.createMixin<Issue, GithubIssueP>(
// existing._id as Ref<GithubIssueP>,
// existing._class,
// existing.space,
// github.mixin.GithubIssue,
// {
// githubNumber: issueExternal.number,
// url: issueExternal.url,
// repository: info.repository as Ref<GithubIntegrationRepository>
// }
// )
// await this.notifyConnected(container, info, existing, issueExternal)
// },
// { identifier: existing.identifier, url: issueExternal.url }
// )
// // Re iterate to have existing value with mixin inside.
// needUpdate = true
// } else {
// const ghIssue = this.client.getHierarchy().as(existing, github.mixin.GithubIssue)
// await this.client.diffUpdate(ghIssue, {
// githubNumber: issueExternal.number,
// url: issueExternal.url,
// repository: info.repository as Ref<GithubIntegrationRepository>
// })
// if (ghIssue.url !== issueExternal.url) {
// await this.notifyConnected(container, info, existing, issueExternal)
// }
// }
// if (!this.client.getHierarchy().hasMixin(existing, container.project.mixinClass)) {
// await this.ctx.withLog(
// 'create mixin issue',
// {},
// () =>
// this.client.createMixin<Issue, Issue>(
// existing._id as Ref<GithubIssueP>,
// existing._class,
// existing.space,
// container.project.mixinClass,
// {}
// ),
// { identifier: existing.identifier, url: issueExternal.url }
// )
// // Re iterate to have existing value with mixin inside.
// needUpdate = true
// }
// if (needUpdate) {
// return { needSync: '' }
// }
let needUpdate = false
if (!this.client.getHierarchy().hasMixin(existing, github.mixin.GithubIssue)) {
await this.ctx.withLog(
'create mixin issue: GithubIssue',
{},
async () => {
await this.client.createMixin<Issue, GithubIssue>(
existing._id as Ref<GithubIssue>,
existing._class,
existing.space,
github.mixin.GithubIssue,
{
githubNumber: issueExternal.number,
url: issueExternal.url,
repository: info.repository as Ref<GithubIntegrationRepository>
}
)
await this.notifyConnected(container, info, existing, issueExternal)
},
{ identifier: existing.identifier, url: issueExternal.url }
)
// Re iterate to have existing value with mixin inside.
needUpdate = true
} else {
const ghIssue = this.client.getHierarchy().as(existing, github.mixin.GithubIssue)
await this.client.diffUpdate(ghIssue, {
githubNumber: issueExternal.number,
url: issueExternal.url,
repository: info.repository as Ref<GithubIntegrationRepository>
})
if (ghIssue.url !== issueExternal.url) {
await this.notifyConnected(container, info, existing, issueExternal)
}
}
if (!this.client.getHierarchy().hasMixin(existing, container.project.mixinClass)) {
await this.ctx.withLog(
'create mixin issue',
{},
() =>
this.client.createMixin<Issue, Issue>(
existing._id as Ref<GithubIssue>,
existing._class,
existing.space,
container.project.mixinClass,
{}
),
{ identifier: existing.identifier, url: issueExternal.url }
)
// Re iterate to have existing value with mixin inside.
needUpdate = true
}
if (needUpdate) {
return { needSync: '' }
}
// const existingIssue = this.client.getHierarchy().as(existing, container.project.mixinClass)
// const previousData: GithubIssueData = info.current ?? ({} as unknown as GithubIssueData)
// const type = await this.provider.getTaskTypeOf(container.project.type, existing._class)
// const stst = await this.provider.getStatuses(type?._id)
const existingIssue = this.client.getHierarchy().as(existing, container.project.mixinClass)
const previousData: GithubIssueData = info.current ?? ({} as unknown as GithubIssueData)
const type = await this.provider.getTaskTypeOf(container.project.type, existing._class)
const stst = await this.provider.getStatuses(type?._id)
// const update = collectUpdate<Issue>(previousData, issueData, Object.keys(issueData))
const update = collectUpdate<Issue>(previousData, issueData, Object.keys(issueData))
// const allAttributes = this.client.getHierarchy().getAllAttributes(container.project.mixinClass)
// const platformUpdate = collectUpdate<Issue>(previousData, existingIssue, Array.from(allAttributes.keys()))
const allAttributes = this.client.getHierarchy().getAllAttributes(container.project.mixinClass)
const platformUpdate = collectUpdate<Issue>(previousData, existingIssue, Array.from(allAttributes.keys()))
// const okit = (await this.provider.getOctokit(account as PersonId)) ?? container.container.octokit
const okit = (await this.provider.getOctokit(account)) ?? container.container.octokit
// // Remove current same values from update
// for (const [k, v] of Object.entries(update)) {
// if ((existingIssue as any)[k] === v) {
// // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
// delete (update as any)[k]
// }
// }
// Remove current same values from update
for (const [k, v] of Object.entries(update)) {
if ((existingIssue as any)[k] === v) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (update as any)[k]
}
}
// if (update.description !== undefined) {
// if (update.description === existingIssue.description) {
// delete update.description
// }
// }
if (update.description !== undefined) {
if (update.description === existingIssue.description) {
delete update.description
}
}
// for (const [k, v] of Object.entries(update)) {
// let pv = (platformUpdate as any)[k]
for (const [k, v] of Object.entries(update)) {
let pv = (platformUpdate as any)[k]
// if (k === 'description' && pv != null) {
// const mdown = await this.provider.getMarkdown(pv)
// pv = await this.provider.getMarkupSafe(container.container, mdown, this.stripGuestLink)
// }
// if (pv != null && pv !== v) {
// // We have conflict of values, assume platform is more proper one.
// this.ctx.error('conflict', { id: existing.identifier, k })
// // Assume platform change is more important in case of conflict values.
// // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
// delete (update as any)[k]
// continue
// }
// }
if (k === 'description' && pv != null) {
const mdown = await this.provider.getMarkdown(pv)
pv = await this.provider.getMarkupSafe(container.container, mdown, this.stripGuestLink)
}
if (pv != null && pv !== v) {
// We have conflict of values, assume platform is more proper one.
this.ctx.error('conflict', { id: existing.identifier, k })
// Assume platform change is more important in case of conflict values.
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (update as any)[k]
continue
}
}
// await this.fillBackChanges(update, existingIssue, issueExternal)
await this.fillBackChanges(update, existingIssue, issueExternal)
// let needExternalSync = false
let needExternalSync = false
// if (container !== undefined && okit !== undefined) {
// // Check and update issue fields.
// needExternalSync = await this.performIssueFieldsUpdate(
// info,
// existing,
// platformUpdate,
// issueData,
// container,
// issueExternal,
// okit,
// account
// )
if (container !== undefined && okit !== undefined) {
// Check and update issue fields.
needExternalSync = await this.performIssueFieldsUpdate(
info,
existing,
platformUpdate,
issueData,
container,
issueExternal,
okit,
account
)
// const fieldsUpdate: { id: string, value: any, dataType: GithubDataType }[] = []
const fieldsUpdate: { id: string, value: any, dataType: GithubDataType }[] = []
// // Collect field update.
// for (const [k, v] of Object.entries(platformUpdate)) {
// const mapping = target.mappings.filter((it) => it != null).find((it) => it.name === k)
// if (mapping === undefined) {
// continue
// }
// const attr = this.client.getHierarchy().getAttribute(mapping._class, mapping.name)
// Collect field update.
for (const [k, v] of Object.entries(platformUpdate)) {
const mapping = target.mappings.filter((it) => it != null).find((it) => it.name === k)
if (mapping === undefined) {
continue
}
const attr = this.client.getHierarchy().getAttribute(mapping._class, mapping.name)
// if (attr.name === 'status') {
// // Handle status field
// const status = stst.find((it) => it._id === v) as Status
// const optionId = this.findOptionId(container, mapping.githubId, status.name, target)
// if (optionId !== undefined) {
// fieldsUpdate.push({
// id: mapping.githubId,
// dataType: 'SINGLE_SELECT',
// value: optionId
// })
// this.ctx.info(' => prepare issue status update', {
// url: issueExternal.url,
// name: status.name,
// workspace: this.provider.getWorkspaceId()
// })
// continue
// }
// }
// if (attr.name === 'priority') {
// const values: Record<IssuePriority, string> = {
// [IssuePriority.NoPriority]: '',
// [IssuePriority.High]: 'High',
// [IssuePriority.Medium]: 'Medium',
// [IssuePriority.Low]: 'Low',
// [IssuePriority.Urgent]: 'Urgent'
// }
// // Handle priority field TODO: Add clear of field
// const priorityName = values[v as IssuePriority]
// const optionId = this.findOptionId(container, mapping.githubId, priorityName, target)
// if (optionId !== undefined) {
// fieldsUpdate.push({
// id: mapping.githubId,
// dataType: 'SINGLE_SELECT',
// value: optionId
// })
// this.ctx.info(' => prepare issue priority update', {
// url: issueExternal.url,
// priority: priorityName,
// workspace: this.provider.getWorkspaceId()
// })
// continue
// }
// }
if (attr.name === 'status') {
// Handle status field
const status = stst.find((it) => it._id === v) as Status
const optionId = this.findOptionId(container, mapping.githubId, status.name, target)
if (optionId !== undefined) {
fieldsUpdate.push({
id: mapping.githubId,
dataType: 'SINGLE_SELECT',
value: optionId
})
this.ctx.info(' => prepare issue status update', {
url: issueExternal.url,
name: status.name,
workspace: this.provider.getWorkspaceId()
})
continue
}
}
if (attr.name === 'priority') {
const values: Record<IssuePriority, string> = {
[IssuePriority.NoPriority]: '',
[IssuePriority.High]: 'High',
[IssuePriority.Medium]: 'Medium',
[IssuePriority.Low]: 'Low',
[IssuePriority.Urgent]: 'Urgent'
}
// Handle priority field TODO: Add clear of field
const priorityName = values[v as IssuePriority]
const optionId = this.findOptionId(container, mapping.githubId, priorityName, target)
if (optionId !== undefined) {
fieldsUpdate.push({
id: mapping.githubId,
dataType: 'SINGLE_SELECT',
value: optionId
})
this.ctx.info(' => prepare issue priority update', {
url: issueExternal.url,
priority: priorityName,
workspace: this.provider.getWorkspaceId()
})
continue
}
}
// const dataType = getType(attr)
// if (dataType === 'SINGLE_SELECT') {
// // Handle status field
// const optionId = this.findOptionId(container, mapping.githubId, v, target)
// if (optionId !== undefined) {
// fieldsUpdate.push({
// id: mapping.githubId,
// dataType: 'SINGLE_SELECT',
// value: optionId
// })
// this.ctx.info(` => prepare issue field ${attr.label} update`, {
// url: issueExternal.url,
// value: v,
// workspace: this.provider.getWorkspaceId()
// })
// continue
// }
// }
const dataType = getType(attr)
if (dataType === 'SINGLE_SELECT') {
// Handle status field
const optionId = this.findOptionId(container, mapping.githubId, v, target)
if (optionId !== undefined) {
fieldsUpdate.push({
id: mapping.githubId,
dataType: 'SINGLE_SELECT',
value: optionId
})
this.ctx.info(` => prepare issue field ${attr.label} update`, {
url: issueExternal.url,
value: v,
workspace: this.provider.getWorkspaceId()
})
continue
}
}
// if (dataType === undefined) {
// continue
// }
// fieldsUpdate.push({
// id: mapping.githubId,
// dataType,
// value: v
// })
// this.ctx.info(`=> prepare issue field ${attr.label} update`, {
// url: issueExternal.url,
// value: v,
// workspace: this.provider.getWorkspaceId()
// })
// }
// if (fieldsUpdate.length > 0 && syncToProject && target.prjData !== undefined) {
// const errors = await this.updateIssueValues(target, okit, fieldsUpdate)
// if (errors.length === 0) {
// needExternalSync = true
// }
// }
// // TODO: Add support for labels, milestone, assignees
// }
if (dataType === undefined) {
continue
}
fieldsUpdate.push({
id: mapping.githubId,
dataType,
value: v
})
this.ctx.info(`=> prepare issue field ${attr.label} update`, {
url: issueExternal.url,
value: v,
workspace: this.provider.getWorkspaceId()
})
}
if (fieldsUpdate.length > 0 && syncToProject && target.prjData !== undefined) {
const errors = await this.updateIssueValues(target, okit, fieldsUpdate)
if (errors.length === 0) {
needExternalSync = true
}
}
// TODO: Add support for labels, milestone, assignees
}
// // We need remove all readonly field values
// for (const k of Object.keys(update)) {
// // Skip readonly fields
// const attr = this.client.getHierarchy().findAttribute(target.project.mixinClass, k)
// if (attr?.readonly === true) {
// // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
// delete (update as any)[k]
// continue
// }
// }
// We need remove all readonly field values
for (const k of Object.keys(update)) {
// Skip readonly fields
const attr = this.client.getHierarchy().findAttribute(target.project.mixinClass, k)
if (attr?.readonly === true) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete (update as any)[k]
continue
}
}
// // Update collaborative description
// if (update.description !== undefined) {
// this.ctx.info(`<= perform ${issueExternal.url} update to collaborator`, {
// workspace: this.provider.getWorkspaceId()
// })
// try {
// const description = update.description as Markup
// issueData.description = description
// const collabId = makeDocCollabId(existingIssue, 'description')
// await this.collaborator.updateMarkup(collabId, description)
// } catch (err: any) {
// Analytics.handleError(err)
// this.ctx.error('error during description update', err)
// }
// }
// Update collaborative description
if (update.description !== undefined) {
this.ctx.info(`<= perform ${issueExternal.url} update to collaborator`, {
workspace: this.provider.getWorkspaceId()
})
try {
const description = update.description as Markup
issueData.description = description
const collabId = makeDocCollabId(existingIssue, 'description')
await this.collaborator.updateMarkup(collabId, description)
} catch (err: any) {
Analytics.handleError(err)
this.ctx.error('error during description update', err)
}
}
// if (Object.keys(update).length > 0) {
// // We have some fields to update of existing from external
// this.ctx.info(`<= perform ${issueExternal.url} update to platform`, {
// ...update,
// workspace: this.provider.getWorkspaceId()
// })
// await this.client.update(existingIssue, update, false, new Date().getTime(), accountGH)
// }
if (Object.keys(update).length > 0) {
// We have some fields to update of existing from external
this.ctx.info(`<= perform ${issueExternal.url} update to platform`, {
...update,
workspace: this.provider.getWorkspaceId()
})
await this.client.update(existingIssue, update, false, new Date().getTime(), accountGH)
}
// await this.afterSync(existingIssue, accountGH, issueExternal, info)
// // We need to trigger external version retrieval, via sync or event, to prevent move sync operations from platform before we will be sure all is updated on github.
// return {
// current: issueData,
// needSync: githubSyncVersion,
// ...(needExternalSync ? { externalVersion: '' } : {}),
// lastGithubUser: null
// }
await this.afterSync(existingIssue, accountGH, issueExternal, info)
// We need to trigger external version retrieval, via sync or event, to prevent move sync operations from platform before we will be sure all is updated on github.
return {
current: issueData,
needSync: githubSyncVersion,
...(needExternalSync ? { externalVersion: '' } : {}),
lastGithubUser: null
}
}
private async notifyConnected (
@@ -997,83 +998,81 @@ export abstract class IssueSyncManagerBase {
issueExternal: IssueExternalData,
_class: Ref<Class<Issue>>
): Promise<Record<string, any>> {
// TODO: FIXME
throw new Error('Not implemented')
// const issueUpdate: {
// title?: string
// body?: string
// stateReason?: string
// assigneeIds?: string[]
// } & Record<string, any> = {}
// if (platformUpdate.title != null) {
// if (platformUpdate.title !== issueExternal.title) {
// issueUpdate.title = platformUpdate.title
// }
// issueData.title = platformUpdate.title
// }
// if (platformUpdate.description != null) {
// // Need to convert to markdown
// issueUpdate.body = await this.provider.getMarkdown(platformUpdate.description ?? '')
// issueData.description = await this.provider.getMarkupSafe(
// container.container,
// issueUpdate.body ?? '',
// this.stripGuestLink
// )
const issueUpdate: {
title?: string
body?: string
stateReason?: string
assigneeIds?: string[]
} & Record<string, any> = {}
if (platformUpdate.title != null) {
if (platformUpdate.title !== issueExternal.title) {
issueUpdate.title = platformUpdate.title
}
issueData.title = platformUpdate.title
}
if (platformUpdate.description != null) {
// Need to convert to markdown
issueUpdate.body = await this.provider.getMarkdown(platformUpdate.description ?? '')
issueData.description = await this.provider.getMarkupSafe(
container.container,
issueUpdate.body ?? '',
this.stripGuestLink
)
// // Of value is same, not need to update.
// if (compareMarkdown(issueUpdate.body, issueExternal.body)) {
// delete issueUpdate.body
// }
// }
// if (platformUpdate.assignee !== undefined) {
// const info =
// platformUpdate.assignee !== null
// ? await this.provider.getGithubLogin(container.container, platformUpdate.assignee)
// : undefined
// // Check external
// Of value is same, not need to update.
if (compareMarkdown(issueUpdate.body, issueExternal.body)) {
delete issueUpdate.body
}
}
if (platformUpdate.assignee !== undefined) {
const info =
platformUpdate.assignee !== null
? await this.provider.getGithubLogin(container.container, platformUpdate.assignee)
: undefined
// Check external
// const currentAssignees = issueExternal.assignees.nodes.map((it) => it.id)
// currentAssignees.sort((a, b) => a.localeCompare(b))
const currentAssignees = issueExternal.assignees.nodes.map((it) => it.id)
currentAssignees.sort((a, b) => a.localeCompare(b))
// issueUpdate.assigneeIds = info !== undefined ? [info.id] : []
// issueUpdate.assigneeIds.sort((a, b) => a.localeCompare(b))
issueUpdate.assigneeIds = info !== undefined ? [info.id] : []
issueUpdate.assigneeIds.sort((a, b) => a.localeCompare(b))
// if (deepEqual(currentAssignees, issueUpdate.assigneeIds)) {
// // Same ids
// delete issueUpdate.assigneeIds
// }
// issueData.assignee = platformUpdate.assignee
// }
if (deepEqual(currentAssignees, issueUpdate.assigneeIds)) {
// Same ids
delete issueUpdate.assigneeIds
}
issueData.assignee = platformUpdate.assignee
}
// const status = platformUpdate.status ?? issueData.status
// const type = await this.provider.getTaskTypeOf(container.project.type, _class)
// const statuses = await this.provider.getStatuses(type?._id)
// const st = statuses.find((it) => it._id === status)
// if (st !== undefined) {
// // Need to convert to two operations.
// switch (st.category) {
// case task.statusCategory.UnStarted:
// case task.statusCategory.ToDo:
// case task.statusCategory.Active:
// if (issueExternal.state !== 'OPEN') {
// issueUpdate.state = 'OPEN'
// }
// break
// case task.statusCategory.Won:
// if (issueExternal.state !== 'CLOSED' || issueExternal.stateReason !== 'COMPLETED') {
// issueUpdate.state = 'CLOSED'
// issueUpdate.stateReason = 'COMPLETED'
// }
// break
// case task.statusCategory.Lost:
// if (issueExternal.state !== 'CLOSED' || issueExternal.stateReason !== 'NOT_PLANNED') {
// issueUpdate.state = 'CLOSED'
// issueUpdate.stateReason = 'not_planed' // Not supported change to github
// }
// break
// }
// }
// return issueUpdate
const status = platformUpdate.status ?? issueData.status
const type = await this.provider.getTaskTypeOf(container.project.type, _class)
const statuses = await this.provider.getStatuses(type?._id)
const st = statuses.find((it) => it._id === status)
if (st !== undefined) {
// Need to convert to two operations.
switch (st.category) {
case task.statusCategory.UnStarted:
case task.statusCategory.ToDo:
case task.statusCategory.Active:
if (issueExternal.state !== 'OPEN') {
issueUpdate.state = 'OPEN'
}
break
case task.statusCategory.Won:
if (issueExternal.state !== 'CLOSED' || issueExternal.stateReason !== 'COMPLETED') {
issueUpdate.state = 'CLOSED'
issueUpdate.stateReason = 'COMPLETED'
}
break
case task.statusCategory.Lost:
if (issueExternal.state !== 'CLOSED' || issueExternal.stateReason !== 'NOT_PLANNED') {
issueUpdate.state = 'CLOSED'
issueUpdate.stateReason = 'not_planed' // Not supported change to github
}
break
}
}
return issueUpdate
}
async syncIssues (
@@ -1219,9 +1218,8 @@ export abstract class IssueSyncManagerBase {
// No external issue yet, safe delete, since platform document will be deleted a well.
return true
}
const account =
existing?.createdBy ?? (await this.provider.getAccount(issueExternal.author))?._id ?? core.account.System
const okit = (await this.provider.getOctokit(account as PersonId)) ?? container.container.octokit
const account = existing?.createdBy ?? (await this.provider.getAccount(issueExternal.author)) ?? core.account.System
const okit = (await this.provider.getOctokit(account)) ?? container.container.octokit
if (existing !== undefined && issueExternal !== undefined) {
let target = await this.getMilestoneIssueTarget(