Fix notification when Request.requested changed (#9105)
Some checks are pending
CI / build (push) Waiting to run
CI / svelte-check (push) Blocked by required conditions
CI / formatting (push) Blocked by required conditions
CI / test (push) Blocked by required conditions
CI / uitest (push) Waiting to run
CI / uitest-pg (push) Waiting to run
CI / uitest-qms (push) Waiting to run
CI / uitest-workspaces (push) Waiting to run
CI / docker-build (push) Blocked by required conditions
CI / dist-build (push) Blocked by required conditions

Signed-off-by: Kristina Fefelova <kristin.fefelova@gmail.com>
This commit is contained in:
Kristina 2025-05-26 23:31:53 +04:00 committed by GitHub
parent d6ce90ffd6
commit 3d3285d1b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 302 additions and 32 deletions

View File

@ -462,11 +462,11 @@ export class TDocumentComment extends TChatMessage implements DocumentComment {
export class TDocumentRequest extends TRequest implements DocumentRequest {}
@Model(documents.class.DocumentReviewRequest, documents.class.DocumentRequest)
@UX(documents.string.DocumentReviewRequest)
@UX(documents.string.DocumentReviewRequest, documents.icon.Document)
export class TDocumentReviewRequest extends TDocumentRequest implements DocumentReviewRequest {}
@Model(documents.class.DocumentApprovalRequest, documents.class.DocumentRequest)
@UX(documents.string.DocumentApprovalRequest)
@UX(documents.string.DocumentApprovalRequest, documents.icon.Document)
export class TDocumentApprovalRequest extends TDocumentRequest implements DocumentApprovalRequest {}
@Mixin(documents.mixin.DocumentSpaceTypeData, documents.class.DocumentSpace)

View File

@ -133,24 +133,56 @@ export function createModel (builder: Builder): void {
field: 'requested',
generated: false,
group: request.ids.RequestNotificationGroup,
label: request.string.Request,
label: request.string.NewRequest,
allowedForAuthor: true,
defaultEnabled: true,
templates: {
textTemplate: '{sender} sent you a {doc}',
htmlTemplate: '<p><b>{sender}</b> sent you a {doc}</p>',
textTemplate: '{sender} sent you a request for the {doc}',
htmlTemplate: '<p><b>{sender}</b> sent you a request for the {doc}</p>',
subjectTemplate: '{doc}'
}
},
request.ids.CreateRequestNotification
)
builder.createDoc(
notification.class.NotificationType,
core.space.Model,
{
hidden: false,
objectClass: request.class.Request,
txClasses: [core.class.TxUpdateDoc],
field: 'requested',
generated: false,
group: request.ids.RequestNotificationGroup,
label: request.string.CancelRequest,
allowedForAuthor: true,
defaultEnabled: true,
templates: {
textTemplate: '{sender} canceled the request for the {doc}',
htmlTemplate: '<p><b>{sender}</b> canceled the request for the {doc}</p>',
subjectTemplate: '{doc}'
}
},
request.ids.RemoveRequestNotification
)
builder.createDoc(notification.class.ActivityNotificationViewlet, core.space.Model, {
messageMatch: {
_class: activity.class.DocUpdateMessage,
objectClass: request.class.Request,
action: 'update',
'attributeUpdates.attrKey': 'requested'
},
presenter: request.component.RequestedChangedNotification
})
generateClassNotificationTypes(
builder,
request.class.Request,
request.ids.RequestNotificationGroup,
['requested'],
['comments', 'approved', 'rejected', 'status']
['comments']
)
builder.createDoc(core.class.DomainIndexConfiguration, core.space.Model, {

View File

@ -24,14 +24,18 @@ import type { NotificationGroup, NotificationType } from '@hcengineering/notific
export default mergeIds(requestId, request, {
component: {
EditRequest: '' as AnyComponent,
NotificationRequestView: '' as AnyComponent
NotificationRequestView: '' as AnyComponent,
RequestedChangedNotification: '' as AnyComponent
},
ids: {
RequestNotificationGroup: '' as Ref<NotificationGroup>,
CreateRequestNotification: '' as Ref<NotificationType>
CreateRequestNotification: '' as Ref<NotificationType>,
RemoveRequestNotification: '' as Ref<NotificationType>
},
string: {
Status: '' as IntlString,
Requested: '' as IntlString
Requested: '' as IntlString,
NewRequest: '' as IntlString,
CancelRequest: '' as IntlString
}
})

View File

@ -35,6 +35,7 @@
"@hcengineering/server-request": "^0.6.0",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/model-request": "^0.6.0",
"@hcengineering/server-notification": "^0.6.1"
"@hcengineering/server-notification": "^0.6.1",
"@hcengineering/notification": "^0.6.23"
}
}

View File

@ -20,6 +20,7 @@ import serverCore from '@hcengineering/server-core'
import serverRequest from '@hcengineering/server-request'
import serverNotification from '@hcengineering/server-notification'
import request from '@hcengineering/model-request'
import notification from '@hcengineering/notification'
export { serverRequestId } from '@hcengineering/server-request'
@ -34,4 +35,22 @@ export function createModel (builder: Builder): void {
builder.mixin(request.class.Request, core.class.Class, serverNotification.mixin.TextPresenter, {
presenter: serverRequest.function.RequestTextPresenter
})
builder.mixin(
request.ids.CreateRequestNotification,
notification.class.NotificationType,
serverNotification.mixin.TypeMatch,
{
func: serverRequest.function.SendRequestMatch
}
)
builder.mixin(
request.ids.RemoveRequestNotification,
notification.class.NotificationType,
serverNotification.mixin.TypeMatch,
{
func: serverRequest.function.RemoveRequestMatch
}
)
}

View File

@ -21,7 +21,7 @@
DocumentReviewRequest,
DocumentState
} from '@hcengineering/controlled-documents'
import { Ref } from '@hcengineering/core'
import { DocumentUpdate, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import { Scroller } from '@hcengineering/ui'
@ -88,16 +88,51 @@
}
const requiredApprovesCount = users.length
const requestedQuery: DocumentUpdate<DocumentReviewRequest | DocumentApprovalRequest> = {}
if (addedPersons.size > 0) {
requestedQuery.$push = { requested: { $each: Array.from(addedPersons), $position: 0 } }
}
if (removedPersons.size > 0) {
requestedQuery.$pull = { requested: { $in: Array.from(removedPersons) } }
}
if (Object.keys(requestedQuery).length > 0) {
await ops.update(request, requestedQuery)
}
await ops.update(request, {
requested: users,
approved,
approvedDates,
requiredApprovesCount
})
}
await ops.update(controlledDoc, { [type]: users })
const added = new Set()
const removed = new Set()
for (const user of users) {
if (!controlledDoc[type].includes(user as Ref<Employee>)) {
added.add(user)
}
}
for (const user of controlledDoc[type]) {
if (!users.includes(user)) {
removed.add(user)
}
}
const updateQuery: DocumentUpdate<ControlledDocument> = {}
if (added.size > 0) {
updateQuery.$push = { [type]: { $each: Array.from(added), $position: 0 } }
}
if (removed.size > 0) {
updateQuery.$pull = { [type]: { $in: Array.from(removed) } }
}
if (Object.keys(updateQuery).length > 0) {
await ops.update(controlledDoc, updateQuery)
}
await ops.commit()
}
</script>

