Fix unreadable channels and duplicated inbox cards (#6838)

This commit is contained in:
Kristina 2024-10-09 07:09:43 +04:00 committed by GitHub
parent 62e330d111
commit 85b76bc911
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 143 additions and 60 deletions

View File

@ -381,7 +381,7 @@ export const notificationOperation: MigrateOperation = {
}
},
{
state: 'migrate-duplicated-contexts-v2',
state: 'migrate-duplicated-contexts-v3',
func: migrateDuplicateContexts
},
{

View File

@ -14,7 +14,18 @@
-->
<script lang="ts">
import { Attachment } from '@hcengineering/attachment'
import { Account, Class, Doc, generateId, Markup, Ref, Space, toIdMap, type Blob } from '@hcengineering/core'
import {
Account,
Class,
Doc,
generateId,
Markup,
Ref,
Space,
toIdMap,
type Blob,
TxOperations
} from '@hcengineering/core'
import { IntlString, setPlatformStatus, unknownError } from '@hcengineering/platform'
import {
createQuery,
@ -178,10 +189,18 @@
}
}
async function saveAttachment (doc: Attachment, objectId: Ref<Doc> | undefined): Promise<void> {
async function saveAttachment (doc: Attachment, objectId: Ref<Doc> | undefined, op?: TxOperations): Promise<void> {
if (space === undefined || objectId === undefined || _class === undefined) return
newAttachments.delete(doc._id)
await client.addCollection(attachment.class.Attachment, space, objectId, _class, 'attachments', doc, doc._id)
await (op ?? client).addCollection(
attachment.class.Attachment,
space,
objectId,
_class,
'attachments',
doc,
doc._id
)
}
async function fileSelected (): Promise<void> {
@ -284,7 +303,7 @@
}
}
export async function createAttachments (_id: Ref<Doc> | undefined = objectId): Promise<void> {
export async function createAttachments (_id: Ref<Doc> | undefined = objectId, op?: TxOperations): Promise<void> {
if (saved) {
return
}
@ -293,7 +312,7 @@
newAttachments.forEach((p) => {
const attachment = attachments.get(p)
if (attachment !== undefined) {
promises.push(saveAttachment(attachment, _id))
promises.push(saveAttachment(attachment, _id, op))
}
})
removedAttachments.forEach((p) => {

View File

@ -287,6 +287,7 @@
updateSelectedDate()
updateScrollData()
updateDownButtonVisibility($metadataStore, messages, scrollDiv)
loadMore()
}
}

View File

@ -58,6 +58,7 @@
let object: Doc | undefined = undefined
let replacedPanel: HTMLElement
let needRestoreLoc = true
const unsubcribe = location.subscribe((loc) => {
syncLocation(loc)
@ -104,10 +105,14 @@
currentSpecial = undefined
selectedData = undefined
object = undefined
restoreLocation(loc, chunterId)
if (needRestoreLoc) {
needRestoreLoc = false
restoreLocation(loc, chunterId)
}
return
}
needRestoreLoc = false
currentSpecial = navigatorModel?.specials?.find((special) => special.id === id)
if (currentSpecial !== undefined) {

View File

@ -59,12 +59,13 @@
fillDefaults(hierarchy, object, contact.class.Organization)
async function createOrganization (): Promise<void> {
const op = client.apply()
await updateMarkup(object.description, { description })
await client.createDoc(contact.class.Organization, contact.space.Contacts, object, id)
await descriptionBox.createAttachments(id)
await op.createDoc(contact.class.Organization, contact.space.Contacts, object, id)
await descriptionBox.createAttachments(id, op)
for (const channel of channels) {
await client.addCollection(
await op.addCollection(
contact.class.Channel,
contact.space.Contacts,
id,
@ -77,8 +78,9 @@
)
}
if (onCreate !== undefined) {
await onCreate?.(id, client)
await onCreate?.(id, op)
}
await op.commit()
Analytics.handleEvent(ContactEvents.CompanyCreated, { id })
dispatch('close', id)
}

View File

@ -42,7 +42,8 @@
if (isCodeWrong || isTitleWrong) {
return
}
await client.createDoc(
const op = client.apply()
await op.createDoc(
documents.class.DocumentCategory,
space,
{
@ -53,8 +54,8 @@
},
_id
)
await descriptionBox.createAttachments(_id)
await descriptionBox.createAttachments(_id, op)
await op.commit()
dispatch('close', _id)
}

View File

@ -180,11 +180,8 @@
// Create space type's mixin with roles assignments
await ops.createMixin(productId, products.class.Product, core.space.Space, spaceType.targetClass, rolesAssignment)
await descriptionBox.createAttachments(undefined, ops)
await ops.commit()
await descriptionBox.createAttachments()
object = createDefaultObject()
dispatch('close', productId)
}

View File

@ -169,7 +169,7 @@
doc._id
)
await descriptionBox.createAttachments()
await descriptionBox.createAttachments(undefined, ops)
if (_comment.trim().length > 0 && !isEmptyMarkup(_comment)) {
await ops.addCollection(chunter.class.ChatMessage, _space, doc._id, recruit.class.Applicant, 'comments', {

View File

@ -250,8 +250,10 @@
await updateMarkup(data.fullDescription, { fullDescription })
const id = await client.createDoc(recruit.class.Vacancy, core.space.Space, data, objectId)
const ops = client.apply()
const id = await ops.createDoc(recruit.class.Vacancy, core.space.Space, data, objectId)
await descriptionBox.createAttachments(undefined, ops)
await ops.commit()
Analytics.handleEvent(RecruitEvents.VacancyCreated, {
id: getSequenceId({
...data,
@ -274,8 +276,6 @@
}
}
await descriptionBox.createAttachments()
// Add vacancy mixin with roles assignment
await client.createMixin(
objectId,

View File

@ -543,8 +543,8 @@
}
}
await descriptionBox?.createAttachments(_id, operations)
const result = await operations.commit()
await descriptionBox?.createAttachments(_id)
const parents: IssueParentInfo[] =
parentIssue != null

View File

@ -349,9 +349,20 @@ export async function generateDocUpdateMessages (
let doc = objectCache?.docs?.get(tx.objectId)
if (doc === undefined) {
doc = (await control.findAll(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0]
objectCache?.docs?.set(tx.objectId, doc)
}
if (doc === undefined) {
const isAttachedDoc = hierarchy.isDerived(tx.objectClass, core.class.AttachedDoc)
const createTx = isAttachedDoc
? (await control.findAll(ctx, core.class.TxCollectionCUD, { 'tx.objectId': tx.objectId }, { limit: 1 }))[0]
: (await control.findAll(ctx, core.class.TxCreateDoc, { objectId: tx.objectId }, { limit: 1 }))[0]
doc =
createTx !== undefined
? TxProcessor.createDoc2Doc(TxProcessor.extractTx(createTx) as TxCreateDoc<Doc>)
: undefined
}
if (doc !== undefined) {
objectCache?.docs?.set(tx.objectId, doc)
return await ctx.with(
'pushDocUpdateMessages',
{},

View File

@ -83,7 +83,7 @@ import { encodeObjectURI } from '@hcengineering/view'
import { workbenchId } from '@hcengineering/workbench'
import webpush, { WebPushError } from 'web-push'
import { Content, NotifyParams, NotifyResult } from './types'
import { Content, ContextsCache, ContextsCacheKey, NotifyParams, NotifyResult } from './types'
import {
createPullCollaboratorsTx,
createPushCollaboratorsTx,
@ -727,6 +727,20 @@ async function createNotifyContext (
updateTimestamp?: Timestamp,
tx?: TxCUD<Doc>
): Promise<Ref<DocNotifyContext>> {
const contextsCache: ContextsCache = control.cache.get(ContextsCacheKey) ?? {
contexts: new Map<string, Ref<DocNotifyContext>>()
}
const cacheKey = `${objectId}_${receiver._id}`
const cachedId = contextsCache.contexts.get(cacheKey)
if (cachedId !== undefined) {
if (control.removedMap.has(cachedId)) {
contextsCache.contexts.delete(cacheKey)
} else {
return cachedId
}
}
const createTx = control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, receiver.space, {
user: receiver._id,
objectId,
@ -738,6 +752,9 @@ async function createNotifyContext (
lastUpdateTimestamp: updateTimestamp,
lastViewedTimestamp: sender === receiver._id ? updateTimestamp : undefined
})
contextsCache.contexts.set(cacheKey, createTx.objectId)
control.cache.set(ContextsCacheKey, contextsCache)
await ctx.with('apply', {}, () => control.apply(control.ctx, [createTx]))
if (receiver.account?.email !== undefined) {
control.ctx.contextData.broadcast.targets['docNotifyContext' + createTx._id] = (it) => {
@ -1334,6 +1351,29 @@ async function collectionCollabDoc (
return res
}
async function removeContextNotifications (
control: TriggerControl,
notifyContextRefs: Ref<DocNotifyContext>[]
): Promise<Tx[]> {
const inboxNotifications = await control.findAll(
control.ctx,
notification.class.InboxNotification,
{
docNotifyContext: { $in: notifyContextRefs }
},
{
projection: {
_id: 1,
_class: 1,
space: 1
}
}
)
return inboxNotifications.map((notification) =>
control.txFactory.createTxRemoveDoc(notification._class, notification.space, notification._id)
)
}
async function removeCollaboratorDoc (tx: TxRemoveDoc<Doc>, control: TriggerControl): Promise<Tx[]> {
const hierarchy = control.hierarchy
const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators)
@ -1362,24 +1402,8 @@ async function removeCollaboratorDoc (tx: TxRemoveDoc<Doc>, control: TriggerCont
const notifyContextRefs = notifyContexts.map(({ _id }) => _id)
const inboxNotifications = await control.findAll(
control.ctx,
notification.class.InboxNotification,
{
docNotifyContext: { $in: notifyContextRefs }
},
{
projection: {
_id: 1,
_class: 1,
space: 1
}
}
)
inboxNotifications.forEach((notification) => {
res.push(control.txFactory.createTxRemoveDoc(notification._class, notification.space, notification._id))
})
const txes = await removeContextNotifications(control, notifyContextRefs)
res.push(...txes)
notifyContexts.forEach((context) => {
res.push(control.txFactory.createTxRemoveDoc(context._class, context.space, context._id))
})
@ -1896,6 +1920,17 @@ async function OnDocRemove (originTx: TxCUD<Doc>, control: TriggerControl): Prom
const txes = await OnActivityMessageRemove(message, control)
res.push(...txes)
}
} else if (control.hierarchy.isDerived(tx.objectClass, notification.class.DocNotifyContext)) {
const contextsCache: ContextsCache | undefined = control.cache.get(ContextsCacheKey)
if (contextsCache !== undefined) {
for (const [key, value] of contextsCache.contexts.entries()) {
if (value === tx.objectId) {
contextsCache.contexts.delete(key)
}
}
}
return await removeContextNotifications(control, [tx.objectId as Ref<DocNotifyContext>])
}
const txes = await removeCollaboratorDoc(tx, control)

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
//
import { BaseNotificationType, NotificationProvider } from '@hcengineering/notification'
import { BaseNotificationType, DocNotifyContext, NotificationProvider } from '@hcengineering/notification'
import { Ref } from '@hcengineering/core'
/**
@ -34,3 +34,8 @@ export interface NotifyParams {
isSpace: boolean
shouldUpdateTimestamp: boolean
}
export const ContextsCacheKey = 'DocNotifyContexts'
export interface ContextsCache {
contexts: Map<string, Ref<DocNotifyContext>>
}

View File

@ -213,6 +213,9 @@ export interface TriggerControl {
modelDb: ModelDb
removedMap: Map<Ref<Doc>, Doc>
// Cache per workspace
cache: Map<string, any>
// Cache per root tx
contextCache: Map<string, any>
// Since we don't have other storages let's consider adapter is MinioClient

View File

@ -55,10 +55,22 @@ import serverCore, { BaseMiddleware, SessionDataImpl, SessionFindAll, Triggers }
export class TriggersMiddleware extends BaseMiddleware implements Middleware {
triggers: Triggers
storageAdapter!: StorageAdapter
cache = new Map<string, any>()
intervalId: NodeJS.Timeout
constructor (context: PipelineContext, next: Middleware | undefined) {
super(context, next)
this.triggers = new Triggers(this.context.hierarchy)
this.intervalId = setInterval(
() => {
this.cache.clear()
},
30 * 60 * 1000
)
}
async close (): Promise<void> {
clearInterval(this.intervalId)
}
static async create (ctx: MeasureContext, context: PipelineContext, next?: Middleware): Promise<Middleware> {
@ -113,6 +125,7 @@ export class TriggersMiddleware extends BaseMiddleware implements Middleware {
contextCache: ctx.contextData.contextCache,
modelDb: this.context.modelDb,
hierarchy: this.context.hierarchy,
cache: this.cache,
apply: async (ctx, tx, needResult) => {
if (needResult === true) {
return (await this.context.derived?.tx(ctx, tx)) ?? {}

View File

@ -439,22 +439,8 @@ test.describe('Channel tests', () => {
await channelPage.makeActionWithChannelInMenu(data.channelName, 'Leave channel')
})
await test.step('Join channel from a leaved channel page', async () => {
await channelPage.checkIfChannelDefaultExist(true, data.channelName)
await channelPage.clickJoinChannelButton()
await channelPage.checkIfChannelDefaultExist(true, data.channelName)
})
await test.step('Leave channel #2', async () => {
await channelPage.makeActionWithChannelInMenu(data.channelName, 'Leave channel')
})
await test.step('Open another channel and then check that leaved channel is removed from left menu', async () => {
await channelPage.clickChooseChannel('random')
await test.step('Join channel from channels page', async () => {
await channelPage.checkIfChannelDefaultExist(false, data.channelName)
})
await test.step('Join channel from a channels table', async () => {
await channelPage.clickChannelTab()
await channelPage.checkIfChannelTableExist(data.channelName, true)
await channelPage.clickJoinChannelButton()
@ -462,6 +448,11 @@ test.describe('Channel tests', () => {
await channelPage.clickChooseChannel(data.channelName)
await channelPage.checkMessageExist('Test message', true, 'Test message')
})
await test.step('Open another channel and check that joined channel is visible', async () => {
await channelPage.clickChooseChannel('random')
await channelPage.checkIfChannelDefaultExist(true, data.channelName)
})
})
test('User is able to filter channels in table', async () => {