From 34ef72672423a8a2eb3ffaba78438d81ea2000b6 Mon Sep 17 00:00:00 2001 From: Artyom Savchenko Date: Wed, 3 Sep 2025 12:44:45 +0700 Subject: [PATCH] UBERF-13433: Migrate channels to threads (#9761) * UBERF-13433: Migrate from channels to threads Signed-off-by: Artem Savchenko * UBERF-13433: Migrate parent info Signed-off-by: Artem Savchenko * UBERF-13433: Use client traverse Signed-off-by: Artem Savchenko * UBERF-13433: Add tests Signed-off-by: Artem Savchenko * UBERF-13433: Support Jest in model packages Signed-off-by: Artem Savchenko * Revert "UBERF-13433: Support Jest in model packages" This reverts commit 415f5916c9c5d3400829a7fdcf931940c14ae6e9. Signed-off-by: Artem Savchenko * Revert "UBERF-13433: Add tests" This reverts commit 38d7fcd7943ead64b027a6df52bdfc7d0f6b0dc5. Signed-off-by: Artem Savchenko --------- Signed-off-by: Artem Savchenko --- common/scripts/version.txt | 2 +- models/chat/src/index.ts | 16 ---- models/chat/src/migration.ts | 86 ++++++++++++++++++- models/mail/src/index.ts | 11 --- plugins/chat-assets/assets/icons.svg | 5 -- plugins/chat-assets/src/index.ts | 1 - .../src/components/ChatNavigation.svelte | 5 +- plugins/chat/src/index.ts | 2 - plugins/mail/src/index.ts | 1 - .../src/components/General.svelte | 2 +- .../mail-common/src/__tests__/channel.test.ts | 14 +-- services/mail/mail-common/src/channel.ts | 10 +-- services/mail/mail-common/src/txHandler.ts | 4 +- .../mail/pod-mail-worker/src/mailWorker.ts | 2 +- 14 files changed, 103 insertions(+), 58 deletions(-) diff --git a/common/scripts/version.txt b/common/scripts/version.txt index f7d6d7865f..04c724932a 100644 --- a/common/scripts/version.txt +++ b/common/scripts/version.txt @@ -1 +1 @@ -"0.7.173" +"0.7.230" diff --git a/models/chat/src/index.ts b/models/chat/src/index.ts index e8bef7ea3e..fc7db551cd 100644 --- a/models/chat/src/index.ts +++ b/models/chat/src/index.ts @@ -60,22 +60,6 @@ export function createModel (builder: Builder): void { }, PaletteColorIndexes.Houseplant ) - createSystemType( - builder, - chat.masterTag.Channel, - chat.icon.Channel, - chat.string.Channel, - chat.string.Channels, - { - defaultSection: communication.ids.CardMessagesSection, - defaultNavigation: MessagesNavigationAnchors.LatestMessages - }, - PaletteColorIndexes.Blueberry - ) - - builder.mixin(chat.masterTag.Channel, core.class.Class, core.mixin.TxAccessLevel, { - updateAccessLevel: AccountRole.Guest - }) builder.mixin(chat.masterTag.Thread, core.class.Class, core.mixin.TxAccessLevel, { updateAccessLevel: AccountRole.Guest diff --git a/models/chat/src/migration.ts b/models/chat/src/migration.ts index 6d724e807d..76a89e0972 100644 --- a/models/chat/src/migration.ts +++ b/models/chat/src/migration.ts @@ -13,9 +13,91 @@ // limitations under the License. // -import { type MigrateOperation, type MigrationClient, type MigrationUpgradeClient } from '@hcengineering/model' +import card, { type Card, cardId, DOMAIN_CARD, type MasterTag } from '@hcengineering/card' +import type { Doc, Ref } from '@hcengineering/core' +import { + type MigrateOperation, + type MigrationClient, + type MigrationUpgradeClient, + type MigrateUpdate, + type MigrationDocumentQuery, + tryMigrate +} from '@hcengineering/model' +import chat from './plugin' + +const channelMasterTag = 'chat:masterTag:Channel' as Ref export const chatOperation: MigrateOperation = { - async migrate (client: MigrationClient, mode): Promise {}, + async migrate (client: MigrationClient, mode): Promise { + await tryMigrate(mode, client, cardId, [ + { + state: 'migrate-channels', + mode: 'upgrade', + func: migrateChannelsToThreads + }, + { + state: 'migrate-parent-info', + mode: 'upgrade', + func: migrateParentInfo + } + ]) + }, async upgrade (state: Map>, client: () => Promise, mode): Promise {} } + +async function migrateChannelsToThreads (client: MigrationClient): Promise { + await client.update( + DOMAIN_CARD, + { + _class: channelMasterTag + }, + { + _class: chat.masterTag.Thread + } + ) +} + +async function migrateParentInfo (client: MigrationClient): Promise { + let processedCards = 0 + const iterator = await client.traverse(DOMAIN_CARD, { _class: card.class.Card }) + try { + while (true) { + const cards = await iterator.next(1000) + if (cards === null || cards.length === 0) { + break + } + const operations: { filter: MigrationDocumentQuery, update: MigrateUpdate }[] = [] + for (const card of cards) { + if (card.parentInfo == null || card.parentInfo.length === 0) { + continue + } + const needUpdate = card.parentInfo.some((info) => info._class === channelMasterTag) + if (!needUpdate) { + continue + } + const parents = card.parentInfo.map((info) => { + if (info._class !== channelMasterTag) { + return info + } + return { + ...info, + _class: chat.masterTag.Thread + } + }) + operations.push({ + filter: { _id: card._id }, + update: { + parentInfo: parents + } + }) + } + if (operations.length > 0) { + await client.bulk(DOMAIN_CARD, operations) + } + processedCards += cards.length + client.logger.log('Migrated cards', { count: processedCards }) + } + } finally { + await iterator.close() + } +} diff --git a/models/mail/src/index.ts b/models/mail/src/index.ts index 73ad731191..474285fa5b 100644 --- a/models/mail/src/index.ts +++ b/models/mail/src/index.ts @@ -35,15 +35,4 @@ export function createModel (builder: Builder): void { }, mail.tag.MailThread ) - builder.createDoc( - card.class.Tag, - core.space.Model, - { - extends: chat.masterTag.Channel, - label: mail.string.MailTag, - kind: ClassifierKind.MIXIN, - icon: mail.icon.Mail - }, - mail.tag.MailChannel - ) } diff --git a/plugins/chat-assets/assets/icons.svg b/plugins/chat-assets/assets/icons.svg index 8d4e25ae67..1b8f2c45a4 100644 --- a/plugins/chat-assets/assets/icons.svg +++ b/plugins/chat-assets/assets/icons.svg @@ -15,9 +15,4 @@ d="M22.2646 18.4346C23.8127 15.061 21.4542 11.3919 18.3526 10.5041C16.8345 10.0007 15.1435 10.203 13.7239 10.8708C12.2987 11.5412 11.0662 12.7196 10.5459 14.2602C9.54063 17.2366 11.4209 20.8545 14.6395 21.8814C15.9617 22.3032 17.5347 22.1569 18.1927 21.9491C18.431 22.0161 18.7158 22.1372 19.0197 22.2664C19.7954 22.5963 20.6952 22.979 21.2615 22.6468C21.5475 22.479 21.6604 22.1705 21.6471 21.8546C21.6294 21.4301 21.573 21.0054 21.5167 20.5813C21.4805 20.3088 21.4443 20.0367 21.4185 19.7651C21.7475 19.356 22.0453 18.9123 22.2646 18.4346ZM11.9671 14.7402C12.3308 13.6633 13.226 12.7627 14.3623 12.2282C15.4988 11.6936 16.7959 11.564 17.8928 11.932L17.9099 11.9377L17.9273 11.9426C20.3341 12.6228 21.9859 15.4235 20.9068 17.7968C20.633 18.3376 20.2858 18.8072 19.8659 19.2409C19.9138 19.791 19.9655 20.3406 20.03 20.889C19.7776 20.8166 19.5238 20.7496 19.2701 20.6826C18.8945 20.5833 18.5189 20.4841 18.148 20.3677L17.9034 20.4622C16.9745 20.8209 15.9738 20.7129 15.0414 20.436C12.3639 19.7416 11.2065 16.9918 11.9671 14.7402Z" fill="currentColor"/> - - - diff --git a/plugins/chat-assets/src/index.ts b/plugins/chat-assets/src/index.ts index e06aa74fa0..8736e2fdf2 100644 --- a/plugins/chat-assets/src/index.ts +++ b/plugins/chat-assets/src/index.ts @@ -18,7 +18,6 @@ import chat from '@hcengineering/chat' const icons = require('../assets/icons.svg') as string // eslint-disable-line loadMetadata(chat.icon, { - Channel: `${icons}#grid`, ChatBubble: `${icons}#chat-bubble`, Thread: `${icons}#message-multiple` }) diff --git a/plugins/chat-resources/src/components/ChatNavigation.svelte b/plugins/chat-resources/src/components/ChatNavigation.svelte index 4804c6b682..4dd42bf442 100644 --- a/plugins/chat-resources/src/components/ChatNavigation.svelte +++ b/plugins/chat-resources/src/components/ChatNavigation.svelte @@ -48,10 +48,9 @@ labelFilter: [SubscriptionLabelID], preorder: [ { type: chat.masterTag.Thread, order: 1 }, - { type: chat.masterTag.Channel, order: 2 }, - { type: communication.type.Direct, order: 3 } + { type: communication.type.Direct, order: 2 } ], - fixedTypes: [chat.masterTag.Thread, chat.masterTag.Channel, communication.type.Direct], + fixedTypes: [chat.masterTag.Thread, communication.type.Direct], specialSorting: { [communication.type.Direct]: 'alphabetical' }, diff --git a/plugins/chat/src/index.ts b/plugins/chat/src/index.ts index c1b19eefb7..db59e6a68a 100644 --- a/plugins/chat/src/index.ts +++ b/plugins/chat/src/index.ts @@ -32,12 +32,10 @@ const chat = plugin(chatId, { Title: '' as IntlString }, icon: { - Channel: '' as Asset, ChatBubble: '' as Asset, Thread: '' as Asset }, masterTag: { - Channel: '' as Ref, Thread: '' as Ref } }) diff --git a/plugins/mail/src/index.ts b/plugins/mail/src/index.ts index 7207ea5496..cfff6ca48b 100644 --- a/plugins/mail/src/index.ts +++ b/plugins/mail/src/index.ts @@ -24,7 +24,6 @@ export const mailId = 'mail' as Plugin export default plugin(mailId, { tag: { - MailChannel: '' as Ref, MailThread: '' as Ref }, string: { diff --git a/plugins/setting-resources/src/components/General.svelte b/plugins/setting-resources/src/components/General.svelte index df2c48ba2b..5b18922560 100644 --- a/plugins/setting-resources/src/components/General.svelte +++ b/plugins/setting-resources/src/components/General.svelte @@ -353,7 +353,7 @@ { const result = await channelCache.getOrCreateChannel(spaceId, participants, emailAccount, personId) expect(result).toBe(mockChannel._id) - expect(mockClient.findOne).toHaveBeenCalledWith(mail.tag.MailChannel, { title: emailAccount }) + expect(mockClient.findOne).toHaveBeenCalledWith(mail.tag.MailThread, { title: emailAccount }) expect(mockClient.createDoc).not.toHaveBeenCalled() expect(mockCtx.info).toHaveBeenCalledWith('Using existing channel', { me: emailAccount, @@ -95,7 +95,7 @@ describe('ChannelCache', () => { expect(result).toBe(generatedId) expect(mockClient.findOne).toHaveBeenCalledTimes(2) expect(mockClient.createDoc).toHaveBeenCalledWith( - chat.masterTag.Channel, + chat.masterTag.Thread, spaceId, { title: emailAccount, @@ -111,9 +111,9 @@ describe('ChannelCache', () => { ) expect(mockClient.createMixin).toHaveBeenCalledWith( expect.any(String), - chat.masterTag.Channel, + chat.masterTag.Thread, spaceId, - mail.tag.MailChannel, + mail.tag.MailThread, {}, expect.any(Number), personId @@ -184,8 +184,8 @@ describe('ChannelCache', () => { const channelId2 = await channelCache.getOrCreateChannel(spaceId, participants, upperCaseEmail, personId) // Assert - expect(mockClient.findOne).toHaveBeenNthCalledWith(1, mail.tag.MailChannel, { title: lowerCaseEmail }) - expect(mockClient.findOne).toHaveBeenNthCalledWith(2, mail.tag.MailChannel, { title: lowerCaseEmail }) + expect(mockClient.findOne).toHaveBeenNthCalledWith(1, mail.tag.MailThread, { title: lowerCaseEmail }) + expect(mockClient.findOne).toHaveBeenNthCalledWith(2, mail.tag.MailThread, { title: lowerCaseEmail }) // Should only create doc once expect(mockClient.createDoc).toHaveBeenCalledTimes(1) @@ -227,7 +227,7 @@ describe('ChannelCache', () => { await channelCache.getOrCreateChannel(spaceId, participants, mixedCaseEmail, personId) // Assert - If email normalization is implemented, this would pass - expect(mockClient.findOne).toHaveBeenCalledWith(mail.tag.MailChannel, { + expect(mockClient.findOne).toHaveBeenCalledWith(mail.tag.MailThread, { title: expect.stringMatching(/mixed@example\.com/i) }) }) diff --git a/services/mail/mail-common/src/channel.ts b/services/mail/mail-common/src/channel.ts index e79afdef08..bc81ff8130 100644 --- a/services/mail/mail-common/src/channel.ts +++ b/services/mail/mail-common/src/channel.ts @@ -82,7 +82,7 @@ export class ChannelCache { const normalizedEmail = normalizeEmail(email) try { // First try to find existing channel - const channel = await this.client.findOne(mail.tag.MailChannel, { title: normalizedEmail }) + const channel = await this.client.findOne(mail.tag.MailThread, { title: normalizedEmail }) if (channel != null) { this.ctx.info('Using existing channel', { me: normalizedEmail, space, channel: channel._id }) @@ -119,7 +119,7 @@ export class ChannelCache { try { // Double-check that channel doesn't exist after acquiring lock - const existingChannel = await this.client.findOne(mail.tag.MailChannel, { title: normalizedEmail }) + const existingChannel = await this.client.findOne(mail.tag.MailThread, { title: normalizedEmail }) if (existingChannel != null) { this.ctx.info('Using existing channel (found after mutex lock)', { me: normalizedEmail, @@ -132,7 +132,7 @@ export class ChannelCache { // Create new channel if it doesn't exist this.ctx.info('Creating new channel', { me: normalizedEmail, space, personId }) const channelId = await this.client.createDoc( - chat.masterTag.Channel, + chat.masterTag.Thread, space, { title: normalizedEmail, @@ -150,9 +150,9 @@ export class ChannelCache { this.ctx.info('Creating mixin', { me: normalizedEmail, space, personId, channelId }) await this.client.createMixin( channelId, - chat.masterTag.Channel, + chat.masterTag.Thread, space, - mail.tag.MailChannel, + mail.tag.MailThread, {}, Date.now() + MessageTimeShift.MailTag, personId diff --git a/services/mail/mail-common/src/txHandler.ts b/services/mail/mail-common/src/txHandler.ts index 9fba73c567..112f2015aa 100644 --- a/services/mail/mail-common/src/txHandler.ts +++ b/services/mail/mail-common/src/txHandler.ts @@ -56,12 +56,12 @@ export function isNewChannelTx (tx: Tx): boolean { return false } const createTx = tx as TxCreateDoc - return createTx.objectClass === chat.masterTag.Channel + return createTx.objectClass === chat.masterTag.Thread } export async function getChannel (client: TxOperations, email: string): Promise { const normalizedEmail = normalizeEmail(email) - return await client.findOne(mail.tag.MailChannel, { title: normalizedEmail }) + return await client.findOne(mail.tag.MailThread, { title: normalizedEmail }) } export async function getRecipients ( diff --git a/services/mail/pod-mail-worker/src/mailWorker.ts b/services/mail/pod-mail-worker/src/mailWorker.ts index 1104c51be6..675dec9a11 100644 --- a/services/mail/pod-mail-worker/src/mailWorker.ts +++ b/services/mail/pod-mail-worker/src/mailWorker.ts @@ -213,7 +213,7 @@ export class MailWorker { if (thread?.parent == null) { return } - const channel = await workspaceClient.findOne(chat.masterTag.Channel, { _id: thread.parent }) + const channel = await workspaceClient.findOne(chat.masterTag.Thread, { _id: thread.parent }) if (channel === undefined || !this.isHulyMailChannel(channel)) { return }