View File

@ -25,7 +25,7 @@
combineActivityMessages,
sortActivityMessages
} from '@hcengineering/activity-resources'
import { ActivityMessage, DisplayActivityMessage } from '@hcengineering/activity'
import activity, { ActivityMessage, DisplayActivityMessage, DocUpdateMessage } from '@hcengineering/activity'
import { Action, Component } from '@hcengineering/ui'
import { getActions } from '@hcengineering/view-resources'
import { getResource } from '@hcengineering/platform'
@ -55,6 +55,24 @@
$: updateViewlet(viewlets, displayMessage)
function matchViewlet (viewlet: ActivityNotificationViewlet, message: DisplayActivityMessage): boolean {
const hierarchy = client.getHierarchy()
const matched = matchQuery([message], viewlet.messageMatch, message._class, hierarchy, true)[0]
if (matched !== undefined) return true
if (hierarchy.isDerived(message._class, activity.class.DocUpdateMessage)) {
const dum = message as DocUpdateMessage
const dumUpdated: DocUpdateMessage = {
...dum,
objectClass: hierarchy.getParentClass(dum.objectClass)
}
const matched = matchQuery([dumUpdated], viewlet.messageMatch, message._class, hierarchy, true)[0]
return matched !== undefined
}
return false
}
function updateViewlet (viewlets: ActivityNotificationViewlet[], message?: DisplayActivityMessage): void {
if (viewlets.length === 0 || message === undefined) {
viewlet = undefined
@ -62,8 +80,8 @@
}
for (const v of viewlets) {
const matched = matchQuery([message], v.messageMatch, message._class, client.getHierarchy(), true)
if (matched.length > 0) {
const matched = matchViewlet(v, message)
if (matched) {
viewlet = v
return
}

View File

@ -18,6 +18,8 @@
"PleaseTypeMessage": "Zadejte prosím zprávu ke komentáři, abyste mohli pokračovat...",
"NoRequests": "Žádné žádosti",
"Cancel": "Zrušit",
"Cancelled": "Zrušeno"
"Cancelled": "Zrušeno",
"NewRequest": "Nový požadavek",
"CancelRequest": "Zrušit požadavek"
}
}

View File

@ -18,6 +18,8 @@
"PleaseTypeMessage": "Bitte geben Sie eine Kommentarnachricht ein, um fortzufahren...",
"NoRequests": "Keine Anfragen",
"Cancel": "Abbrechen",
"Cancelled": "Abgebrochen"
"Cancelled": "Abgebrochen",
"NewRequest": "Neue Anfrage",
"CancelRequest": "Anfrage abbrechen"
}
}

