UBERF-13433: Migrate channels to threads (#9761)

* UBERF-13433: Migrate from channels to threads

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-13433: Migrate parent info

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-13433: Use client traverse

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-13433: Add tests

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* UBERF-13433: Support Jest in model packages

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Revert "UBERF-13433: Support Jest in model packages"

This reverts commit 415f5916c9.

Signed-off-by: Artem Savchenko <armisav@gmail.com>

* Revert "UBERF-13433: Add tests"

This reverts commit 38d7fcd794.

Signed-off-by: Artem Savchenko <armisav@gmail.com>

---------

Signed-off-by: Artem Savchenko <armisav@gmail.com>
This commit is contained in:
Artyom Savchenko
2025-09-03 12:44:45 +07:00
committed by GitHub
parent f0dd63e8a0
commit 34ef726724
14 changed files with 103 additions and 58 deletions
+1 -1
View File
@@ -1 +1 @@
"0.7.173"
"0.7.230"
-16
View File
@@ -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
+84 -2
View File
@@ -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<MasterTag>
export const chatOperation: MigrateOperation = {
async migrate (client: MigrationClient, mode): Promise<void> {},
async migrate (client: MigrationClient, mode): Promise<void> {
await tryMigrate(mode, client, cardId, [
{
state: 'migrate-channels',
mode: 'upgrade',
func: migrateChannelsToThreads
},
{
state: 'migrate-parent-info',
mode: 'upgrade',
func: migrateParentInfo
}
])
},
async upgrade (state: Map<string, Set<string>>, client: () => Promise<MigrationUpgradeClient>, mode): Promise<void> {}
}
async function migrateChannelsToThreads (client: MigrationClient): Promise<void> {
await client.update(
DOMAIN_CARD,
{
_class: channelMasterTag
},
{
_class: chat.masterTag.Thread
}
)
}
async function migrateParentInfo (client: MigrationClient): Promise<void> {
let processedCards = 0
const iterator = await client.traverse<Card>(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<Doc>, update: MigrateUpdate<Doc> }[] = []
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()
}
}
-11
View File
@@ -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
)
}
-5
View File
@@ -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"/>
</symbol>
<symbol id="grid" viewBox="0 0 24 24">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M7.74991 1.99996C7.74991 1.58575 7.41412 1.24996 6.99991 1.24996C6.58569 1.24996 6.24991 1.58575 6.24991 1.99996V6.24996H1.99991C1.58569 6.24996 1.24991 6.58575 1.24991 6.99996C1.24991 7.41418 1.58569 7.74996 1.99991 7.74996H6.24991V16.25H1.99991C1.58569 16.25 1.24991 16.5857 1.24991 17C1.24991 17.4142 1.58569 17.75 1.99991 17.75H6.24991V22C6.24991 22.4142 6.58569 22.75 6.99991 22.75C7.41412 22.75 7.74991 22.4142 7.74991 22V17.75H16.2499V22C16.2499 22.4142 16.5857 22.75 16.9999 22.75C17.4141 22.75 17.7499 22.4142 17.7499 22V17.75H21.9999C22.4141 17.75 22.7499 17.4142 22.7499 17C22.7499 16.5857 22.4141 16.25 21.9999 16.25H17.7499V7.74996H21.9999C22.4141 7.74996 22.7499 7.41418 22.7499 6.99996C22.7499 6.58575 22.4141 6.24996 21.9999 6.24996H17.7499V1.99996C17.7499 1.58575 17.4141 1.24996 16.9999 1.24996C16.5857 1.24996 16.2499 1.58575 16.2499 1.99996V6.24996H7.74991V1.99996ZM16.2499 16.25V7.74996H7.74991V16.25H16.2499Z"
fill="currentColor"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

-1
View File
@@ -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`
})
@@ -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'
},
-2
View File
@@ -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<MasterTag>,
Thread: '' as Ref<MasterTag>
}
})
-1
View File
@@ -24,7 +24,6 @@ export const mailId = 'mail' as Plugin
export default plugin(mailId, {
tag: {
MailChannel: '' as Ref<Tag>,
MailThread: '' as Ref<Tag>
},
string: {
@@ -353,7 +353,7 @@
<Component
is={card.component.CardArrayEditor}
props={{
_class: chat.masterTag.Channel,
_class: chat.masterTag.Thread,
value: existingGuestChatSettings !== undefined ? existingGuestChatSettings.allowedCards : [],
label: settingsRes.string.GuestChannelsArrayLabel,
onChange: onAllowedCardsChange
@@ -74,7 +74,7 @@ describe('ChannelCache', () => {
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)
})
})
+5 -5
View File
@@ -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
+2 -2
View File
@@ -56,12 +56,12 @@ export function isNewChannelTx (tx: Tx): boolean {
return false
}
const createTx = tx as TxCreateDoc<any>
return createTx.objectClass === chat.masterTag.Channel
return createTx.objectClass === chat.masterTag.Thread
}
export async function getChannel (client: TxOperations, email: string): Promise<Card | undefined> {
const normalizedEmail = normalizeEmail(email)
return await client.findOne<Card>(mail.tag.MailChannel, { title: normalizedEmail })
return await client.findOne<Card>(mail.tag.MailThread, { title: normalizedEmail })
}
export async function getRecipients (
@@ -213,7 +213,7 @@ export class MailWorker {
if (thread?.parent == null) {
return
}
const channel = await workspaceClient.findOne<Card>(chat.masterTag.Channel, { _id: thread.parent })
const channel = await workspaceClient.findOne<Card>(chat.masterTag.Thread, { _id: thread.parent })
if (channel === undefined || !this.isHulyMailChannel(channel)) {
return
}