mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-16 05:13:06 +00:00
Fix unreadable channels and duplicated inbox cards (#6838)
This commit is contained in:
parent
62e330d111
commit
85b76bc911
@ -381,7 +381,7 @@ export const notificationOperation: MigrateOperation = {
|
||||
}
|
||||
},
|
||||
{
|
||||
state: 'migrate-duplicated-contexts-v2',
|
||||
state: 'migrate-duplicated-contexts-v3',
|
||||
func: migrateDuplicateContexts
|
||||
},
|
||||
{
|
||||
|
@ -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) => {
|
||||
|
@ -287,6 +287,7 @@
|
||||
updateSelectedDate()
|
||||
updateScrollData()
|
||||
updateDownButtonVisibility($metadataStore, messages, scrollDiv)
|
||||
loadMore()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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', {
|
||||
|
@ -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,
|
||||
|
@ -543,8 +543,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
await descriptionBox?.createAttachments(_id, operations)
|
||||
const result = await operations.commit()
|
||||
await descriptionBox?.createAttachments(_id)
|
||||
|
||||
const parents: IssueParentInfo[] =
|
||||
parentIssue != null
|
||||
|
@ -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',
|
||||
{},
|
||||
|
@ -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)
|
||||
|
@ -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>>
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)) ?? {}
|
||||
|
@ -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 () => {
|
||||
|
Loading…
Reference in New Issue
Block a user