View File

@ -18,6 +18,8 @@
"PleaseTypeMessage": "Please type comment message to continue...",
"NoRequests": "No requests",
"Cancel": "Cancel",
"Cancelled": "Cancelled"
"Cancelled": "Cancelled",
"NewRequest": "New Request",
"CancelRequest": "Cancel Request"
}
}

View File

@ -18,6 +18,8 @@
"PleaseTypeMessage": "Escriba un mensaje de comentario para continuar...",
"NoRequests": "No hay solicitudes",
"Cancel": "Cancelar",
"Cancelled": "Cancelado"
"Cancelled": "Cancelado",
"NewRequest": "Nueva solicitud",
"CancelRequest": "Cancelar solicitud"
}
}

View File

@ -18,6 +18,8 @@
"PleaseTypeMessage": "Veuillez taper un message de commentaire pour continuer...",
"NoRequests": "Aucune demande",
"Cancel": "Annuler",
"Cancelled": "Annulé"
"Cancelled": "Annulé",
"NewRequest": "Nouvelle demande",
"CancelRequest": "Annuler la demande"
}
}

View File

@ -18,6 +18,8 @@
"PleaseTypeMessage": "Per favore, digita un commento per continuare...",
"NoRequests": "Nessuna richiesta",
"Cancel": "Annulla",
"Cancelled": "Annullato"
"Cancelled": "Annullato",
"NewRequest": "Nuova richiesta",
"CancelRequest": "Annulla richiesta"
}
}

View File

@ -18,6 +18,8 @@
"PleaseTypeMessage": "コメントを入力して続行...",
"NoRequests": "リクエストはありません",
"Cancel": "キャンセル",
"Cancelled": "キャンセル済み"
"Cancelled": "キャンセル済み",
"NewRequest": "新しいリクエスト",
"CancelRequest": "リクエストをキャンセル"
}
}

View File

@ -18,6 +18,8 @@
"PleaseTypeMessage": "Digite uma mensagem de comentário para continuar...",
"NoRequests": "Sem solicitações",
"Cancel": "Cancelar",
"Cancelled": "Cancelado"
"Cancelled": "Cancelado",
"NewRequest": "Nova solicitação",
"CancelRequest": "Cancelar solicitação"
}
}

View File

@ -18,6 +18,8 @@
"PleaseTypeMessage": "Пожалуйста, оставьте комментарий, чтобы продолжить...",
"NoRequests": "Нет запросов",
"Cancel": "Отменить",
"Cancelled": "Отменен"
"Cancelled": "Отменен",
"NewRequest": "Новый запрос",
"CancelRequest": "Отмена запроса"
}
}

View File

@ -18,6 +18,8 @@
"PleaseTypeMessage": "请键入评论消息以继续...",
"NoRequests": "没有请求",
"Cancel": "取消",
"Cancelled": "已取消"
"Cancelled": "已取消",
"NewRequest": "新请求",
"CancelRequest": "取消请求"
}
}

View File

@ -0,0 +1,80 @@
<!--
// Copyright © 2025 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import activity, { DisplayDocUpdateMessage } from '@hcengineering/activity'
import { BaseMessagePreview } from '@hcengineering/activity-resources'
import { getCurrentEmployee } from '@hcengineering/contact'
import { getClient } from '@hcengineering/presentation'
import { Icon, Label } from '@hcengineering/ui'
import { ObjectPresenter } from '@hcengineering/view-resources'
import request from '../plugin'
export let message: DisplayDocUpdateMessage
const me = getCurrentEmployee()
const client = getClient()
$: isRemovedMe = message.attributeUpdates?.removed.includes(me) ?? false
$: isAddedMe = message.attributeUpdates?.added.includes(me) ?? false
</script>
<BaseMessagePreview {message} on:click>
<slot name="content">
<div class="content overflow-label ml-1" class:preview={true}>
<span class="mr-1">
<Icon icon={client.getHierarchy().getClass(message.objectClass).icon ?? activity.icon.Activity} size="small" />
</span>
{#if isAddedMe}
<Label label={activity.string.New} />
{:else if isRemovedMe}
<Label label={request.string.Cancelled} />
{:else}
<Label label={activity.string.Updated} />
{/if}
<span class="lower">
<Label label={request.string.Requests} />
</span>
:
<span class="overflow-label values" class:preview={true}>
<ObjectPresenter objectId={message.objectId} _class={message.objectClass} shouldShowAvatar={false} />
</span>
</div>
</slot>
</BaseMessagePreview>
<style lang="scss">
.content {
display: flex;
gap: 0.25rem;
align-items: center;
flex-wrap: wrap;
color: var(--global-primary-TextColor);
&.preview {
flex-wrap: nowrap;
}
}
.values {
display: flex;
align-items: center;
flex-wrap: wrap;
&.preview {
flex-wrap: nowrap;
}
}
</style>

View File

@ -18,6 +18,7 @@ import EditRequest from './components/EditRequest.svelte'
import RequestPresenter from './components/RequestPresenter.svelte'
import RequestView from './components/RequestView.svelte'
import NotificationRequestView from './components/NotificationRequestView.svelte'
import RequestedChangedNotification from './components/RequestedChangedNotification.svelte'
export { default as RequestStatusPresenter } from './components/RequestStatusPresenter.svelte'
export { default as RequestDetailPopup } from './components/RequestDetailPopup.svelte'
@ -27,6 +28,7 @@ export default async (): Promise<Resources> => ({
EditRequest,
RequestPresenter,
RequestView,
NotificationRequestView
NotificationRequestView,
RequestedChangedNotification
}
})

View File

@ -14,8 +14,20 @@
//
import { DocUpdateMessage } from '@hcengineering/activity'
import core, { Doc, Tx, TxCUD, TxCreateDoc, TxProcessor, TxUpdateDoc, type MeasureContext } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import core, {
Doc,
Tx,
TxCUD,
TxCreateDoc,
TxProcessor,
TxUpdateDoc,
type MeasureContext,
Ref,
PersonId,
AccountUuid,
combineAttributes
} from '@hcengineering/core'
import notification, { NotificationType } from '@hcengineering/notification'
import { getResource, translate } from '@hcengineering/platform'
import request, { Request, RequestStatus } from '@hcengineering/request'
import { pushDocUpdateMessages } from '@hcengineering/server-activity-resources'
@ -28,6 +40,7 @@ import {
getSenderInfo,
getTextPresenter
} from '@hcengineering/server-notification-resources'
import { Person } from '@hcengineering/contact'
/**
* @public
@ -191,10 +204,54 @@ export async function requestTextPresenter (doc: Doc, control: TriggerControl):
return title
}
export const sendRequestMatch = (
tx: TxCreateDoc<Request> | TxUpdateDoc<Request>,
doc: Doc,
person: Ref<Person>,
socialIds: PersonId[],
type: NotificationType,
control: TriggerControl,
account: AccountUuid
): boolean => {
if (tx._class === core.class.TxCreateDoc) {
const createTx = tx as TxCreateDoc<Request>
const request = TxProcessor.createDoc2Doc(createTx)
return request.requested.includes(person)
} else if (tx._class === core.class.TxUpdateDoc) {
const updateTx = tx as TxUpdateDoc<Request>
const pushed: Ref<Person>[] = combineAttributes([updateTx.operations], 'requested', '$push', '$each') ?? []
return pushed.includes(person)
}
return false
}
export const removeRequestMatch = (
tx: TxUpdateDoc<Request>,
doc: Doc,
person: Ref<Person>,
socialIds: PersonId[],
type: NotificationType,
control: TriggerControl,
account: AccountUuid
): boolean => {
if (tx._class === core.class.TxUpdateDoc) {
const removed: Ref<Person>[] = combineAttributes([tx.operations], 'requested', '$pull', '$in') ?? []
return removed.includes(person)
}
return false
}
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
export default async () => ({
function: {
RequestTextPresenter: requestTextPresenter
RequestTextPresenter: requestTextPresenter,
SendRequestMatch: sendRequestMatch,
RemoveRequestMatch: removeRequestMatch
},
trigger: {
OnRequest

View File

@ -15,7 +15,7 @@
import { Plugin, plugin, Resource } from '@hcengineering/platform'
import type { TriggerFunc } from '@hcengineering/server-core'
import { Presenter } from '@hcengineering/server-notification'
import { Presenter, TypeMatchFunc } from '@hcengineering/server-notification'
/**
* @public
@ -27,7 +27,9 @@ export const serverRequestId = 'server-request' as Plugin
*/
export default plugin(serverRequestId, {
function: {
RequestTextPresenter: '' as Resource<Presenter>
RequestTextPresenter: '' as Resource<Presenter>,
SendRequestMatch: '' as TypeMatchFunc,
RemoveRequestMatch: '' as TypeMatchFunc
},
trigger: {
OnRequest: '' as Resource<TriggerFunc>