From d75010466d2ea2d78f9d215a3cb88bb060f75b50 Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Mon, 5 Aug 2024 18:13:01 +0700 Subject: [PATCH 01/20] fix: rekoni service build (#6255) Signed-off-by: Alexander Onnikov --- services/rekoni/package.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/services/rekoni/package.json b/services/rekoni/package.json index a842386a7f..d3291cee99 100644 --- a/services/rekoni/package.json +++ b/services/rekoni/package.json @@ -16,9 +16,11 @@ "_phase:bundle": "rushx bundle", "_phase:docker-build": "rushx docker:build", "_phase:docker-staging": "rushx docker:staging", - "docker:build": "docker build -t hardcoreeng/rekoni-service .", - "docker:staging": "./docker_tag.sh hardcoreeng/rekoni-service staging", - "docker:push": "./docker_tag.sh hardcoreeng/rekoni-service", + "docker:build": "../../common/scripts/docker_build.sh hardcoreeng/rekoni-service", + "docker:tbuild": "docker build -t hardcoreeng/rekoni-service . --platform=linux/amd64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/rekoni-service", + "docker:abuild": "docker build -t hardcoreeng/rekoni-service . --platform=linux/arm64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/rekoni-service", + "docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/rekoni-service staging", + "docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/rekoni-service", "bundle": "node esbuild.js", "start": "ts-node-dev src/index.ts", "run-local": "cross-env ts-node src/index.ts", From c1508305bcc20001bbe191443c02ed0bd57e878d Mon Sep 17 00:00:00 2001 From: JasminMus <167111741+JasminMus@users.noreply.github.com> Date: Mon, 5 Aug 2024 13:15:32 +0200 Subject: [PATCH 02/20] Add test for req-7 (#6233) Signed-off-by: Jasmin --- .../sanity/tests/documents/REQ-03.spec.ts | 7 + .../sanity/tests/documents/REQ-05.spec.ts | 7 + .../sanity/tests/documents/REQ-14.spec.ts | 129 ++++++++++++++++++ .../model/documents/document-content-page.ts | 7 +- qms-tests/sanity/tests/model/types.ts | 3 +- 5 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 qms-tests/sanity/tests/documents/REQ-14.spec.ts diff --git a/qms-tests/sanity/tests/documents/REQ-03.spec.ts b/qms-tests/sanity/tests/documents/REQ-03.spec.ts index 3154e6d645..dce0d5329d 100644 --- a/qms-tests/sanity/tests/documents/REQ-03.spec.ts +++ b/qms-tests/sanity/tests/documents/REQ-03.spec.ts @@ -25,6 +25,13 @@ test.describe('ISO 13485, 4.2.4 Control of documents, ensure that the current re await (await page.goto(`${PlatformURI}/${HomepageURI}`))?.finished() }) + test.afterEach(async ({ browser }) => { + const contexts = browser.contexts() + for (const context of contexts) { + await context.close() + } + }) + test('TESTS-325. Create a Several documents with Minor & Major versions', async ({ page, browser }) => { await allure.description( 'Requirement\nUsers need to make a resolve all comments and done documents for the Effective status' diff --git a/qms-tests/sanity/tests/documents/REQ-05.spec.ts b/qms-tests/sanity/tests/documents/REQ-05.spec.ts index f5a7c19572..11f9021ba1 100644 --- a/qms-tests/sanity/tests/documents/REQ-05.spec.ts +++ b/qms-tests/sanity/tests/documents/REQ-05.spec.ts @@ -16,6 +16,13 @@ test.describe('ISO 13485, 4.2.4 Control of documents', () => { await (await page.goto(`${PlatformURI}/${HomepageURI}`))?.finished() }) + test.afterEach(async ({ browser }) => { + const contexts = browser.contexts() + for (const context of contexts) { + await context.close() + } + }) + test('TESTS-298. Create a new Category from top right corner', async ({ page }) => { await allure.description('Requirement\nUsers need to create a new category') await allure.tms('TESTS-298', 'https://tracex.hc.engineering/workbench/platform/tracker/TESTS-298') diff --git a/qms-tests/sanity/tests/documents/REQ-14.spec.ts b/qms-tests/sanity/tests/documents/REQ-14.spec.ts new file mode 100644 index 0000000000..ab44785598 --- /dev/null +++ b/qms-tests/sanity/tests/documents/REQ-14.spec.ts @@ -0,0 +1,129 @@ +import { test } from '@playwright/test' +import { + attachScreenshot, + DocumentURI, + generateId, + getSecondPage, + HomepageURI, + PlatformSetting, + PlatformURI +} from '../utils' +import { allure } from 'allure-playwright' +import { DocumentsPage } from '../model/documents/documents-page' +import { DocumentDetails, DocumentRights, DocumentStatus, NewDocument } from '../model/types' +import { DocumentContentPage } from '../model/documents/document-content-page' +import { prepareDocumentStep } from './common-documents-steps' +import { DocumentHistoryPage } from '../model/documents/document-history-page' + +test.use({ + storageState: PlatformSetting +}) + +test.describe('QMS. Documents tests for Control of documents ISO 13485, 4.2.4 FS-150', () => { + test.beforeEach(async ({ page }) => { + await (await page.goto(`${PlatformURI}/${HomepageURI}`))?.finished() + }) + + test.afterEach(async ({ browser }) => { + const contexts = browser.contexts() + for (const context of contexts) { + await context.close() + } + }) + + test('TESTS-384. Create a new doc version and previous one then marked to “Obsolete”', async ({ page, browser }) => { + await allure.description('Requirement\nUsers need to create a new document and new version document') + await allure.tms('TESTS-384', 'https://front.hc.engineering/workbench/platform/tracker/TESTS-384') + const userSecondPage = await getSecondPage(browser) + const completeDocument: NewDocument = { + template: 'HR (HR)', + title: `Complete document-${generateId()}`, + description: `Complete document description-${generateId()}` + } + const reviewer = 'Dirak Kainin' + const documentDetails: DocumentDetails = { + type: 'HR', + category: 'Human Resources', + version: 'v0.1', + status: DocumentStatus.DRAFT, + owner: 'Appleseed John', + author: 'Appleseed John' + } + await prepareDocumentStep(page, completeDocument) + + const documentContentPage = new DocumentContentPage(page) + + await test.step('2. Send for Approval', async () => { + await documentContentPage.buttonSendForApproval.click() + await documentContentPage.fillSelectApproversForm([reviewer]) + await documentContentPage.checkDocumentStatus(DocumentStatus.IN_APPROVAL) + await documentContentPage.checkDocument({ + ...documentDetails, + status: DocumentStatus.IN_APPROVAL, + version: 'v0.1' + }) + await documentContentPage.checkCurrentRights(DocumentRights.VIEWING) + }) + + await test.step('3. Approve document', async () => { + const documentsPageSecond = new DocumentsPage(userSecondPage) + await (await userSecondPage.goto(`${PlatformURI}/${DocumentURI}`))?.finished() + await documentsPageSecond.openDocument(completeDocument.title) + + const documentContentPageSecond = new DocumentContentPage(userSecondPage) + await documentContentPageSecond.confirmApproval() + + await documentContentPageSecond.checkDocumentStatus(DocumentStatus.EFFECTIVE) + await documentContentPageSecond.checkDocument({ + ...documentDetails, + status: DocumentStatus.EFFECTIVE, + version: 'v0.1' + }) + await documentContentPageSecond.checkCurrentRights(DocumentRights.VIEWING) + + await attachScreenshot('TEST-384_approve_document.png', page) + }) + + await test.step('4. Check document', async () => { + await documentContentPage.checkDocumentStatus(DocumentStatus.EFFECTIVE) + await documentContentPage.checkDocument({ + ...documentDetails, + status: DocumentStatus.EFFECTIVE, + version: 'v0.1' + }) + await documentContentPage.checkCurrentRights(DocumentRights.VIEWING) + + await attachScreenshot('TESTS-384_check_document.png', page) + }) + await test.step('5. Check History tab', async () => { + await documentContentPage.buttonHistoryTab.first().click() + const documentHistoryPage = new DocumentHistoryPage(page) + await documentHistoryPage.checkHistoryEventExist('New document creation') + await attachScreenshot('TESTS-384_check_history_tab.png', page) + }) + await test.step('6. Send for Approval v0.2', async () => { + await documentContentPage.sendForApproval( + 'Minor', + 'v0.2', + 'Reason 0.2', + 'impact 0.2', + 'v0.1', + 'v0.2', + userSecondPage, + completeDocument, + documentDetails + ) + }) + await test.step('7. Check archived status', async () => { + const documentContentPageSecond = new DocumentContentPage(userSecondPage) + await documentContentPage.clickPreviousVersionHeader(userSecondPage, completeDocument, 'v0.2') + await documentContentPage.clickPreviousVersionHeader(userSecondPage, completeDocument, 'v0.1') + await documentContentPageSecond.checkDocument({ + ...documentDetails, + status: DocumentStatus.ARCHIVED, + version: 'v0.1' + }) + await attachScreenshot('TESTS-384_archived_status.png', page) + }) + }) +}) diff --git a/qms-tests/sanity/tests/model/documents/document-content-page.ts b/qms-tests/sanity/tests/model/documents/document-content-page.ts index 1f447b2a5a..c2059843b5 100644 --- a/qms-tests/sanity/tests/model/documents/document-content-page.ts +++ b/qms-tests/sanity/tests/model/documents/document-content-page.ts @@ -236,7 +236,7 @@ export class DocumentContentPage extends DocumentCommonPage { } async selectRelease (version: string): Promise { - await this.page.getByText('Release').click() + await this.page.getByText('Release', { exact: true }).click() if (version === 'Major') { await this.page.getByText('Major').click() } @@ -459,6 +459,11 @@ export class DocumentContentPage extends DocumentCommonPage { await documentHistoryPage.checkHistoryEventExist(reason) } + async clickPreviousVersionHeader (userPage: Page, completeDocument: NewDocument, prevVersion: string): Promise { + const documentContentPageSecond = new DocumentContentPage(userPage) + await documentContentPageSecond.clickDocumentHeader(completeDocument.title + ' ' + prevVersion) + } + async addMessageToTheSectionTitle (title: string, message: string, closePopup: boolean = true): Promise { const locator = await this.getSectionLocator(title) const parentLocator = diff --git a/qms-tests/sanity/tests/model/types.ts b/qms-tests/sanity/tests/model/types.ts index 8e6c698c3c..92e1009118 100644 --- a/qms-tests/sanity/tests/model/types.ts +++ b/qms-tests/sanity/tests/model/types.ts @@ -32,7 +32,8 @@ export enum DocumentStatus { REVIEWED = 'Reviewed', REJECTED = 'Rejected', APPROVED = 'Approved', - EFFECTIVE = 'Effective' + EFFECTIVE = 'Effective', + ARCHIVED = 'Archived' } export interface DocumentDetails { From 32f0a7d9b2a540d25ba6b5e5766413cf52b05bca Mon Sep 17 00:00:00 2001 From: Kristina Date: Mon, 5 Aug 2024 19:16:25 +0400 Subject: [PATCH 03/20] Get rid of 'ne' in inbox queries (#6258) Signed-off-by: Kristina Fefelova --- models/notification/src/index.ts | 2 +- models/notification/src/migration.ts | 23 ++++++++++++++++++- .../src/components/inbox/Inbox.svelte | 4 ++-- .../src/inboxNotificationsClient.ts | 16 ++++++------- plugins/notification-resources/src/utils.ts | 2 +- plugins/notification/src/index.ts | 2 +- .../activity-resources/src/references.ts | 3 ++- .../notification-resources/src/index.ts | 1 + .../github/pod-github/src/notifications.ts | 1 + 9 files changed, 39 insertions(+), 15 deletions(-) diff --git a/models/notification/src/index.ts b/models/notification/src/index.ts index 2f7431ac37..19921bde15 100644 --- a/models/notification/src/index.ts +++ b/models/notification/src/index.ts @@ -228,7 +228,7 @@ export class TInboxNotification extends TDoc implements InboxNotification { isViewed!: boolean @Prop(TypeBoolean(), core.string.Boolean) - archived?: boolean + archived!: boolean title?: IntlString body?: IntlString diff --git a/models/notification/src/migration.ts b/models/notification/src/migration.ts index cc49e302b3..567f99608f 100644 --- a/models/notification/src/migration.ts +++ b/models/notification/src/migration.ts @@ -25,7 +25,8 @@ import notification, { notificationId, NotificationStatus, type BrowserNotification, - type DocNotifyContext + type DocNotifyContext, + type InboxNotification } from '@hcengineering/notification' import { DOMAIN_PREFERENCE } from '@hcengineering/preference' @@ -166,6 +167,26 @@ export const notificationOperation: MigrateOperation = { DOMAIN_USER_NOTIFY ) } + }, + { + state: 'fill-notification-archived-field-v1', + func: async (client) => { + await client.update( + DOMAIN_NOTIFICATION, + { _class: notification.class.ActivityInboxNotification, archived: { $exists: false } }, + { archived: false } + ) + await client.update( + DOMAIN_NOTIFICATION, + { _class: notification.class.CommonInboxNotification, archived: { $exists: false } }, + { archived: false } + ) + await client.update( + DOMAIN_NOTIFICATION, + { _class: notification.class.MentionInboxNotification, archived: { $exists: false } }, + { archived: false } + ) + } } ]) await client.deleteMany(DOMAIN_USER_NOTIFY, { diff --git a/plugins/notification-resources/src/components/inbox/Inbox.svelte b/plugins/notification-resources/src/components/inbox/Inbox.svelte index 495bbbdab8..0d6004c343 100644 --- a/plugins/notification-resources/src/components/inbox/Inbox.svelte +++ b/plugins/notification-resources/src/components/inbox/Inbox.svelte @@ -110,8 +110,8 @@ ) archivedOtherNotificationsQuery.query( - notification.class.InboxNotification, - { _class: { $ne: notification.class.ActivityInboxNotification }, archived: true, user: me._id }, + notification.class.CommonInboxNotification, + { archived: true, user: me._id }, (res) => { archivedOtherNotifications = res }, diff --git a/plugins/notification-resources/src/inboxNotificationsClient.ts b/plugins/notification-resources/src/inboxNotificationsClient.ts index 1e7ea66e24..dce208794e 100644 --- a/plugins/notification-resources/src/inboxNotificationsClient.ts +++ b/plugins/notification-resources/src/inboxNotificationsClient.ts @@ -34,6 +34,7 @@ import notification, { } from '@hcengineering/notification' import { createQuery, getClient } from '@hcengineering/presentation' import { derived, get, writable } from 'svelte/store' + import { isActivityNotification } from './utils' /** @@ -101,10 +102,9 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient { } ) this.otherInboxNotificationsQuery.query( - notification.class.InboxNotification, + notification.class.CommonInboxNotification, { - _class: { $ne: notification.class.ActivityInboxNotification }, - archived: { $ne: true }, + archived: false, user: getCurrentAccount()._id }, (result: InboxNotification[]) => { @@ -120,7 +120,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient { this.activityInboxNotificationsQuery.query( notification.class.ActivityInboxNotification, { - archived: { $ne: true }, + archived: false, user: getCurrentAccount()._id }, (result: ActivityInboxNotification[]) => { @@ -242,7 +242,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient { notification.class.InboxNotification, { user: getCurrentAccount()._id, - archived: { $ne: true } + archived: false }, { projection: { _id: 1, _class: 1, space: 1 } } ) @@ -267,8 +267,8 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient { notification.class.InboxNotification, { user: getCurrentAccount()._id, - isViewed: { $ne: true }, - archived: { $ne: true } + isViewed: false, + archived: false }, { projection: { _id: 1, _class: 1, space: 1 } } ) @@ -293,7 +293,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient { { user: getCurrentAccount()._id, isViewed: true, - archived: { $ne: true } + archived: false }, { projection: { _id: 1, _class: 1, space: 1, docNotifyContext: 1 }, diff --git a/plugins/notification-resources/src/utils.ts b/plugins/notification-resources/src/utils.ts index 5b6a75f15b..a44b2764d8 100644 --- a/plugins/notification-resources/src/utils.ts +++ b/plugins/notification-resources/src/utils.ts @@ -186,7 +186,7 @@ export async function archiveContextNotifications (doc?: DocNotifyContext): Prom try { const notifications = await ops.findAll( notification.class.InboxNotification, - { docNotifyContext: doc._id, archived: { $ne: true } }, + { docNotifyContext: doc._id, archived: false }, { projection: { _id: 1, _class: 1, space: 1 } } ) diff --git a/plugins/notification/src/index.ts b/plugins/notification/src/index.ts index fe282ea777..80cfb0c486 100644 --- a/plugins/notification/src/index.ts +++ b/plugins/notification/src/index.ts @@ -234,7 +234,7 @@ export interface InboxNotification extends Doc { body?: IntlString intlParams?: Record intlParamsNotLocalized?: Record - archived?: boolean + archived: boolean } export interface ActivityInboxNotification extends InboxNotification { diff --git a/server-plugins/activity-resources/src/references.ts b/server-plugins/activity-resources/src/references.ts index f65d49b876..f2021a96cc 100644 --- a/server-plugins/activity-resources/src/references.ts +++ b/server-plugins/activity-resources/src/references.ts @@ -157,7 +157,8 @@ export async function getPersonNotificationTxes ( mentionedIn: reference.attachedDocId ?? reference.srcDocId, mentionedInClass: reference.attachedDocClass ?? reference.srcDocClass, user: receiver._id, - isViewed: false + isViewed: false, + archived: false } const sender = ( diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index 2d6012e6b3..2169d34523 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -381,6 +381,7 @@ export async function pushInboxNotifications ( user: account._id, isViewed: false, docNotifyContext: docNotifyContextId, + archived: false, ...data } const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData) diff --git a/services/github/pod-github/src/notifications.ts b/services/github/pod-github/src/notifications.ts index 310359a0b1..65be33bb17 100644 --- a/services/github/pod-github/src/notifications.ts +++ b/services/github/pod-github/src/notifications.ts @@ -38,6 +38,7 @@ export async function createNotification ( message: data.message, props: data.props, isViewed: false, + archived: false, docNotifyContext: docNotifyContext?._id as Ref }) } From 94b87cea8f4e64ee6c6632c0d615198746662a77 Mon Sep 17 00:00:00 2001 From: Vyacheslav Tumanov Date: Mon, 5 Aug 2024 22:01:47 +0500 Subject: [PATCH 04/20] add assets for text-editor (#6259) Signed-off-by: Vyacheslav Tumanov --- desktop/src/ui/platform.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/desktop/src/ui/platform.ts b/desktop/src/ui/platform.ts index aaca275732..a9a8405a4b 100644 --- a/desktop/src/ui/platform.ts +++ b/desktop/src/ui/platform.ts @@ -92,6 +92,7 @@ import '@hcengineering/training-assets' import '@hcengineering/products-assets' import '@hcengineering/controlled-documents-assets' import '@hcengineering/analytics-collector-assets' +import '@hcengineering/text-editor-assets' import { coreId } from '@hcengineering/core' import presentation, { parsePreviewConfig, presentationId } from '@hcengineering/presentation' From b96c2e9b89c8d3fb2a2265c0894a5d72be31e84d Mon Sep 17 00:00:00 2001 From: Alexander Platov Date: Mon, 5 Aug 2024 21:25:40 +0300 Subject: [PATCH 05/20] Corrected test in the Planner (#6260) Signed-off-by: Alexander Platov --- tests/sanity/tests/model/planning/planning-page.ts | 4 ++-- tests/sanity/tests/planning/plan.spec.ts | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/sanity/tests/model/planning/planning-page.ts b/tests/sanity/tests/model/planning/planning-page.ts index 3cf324d284..f1a4a721c7 100644 --- a/tests/sanity/tests/model/planning/planning-page.ts +++ b/tests/sanity/tests/model/planning/planning-page.ts @@ -81,7 +81,7 @@ export class PlanningPage extends CalendarPage { readonly eventInSchedule = (title: string): Locator => this.schedule().locator('div.event-container', { hasText: title }) - async dragdropTomorrow (title: string, time: string): Promise { + async dragdropTomorrow (title: string, time: string, addHalf: boolean = false): Promise { await this.toDosContainer().getByRole('button', { name: title }).hover() await expect(async () => { @@ -90,7 +90,7 @@ export class PlanningPage extends CalendarPage { expect(boundingBox).toBeTruthy() if (boundingBox != null) { await this.page.mouse.move(boundingBox.x + 10, boundingBox.y + 10) - await this.page.mouse.move(boundingBox.x + 10, boundingBox.y + 20) + await this.page.mouse.move(boundingBox.x + 10, boundingBox.y + (addHalf ? 40 : 20)) await this.page.mouse.up() } }).toPass(retryOptions) diff --git a/tests/sanity/tests/planning/plan.spec.ts b/tests/sanity/tests/planning/plan.spec.ts index 606d28f46f..06d8df2009 100644 --- a/tests/sanity/tests/planning/plan.spec.ts +++ b/tests/sanity/tests/planning/plan.spec.ts @@ -182,8 +182,8 @@ test.describe('Planning ToDo tests', () => { let hour = new Date().getHours() const ampm = hour < 13 ? 'am' : 'pm' hour = hour < 1 ? 1 : hour >= 11 && hour < 13 ? 11 : hour >= 22 ? 10 : hour > 12 ? hour - 12 : hour - const timeV = `${hour}${ampm}` - const timeI = `${hour + 1}${ampm}` + const time = `${hour}${ampm}` + // const timeI = `${hour + 1}${ampm}` const leftSideMenuPage: LeftSideMenuPage = new LeftSideMenuPage(page) const loginPage: LoginPage = new LoginPage(page) @@ -201,7 +201,7 @@ test.describe('Planning ToDo tests', () => { await planningPage.selectInputToDo().fill(titleV) await planningPage.selectInputToDo().press('Enter') - await planningPage.dragdropTomorrow(titleV, timeV) + await planningPage.dragdropTomorrow(titleV, time) await planningPage.eventInSchedule(titleV).click() await planningPage.buttonPopupCreateVisible().click() await planningPage.buttonPopupVisibleToEveryone().click() @@ -209,7 +209,7 @@ test.describe('Planning ToDo tests', () => { await planningPage.selectInputToDo().fill(titleI) await planningPage.selectInputToDo().press('Enter') - await planningPage.dragdropTomorrow(titleI, timeI) + await planningPage.dragdropTomorrow(titleI, time, true) await planningPage.eventInSchedule(titleI).click() await planningPage.buttonPopupCreateVisible().click() await planningPage.buttonPopupOnlyVisibleToYou().click() From eb1d73801b0f0af02a43588ff8ed684cf2a93b79 Mon Sep 17 00:00:00 2001 From: Alexander Platov Date: Mon, 5 Aug 2024 23:57:14 +0300 Subject: [PATCH 06/20] The corrected color in the Button (#6262) Signed-off-by: Alexander Platov --- packages/theme/styles/_colors.scss | 1 + packages/theme/styles/button.scss | 1 + packages/ui/src/components/CircleButton.svelte | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/theme/styles/_colors.scss b/packages/theme/styles/_colors.scss index 40d2dc54be..6d5c260975 100644 --- a/packages/theme/styles/_colors.scss +++ b/packages/theme/styles/_colors.scss @@ -25,6 +25,7 @@ --primary-button-hovered: #3575DE; --primary-button-pressed: #1C52AB; --primary-button-focused: #205DC2; + --secondary-button-color: rgba(0, 0, 0, .8); --secondary-button-default: #D3E1F8; --secondary-button-hovered: #BDD2F5; --secondary-button-pressed: #A7C3F1; diff --git a/packages/theme/styles/button.scss b/packages/theme/styles/button.scss index 4159e78912..0a00cb20fd 100644 --- a/packages/theme/styles/button.scss +++ b/packages/theme/styles/button.scss @@ -241,6 +241,7 @@ } } &.secondary { + color: var(--secondary-button-color); background-color: var(--secondary-button-default); &:hover { background-color: var(--secondary-button-hovered); } &:active, diff --git a/packages/ui/src/components/CircleButton.svelte b/packages/ui/src/components/CircleButton.svelte index a53e382259..767e310706 100644 --- a/packages/ui/src/components/CircleButton.svelte +++ b/packages/ui/src/components/CircleButton.svelte @@ -110,6 +110,7 @@ } } &.primary { + color: var(--secondary-button-color); background-color: var(--secondary-button-default); &:hover { background-color: var(--secondary-button-hovered); From 5450a0754d8dda7a6564bc5293f00acc5ea85c3a Mon Sep 17 00:00:00 2001 From: Kristina Date: Tue, 6 Aug 2024 09:07:06 +0400 Subject: [PATCH 07/20] Get rid of 'ne'/'nin' in chat queries (#6265) --- models/chunter/src/migration.ts | 3 +- models/notification/src/index.ts | 2 +- models/notification/src/migration.ts | 10 ++++++ .../chat/create/CreateChannel.svelte | 3 +- .../chat/create/CreateDirectChat.svelte | 3 +- .../chat/navigator/ChatNavGroup.svelte | 31 +++++++------------ .../src/components/chat/types.ts | 6 ++-- .../src/components/chat/utils.ts | 25 +++++---------- plugins/notification-resources/src/utils.ts | 4 +-- plugins/notification/src/index.ts | 2 +- server-plugins/chunter-resources/src/index.ts | 5 +-- server-plugins/gmail-resources/src/index.ts | 3 +- .../notification-resources/src/index.ts | 3 +- .../telegram-resources/src/index.ts | 3 +- 14 files changed, 51 insertions(+), 52 deletions(-) diff --git a/models/chunter/src/migration.ts b/models/chunter/src/migration.ts index cc49f9c27b..afdb2c97e3 100644 --- a/models/chunter/src/migration.ts +++ b/models/chunter/src/migration.ts @@ -63,7 +63,8 @@ export async function createDocNotifyContexts ( await tx.createDoc(notification.class.DocNotifyContext, core.space.Space, { user: user._id, attachedTo, - attachedToClass + attachedToClass, + isPinned: false }) } } diff --git a/models/notification/src/index.ts b/models/notification/src/index.ts index 19921bde15..2ddcdfa44b 100644 --- a/models/notification/src/index.ts +++ b/models/notification/src/index.ts @@ -210,7 +210,7 @@ export class TDocNotifyContext extends TDoc implements DocNotifyContext { lastUpdateTimestamp?: Timestamp @Prop(TypeBoolean(), notification.string.Pinned) - isPinned?: boolean + isPinned!: boolean } @Model(notification.class.InboxNotification, core.class.Doc, DOMAIN_NOTIFICATION) diff --git a/models/notification/src/migration.ts b/models/notification/src/migration.ts index 567f99608f..f65cf386ab 100644 --- a/models/notification/src/migration.ts +++ b/models/notification/src/migration.ts @@ -187,6 +187,16 @@ export const notificationOperation: MigrateOperation = { { archived: false } ) } + }, + { + state: 'fill-contexts-pinned-field-v1', + func: async (client) => { + await client.update( + DOMAIN_DOC_NOTIFY, + { _class: notification.class.DocNotifyContext, isPinned: { $exists: false } }, + { isPinned: false } + ) + } } ]) await client.deleteMany(DOMAIN_USER_NOTIFY, { diff --git a/plugins/chunter-resources/src/components/chat/create/CreateChannel.svelte b/plugins/chunter-resources/src/components/chat/create/CreateChannel.svelte index 67bea5688f..9d48cb3d22 100644 --- a/plugins/chunter-resources/src/components/chat/create/CreateChannel.svelte +++ b/plugins/chunter-resources/src/components/chat/create/CreateChannel.svelte @@ -63,7 +63,8 @@ await client.createDoc(notification.class.DocNotifyContext, channelId, { user: accountId, attachedTo: channelId, - attachedToClass: chunter.class.Channel + attachedToClass: chunter.class.Channel, + isPinned: false }) openChannel(channelId, chunter.class.Channel) diff --git a/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte b/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte index 4b64e245b0..8bcf323547 100644 --- a/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte +++ b/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte @@ -89,7 +89,8 @@ await client.createDoc(notification.class.DocNotifyContext, dmId, { user: myAccId, attachedTo: dmId, - attachedToClass: chunter.class.DirectMessage + attachedToClass: chunter.class.DirectMessage, + isPinned: false }) openChannel(dmId, chunter.class.DirectMessage) diff --git a/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte b/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte index 51511eb35b..33de29497f 100644 --- a/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte +++ b/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte @@ -14,8 +14,8 @@ --> + +
+
+ {#each fields as field, index (field.name)} + {#if index === fields.length / 2} +
+ {/if} +
+ { + trim(field.name) + }} + /> +
+ {/each} +
+ + + diff --git a/packages/ui/src/components/CodeInput.svelte b/packages/ui/src/components/CodeInput.svelte index a62ec51296..fe0ea239c1 100644 --- a/packages/ui/src/components/CodeInput.svelte +++ b/packages/ui/src/components/CodeInput.svelte @@ -18,9 +18,10 @@ export let id: string | undefined = undefined export let name: string | undefined = undefined export let size: 'small' | 'medium' = 'small' + export let kind: 'primary' | 'secondary' = 'primary' -
+
import { getClient } from '@hcengineering/presentation' import notification, { NotificationProvider } from '@hcengineering/notification' - import { Icon, Label, ModernToggle } from '@hcengineering/ui' import core, { Ref } from '@hcengineering/core' + import { getResource } from '@hcengineering/platform' import { providersSettings } from '../../utils' + import ProviderPreferences from './ProviderPreferences.svelte' const client = getClient() const providers = client @@ -49,7 +50,10 @@ } } - async function onToggle (provider: NotificationProvider): Promise { + async function onToggle (event: CustomEvent): Promise { + const provider = event.detail + if (provider == null) return + const setting = $providersSettings.find(({ attachedTo }) => attachedTo === provider._id) const enabled = setting !== undefined ? !setting.enabled : !provider.defaultEnabled @@ -69,37 +73,16 @@ } -{#each providers as provider} - {@const setting = $providersSettings.find(({ attachedTo }) => attachedTo === provider._id)} - -
-
-
- - - -
- - -
- {#if provider.canDisable} - onToggle(provider)} - /> +
+ {#each providers as provider (provider._id)} + {#if provider.isAvailableFn} + {#await getResource(provider.isAvailableFn) then isAvailableFn} + {#if isAvailableFn()} + + {/if} + {/await} + {:else} + {/if} -
-{/each} - - + {/each} +
diff --git a/plugins/notification-resources/src/components/settings/NotificationGroupSetting.svelte b/plugins/notification-resources/src/components/settings/NotificationGroupSetting.svelte index 45e48249a9..a18df25410 100644 --- a/plugins/notification-resources/src/components/settings/NotificationGroupSetting.svelte +++ b/plugins/notification-resources/src/components/settings/NotificationGroupSetting.svelte @@ -22,7 +22,7 @@ NotificationTypeSetting, NotificationProviderDefaults } from '@hcengineering/notification' - import { IntlString } from '@hcengineering/platform' + import { getResource, IntlString } from '@hcengineering/platform' import { getClient } from '@hcengineering/presentation' import { Grid, Label, ModernToggle } from '@hcengineering/ui' @@ -130,26 +130,42 @@ return false } - $: filteredProviders = providers.filter((provider) => { - const providerSetting = $providersSettings.find((p) => p.attachedTo === provider._id) + async function getFilteredProviders ( + providers: NotificationProvider[], + types: BaseNotificationType[] + ): Promise { + const result: NotificationProvider[] = [] - if (providerSetting !== undefined && !providerSetting.enabled) { - return false + for (const provider of providers) { + const providerSetting = $providersSettings.find((p) => p.attachedTo === provider._id) + + if (providerSetting !== undefined && !providerSetting.enabled) continue + if (providerSetting === undefined && !provider.defaultEnabled) continue + + if (provider.isAvailableFn !== undefined) { + const isAvailableFn = await getResource(provider.isAvailableFn) + if (!isAvailableFn()) continue + } + + if (provider.ignoreAll === true) { + const ignoreExcluded = providerDefaults + .map((it) => (provider._id === it.provider && it.excludeIgnore !== undefined ? it.excludeIgnore : [])) + .flat() + + const included = types.some((type) => ignoreExcluded.includes(type._id)) + if (!included) continue + } + + result.push(provider) } - if (providerSetting === undefined && !provider.defaultEnabled) { - return false - } + return result + } - if (provider.ignoreAll === true) { - const ignoreExcluded = providerDefaults - .map((it) => (provider._id === it.provider && it.excludeIgnore !== undefined ? it.excludeIgnore : [])) - .flat() + let filteredProviders: NotificationProvider[] = [] - return types.some((type) => ignoreExcluded.includes(type._id)) - } - - return true + $: void getFilteredProviders(providers, types).then((result) => { + filteredProviders = result }) $: column = filteredProviders.length + 1 diff --git a/plugins/notification-resources/src/components/settings/ProviderPreferences.svelte b/plugins/notification-resources/src/components/settings/ProviderPreferences.svelte new file mode 100644 index 0000000000..f37fa3f103 --- /dev/null +++ b/plugins/notification-resources/src/components/settings/ProviderPreferences.svelte @@ -0,0 +1,72 @@ + + + + +
+
+
+
+ + + +
+ {#if provider.description} + + + {/if} +
+ {#if provider.canDisable} + dispatch('toggle', provider)} /> + {/if} +
+ {#if presenter} + + {/if} +
+ + diff --git a/plugins/notification/src/index.ts b/plugins/notification/src/index.ts index e6ce257a6a..978fff22d7 100644 --- a/plugins/notification/src/index.ts +++ b/plugins/notification/src/index.ts @@ -113,6 +113,7 @@ export interface NotificationTemplate { export interface NotificationContent { title: IntlString body: IntlString + data?: Markup intlParams: Record intlParamsNotLocalized?: Record } @@ -160,6 +161,8 @@ export interface NotificationProvider extends Doc { canDisable: boolean ignoreAll?: boolean order: number + presenter?: AnyComponent + isAvailableFn?: Resource<() => boolean> } export interface NotificationProviderDefaults extends Doc { @@ -232,6 +235,7 @@ export interface InboxNotification extends Doc { // For browser notifications title?: IntlString body?: IntlString + data?: Markup intlParams?: Record intlParamsNotLocalized?: Record archived: boolean diff --git a/plugins/telegram-assets/lang/en.json b/plugins/telegram-assets/lang/en.json index c295ff870a..d786f25c1d 100644 --- a/plugins/telegram-assets/lang/en.json +++ b/plugins/telegram-assets/lang/en.json @@ -26,6 +26,28 @@ "Status": "Status", "MessagesSelected": "messages selected", "ConfigLabel": "Telegram", - "ConfigDescription": "Extension to read and write Telegram messages" + "ConfigDescription": "Extension to read and write Telegram messages", + "ConnectBotInfoStart": "To start interacting with our bot, simply open", + "ConnectBotInfoEnd": "and click \"Start\".", + "ConnectBotError": "Looks like a connection could’t be automatically established. Please try reconnecting the bot by clicking “Start”.", + "TestConnection": "Test connection", + "Connected": "Connected", + "TelegramNotificationDescription": "Receive personal notifications in Telegram.", + "BotDescription": "With this bot you can get all notifications and reply to them without leaving Telegram.", + "BotShortDescription": "With this bot you can get all notifications and reply to them without leaving Telegram", + "WelcomeMessage": "Welcome! I am a bot that will help you to get all {app} notifications and reply to them without leaving Telegram.", + "TestMessage": "Your {app} account is connected. You will receive your notifications.", + "ConnectMessage": "⚠️Please connect me to your {app} account.\nSend /connect to generate code and paste it in {app} settings.\nCode valid for 1 minute.", + "StopMessage": "You will no longer receive notifications about {app} events. If you want to turn them back on, just send /start.", + "StartBot": "Start the bot", + "ConnectAccount": "Connect account via OTP", + "ShowCommandsDetails": "Show commands details", + "TurnNotificationsOff": "Turn notifications off", + "ConnectedDescriptionHtml": "✅Your are connected as {email}", + "AccountAlreadyConnectedHtml": "You are already connected as {email}", + "AccountConnectedHtml": "You have been successfully connected as {email}", + "AccountAlreadyConnected": "Account already connected", + "InvalidCode": "Invalid code", + "SomethingWentWrong": "Something went wrong. Please try again." } } \ No newline at end of file diff --git a/plugins/telegram-assets/lang/es.json b/plugins/telegram-assets/lang/es.json index 63a7663de9..24a8368674 100644 --- a/plugins/telegram-assets/lang/es.json +++ b/plugins/telegram-assets/lang/es.json @@ -26,6 +26,28 @@ "Status": "Estado", "MessagesSelected": "mensajes seleccionados", "ConfigLabel": "Telegram", - "ConfigDescription": "Extensión para leer y escribir mensajes de Telegram" + "ConfigDescription": "Extensión para leer y escribir mensajes de Telegram", + "ConnectBotInfoStart": "Para empezar a interactuar con nuestro bot, simplemente abre", + "ConnectBotInfoEnd": "y haz clic en “Start”.", + "ConnectBotError": "Parece que no se pudo establecer una conexión automáticamente. Por favor, intenta reconectar el bot haciendo clic en “Start”.", + "TestConnection": "Probar conexión", + "Connected": "Conectado", + "TelegramNotificationDescription": "Recibe notificaciones personales en Telegram.", + "BotDescription": "Con este bot puedes recibir todas las notificaciones y responder a ellas sin salir de Telegram.", + "BotShortDescription": "Con este bot puedes recibir todas las notificaciones y responder a ellas sin salir de Telegram", + "WelcomeMessage": "¡Bienvenido! Soy un bot que te ayudará a recibir todas las notificaciones de {app} y responder a ellas sin salir de Telegram.", + "TestMessage": "Tu cuenta de {app} está conectada. Recibirás tus notificaciones.", + "ConnectMessage": "⚠️Por favor, conéctame a tu cuenta de {app}.\nEnvía /connect para generar un código y pégalo en la configuración de {app}.\nCódigo válido durante 1 minuto.", + "StopMessage": "Ya no recibirás notificaciones sobre eventos de {app}. Si quieres volver a activarlas, simplemente envía /start.", + "StartBot": "Iniciar el bot", + "ConnectAccount": "Conectar cuenta a través de OTP", + "ShowCommandsDetails": "Mostrar detalles de los comandos", + "TurnNotificationsOff": "Desactivar notificaciones", + "ConnectedDescriptionHtml": "✅Estás conectado como {email}", + "AccountAlreadyConnectedHtml": "Ya estás conectado como {email}", + "AccountConnectedHtml": "Te has conectado correctamente como {email}", + "AccountAlreadyConnected": "Cuenta ya conectada", + "InvalidCode": "Código no válido", + "SomethingWentWrong": "Algo salió mal. Por favor, inténtalo de nuevo." } } \ No newline at end of file diff --git a/plugins/telegram-assets/lang/fr.json b/plugins/telegram-assets/lang/fr.json index ae7b10a68d..d7e3a087ba 100644 --- a/plugins/telegram-assets/lang/fr.json +++ b/plugins/telegram-assets/lang/fr.json @@ -26,6 +26,28 @@ "Status": "Statut", "MessagesSelected": "messages sélectionnés", "ConfigLabel": "Telegram", - "ConfigDescription": "Extension pour lire et écrire des messages Telegram" + "ConfigDescription": "Extension pour lire et écrire des messages Telegram", + "ConnectBotInfoStart": "Pour commencer à interagir avec notre bot, il suffit d'ouvrir", + "ConnectBotInfoEnd": "et cliquez sur “Start”.", + "ConnectBotError": "Il semble qu'une connexion n'ait pas pu être établie automatiquement. Veuillez essayer de reconnecter le bot en cliquant sur “Start”.", + "TestConnection": "Tester la connexion", + "Connected": "Connecté", + "TelegramNotificationDescription": "Recevez des notifications personnelles sur Telegram.", + "BotDescription": "Avec ce bot, vous pouvez obtenir toutes les notifications et y répondre sans quitter Telegram.", + "BotShortDescription": "Avec ce bot, vous pouvez obtenir toutes les notifications et y répondre sans quitter Telegram", + "WelcomeMessage": "Bienvenue! Je suis un bot qui vous aidera à recevoir toutes les notifications de {app} et à y répondre sans quitter Telegram.", + "TestMessage": "Votre compte {app} est connecté. Vous recevrez vos notifications.", + "ConnectMessage": "⚠️Veuillez me connecter à votre compte {app}.\nEnvoyez /connect pour générer un code et collez-le dans les", + "StopMessage": "Vous ne recevrez plus de notifications sur les événements de {app}. Si vous souhaitez les réactiver, envoyez simplement /start.", + "StartBot": "Démarrer le bot", + "ConnectAccount": "Connecter le compte via OTP", + "ShowCommandsDetails": "Afficher les détails des commandes", + "TurnNotificationsOff": "Désactiver les notifications", + "ConnectedDescriptionHtml": "✅Vous êtes connecté en tant que {email}", + "AccountAlreadyConnectedHtml": "Vous êtes déjà connecté en tant que {email}", + "AccountConnectedHtml": "Vous avez été connecté avec succès en tant que {email}", + "AccountAlreadyConnected": "Compte déjà connecté", + "InvalidCode": "Code invalide", + "SomethingWentWrong": "Quelque chose s'est mal passé. Veuillez réessayer." } } \ No newline at end of file diff --git a/plugins/telegram-assets/lang/pt.json b/plugins/telegram-assets/lang/pt.json index 7ca281a213..fb9e6ff12c 100644 --- a/plugins/telegram-assets/lang/pt.json +++ b/plugins/telegram-assets/lang/pt.json @@ -26,6 +26,28 @@ "Status": "Estado", "MessagesSelected": "mensagens selecionadas", "ConfigLabel": "Telegram", - "ConfigDescription": "Extensão para ler e escrever mensagens do Telegram" + "ConfigDescription": "Extensão para ler e escrever mensagens do Telegram", + "ConnectBotInfoStart": "Para começar a interagir com o nosso bot, basta abrir", + "ConnectBotInfoEnd": "e clicar em “Start”.", + "ConnectBotError": "Parece que não foi possível estabelecer uma ligação automaticamente. Por favor, tente reconectar o bot clicando em “Start”.", + "TestConnection": "Testar ligação", + "Connected": "Ligado", + "TelegramNotificationDescription": "Receba notificações pessoais no Telegram.", + "BotDescription": "Com este bot pode receber todas as notificações e responder-lhes sem sair do Telegram.", + "BotShortDescription": "Com este bot pode receber todas as notificações e responder-lhes sem sair do Telegram", + "WelcomeMessage": "Bem-vindo! Sou um bot que o ajudará a receber todas as notificações do {app} e a responder-lhes sem sair do Telegram.", + "TestMessage": "A sua conta {app} está ligada. Irá receber as suas notificações.", + "ConnectMessage": "⚠️Por favor, ligue-me à sua conta {app}.\nEnvie /connect para gerar um código e cole-o nas definições do {app}.\nCódigo válido durante 1 minuto.", + "StopMessage": "Já não receberá notificações sobre eventos do {app}. Se quiser voltar a ativá-las, basta enviar /start.", + "StartBot": "Iniciar o bot", + "ConnectAccount": "Ligar conta via OTP", + "ShowCommandsDetails": "Mostrar detalhes dos comandos", + "TurnNotificationsOff": "Desativar notificações", + "ConnectedDescriptionHtml": "✅Está ligado como {email}", + "AccountAlreadyConnectedHtml": "Já está ligado como {email}", + "AccountConnectedHtml": "Foi ligado com sucesso como {email}", + "AccountAlreadyConnected": "Conta já ligada", + "InvalidCode": "Código inválido", + "SomethingWentWrong": "Algo correu mal. Por favor, tente novamente." } } \ No newline at end of file diff --git a/plugins/telegram-assets/lang/ru.json b/plugins/telegram-assets/lang/ru.json index b64ea3ec8a..cea3288b84 100644 --- a/plugins/telegram-assets/lang/ru.json +++ b/plugins/telegram-assets/lang/ru.json @@ -26,6 +26,28 @@ "Status": "Статус", "MessagesSelected": "сообщений выбрано", "ConfigLabel": "Telegram", - "ConfigDescription": "Расширение по работе с Telegram" + "ConfigDescription": "Расширение по работе с Telegram", + "ConnectBotInfoStart": "Для начала работы с нашим ботом, просто откройте", + "ConnectBotInfoEnd": "и нажмите «Старт».", + "ConnectBotError": "Похоже, что подключение не установилось автоматически. Попробуйте переподключить бота, нажав «Старт».", + "TestConnection": "Проверить подключение", + "Connected": "Подключено", + "TelegramNotificationDescription": "Получайте уведомления в Telegram.", + "BotDescription": "С помощью этого бота вы можете получать все уведомления и отвечать на них, не покидая Telegram.", + "BotShortDescription": "С помощью этого бота вы можете получать все уведомления и отвечать на них, не покидая Telegram", + "WelcomeMessage": "Добро пожаловать! Я бот, который поможет вам получать все уведомления {app} и отвечать на них, не покидая Telegram.", + "TestMessage": "Ваш аккаунт {app} подключен. Вы будете получать уведомления.", + "ConnectMessage": "⚠️Пожалуйста, подключите бота к вашему аккаунту {app}.\nОтправьте /connect, чтобы сгенерировать код и вставить его в настройки {app}.\nКод действителен 1 minute.", + "StopMessage": "Вы больше не будете получать уведомления о событиях {app}. Если вы хотите их снова включить, просто отправьте /start.", + "StartBot": "Запустить бота", + "ConnectAccount": "Подключить аккаунт через OTP", + "ShowCommandsDetails": "Показать информацию о командах", + "TurnNotificationsOff": "Отключить уведомления", + "ConnectedDescriptionHtml": "✅Вы подключены как ${email}", + "AccountAlreadyConnectedHtml": "Вы уже подключены как {email}", + "AccountConnectedHtml": "Вы успешно подключены как {email}", + "AccountAlreadyConnected": "Аккаунт уже подключен", + "InvalidCode": "Неверный код", + "SomethingWentWrong": "Что-то пошло не так. Пожалуйста, попробуйте снова." } } \ No newline at end of file diff --git a/plugins/telegram-assets/lang/zh.json b/plugins/telegram-assets/lang/zh.json index 2e39159156..d5e781c9f5 100644 --- a/plugins/telegram-assets/lang/zh.json +++ b/plugins/telegram-assets/lang/zh.json @@ -26,6 +26,28 @@ "Status": "状态", "MessagesSelected": "选中的消息", "ConfigLabel": "Telegram", - "ConfigDescription": "扩展以读取和撰写 Telegram 消息" + "ConfigDescription": "扩展以读取和撰写 Telegram 消息", + "ConnectBotInfoStart": "要开始与我们的机器人互动,只需打开", + "ConnectBotInfoEnd": "然后单击“开始”。", + "ConnectBotError": "看起来无法自动建立连接。 请尝试通过单击“开始”重新连接机器人。", + "TestConnection": "测试连接", + "Connected": "已连接", + "TelegramNotificationDescription": "在 Telegram 中接收个人通知。", + "BotDescription": "使用此机器人,您可以获取所有通知并在不离开 Telegram 的情况下回复。", + "BotShortDescription": "使用此机器人,您可以获取所有通知并在不离开 Telegram 的情况下回复", + "WelcomeMessage": "欢迎! 我是一个机器人,将帮助您获取所有 {app} 通知并在不离开 Telegram 的情况下回复。", + "TestMessage": "您的 {app} 帐户已连接。 您将收到通知。", + "ConnectMessage": "⚠️请将我连接到您的 {app} 帐户。\n发送 /connect 以生成代码并将其粘贴到 {app} 设置中。\n代码有效期 1 分钟。", + "StopMessage": "您将不再收到有关 {app} 事件的通知。 如果您想重新启用它们,请发送 /start。", + "StartBot": "启动机器人", + "ConnectAccount": "通过 OTP 连接帐户", + "ShowCommandsDetails": "显示命令详细信息", + "TurnNotificationsOff": "关闭通知", + "ConnectedDescriptionHtml": "✅您已连接为 {email}", + "AccountAlreadyConnectedHtml": "您已连接为 {email}", + "AccountConnectedHtml": "您已成功连接为 {email}", + "AccountAlreadyConnected": "帐户已连接", + "InvalidCode": "无效代码", + "SomethingWentWrong": "出现问题。 请重试。" } } diff --git a/plugins/telegram-resources/package.json b/plugins/telegram-resources/package.json index 450462fad0..916770b445 100644 --- a/plugins/telegram-resources/package.json +++ b/plugins/telegram-resources/package.json @@ -43,6 +43,7 @@ "@hcengineering/contact-resources": "^0.6.0", "@hcengineering/core": "^0.6.32", "@hcengineering/login": "^0.6.12", + "@hcengineering/notification": "^0.6.23", "@hcengineering/notification-resources": "^0.6.0", "@hcengineering/panel": "^0.6.23", "@hcengineering/platform": "^0.6.11", diff --git a/plugins/telegram-resources/src/components/NotificationProviderPresenter.svelte b/plugins/telegram-resources/src/components/NotificationProviderPresenter.svelte new file mode 100644 index 0000000000..3fea750286 --- /dev/null +++ b/plugins/telegram-resources/src/components/NotificationProviderPresenter.svelte @@ -0,0 +1,189 @@ + + + + +{#if enabled} + {#if isLoading} +
+ +
+ {:else if info} +
+
+ + + {info.name} (@{info.username}) + + {#if isConnectionEstablished} + + + {/if} +
+ {#if connectionError} + + + {/if} +
+
+ + + {#if codeError} + + + {:else if isCodeValid} + + + {/if} +
+ {:else} + + + {/if} +{/if} + + diff --git a/plugins/telegram-resources/src/components/icons/TelegramColor.svelte b/plugins/telegram-resources/src/components/icons/TelegramColor.svelte index 18a879f9fb..040d911743 100644 --- a/plugins/telegram-resources/src/components/icons/TelegramColor.svelte +++ b/plugins/telegram-resources/src/components/icons/TelegramColor.svelte @@ -14,10 +14,15 @@ // limitations under the License. --> + + diff --git a/plugins/telegram-resources/src/index.ts b/plugins/telegram-resources/src/index.ts index 295e56af08..4c829f2a43 100644 --- a/plugins/telegram-resources/src/index.ts +++ b/plugins/telegram-resources/src/index.ts @@ -24,9 +24,10 @@ import Reconnect from './components/Reconnect.svelte' import IconTelegram from './components/icons/TelegramColor.svelte' import TelegramMessageCreated from './components/activity/TelegramMessageCreated.svelte' import MessagePresenter from './components/MessagePresenter.svelte' +import NotificationProviderPresenter from './components/NotificationProviderPresenter.svelte' import telegram from './plugin' -import { getCurrentEmployeeTG, getIntegrationOwnerTG } from './utils' +import { getCurrentEmployeeTG, getIntegrationOwnerTG, isTelegramNotificationsAvailable } from './utils' import SharedMessages from './components/SharedMessages.svelte' export default async (): Promise => ({ @@ -36,14 +37,16 @@ export default async (): Promise => ({ Reconnect, IconTelegram, SharedMessages, - MessagePresenter + MessagePresenter, + NotificationProviderPresenter }, activity: { TelegramMessageCreated }, function: { GetCurrentEmployeeTG: getCurrentEmployeeTG, - GetIntegrationOwnerTG: getIntegrationOwnerTG + GetIntegrationOwnerTG: getIntegrationOwnerTG, + IsTelegramNotificationsAvailable: isTelegramNotificationsAvailable }, handler: { DisconnectHandler: async () => { diff --git a/plugins/telegram-resources/src/plugin.ts b/plugins/telegram-resources/src/plugin.ts index 76385ba750..1765a3cd1b 100644 --- a/plugins/telegram-resources/src/plugin.ts +++ b/plugins/telegram-resources/src/plugin.ts @@ -35,7 +35,12 @@ export default mergeIds(telegramId, telegram, { Cancel: '' as IntlString, Share: '' as IntlString, PublishSelected: '' as IntlString, - MessagesSelected: '' as IntlString + MessagesSelected: '' as IntlString, + ConnectBotInfoStart: '' as IntlString, + ConnectBotInfoEnd: '' as IntlString, + ConnectBotError: '' as IntlString, + TestConnection: '' as IntlString, + Connected: '' as IntlString }, component: { MessagePresenter: '' as AnyComponent diff --git a/plugins/telegram-resources/src/utils.ts b/plugins/telegram-resources/src/utils.ts index 6e6e99f8b3..b0c20f3d02 100644 --- a/plugins/telegram-resources/src/utils.ts +++ b/plugins/telegram-resources/src/utils.ts @@ -20,6 +20,9 @@ import { getClient } from '@hcengineering/presentation' import setting from '@hcengineering/setting' import { type TemplateDataProvider } from '@hcengineering/templates' import { get } from 'svelte/store' +import { getMetadata } from '@hcengineering/platform' + +import telegram from './plugin' export async function getCurrentEmployeeTG (): Promise { const me = getCurrentAccount() as PersonAccount @@ -44,3 +47,9 @@ export async function getIntegrationOwnerTG (provider: TemplateDataProvider): Pr } } } + +export function isTelegramNotificationsAvailable (): boolean { + const botEndpoint = getMetadata(telegram.metadata.BotUrl) ?? '' + + return botEndpoint !== '' +} diff --git a/plugins/telegram/src/index.ts b/plugins/telegram/src/index.ts index bc34ab429d..83dd982395 100644 --- a/plugins/telegram/src/index.ts +++ b/plugins/telegram/src/index.ts @@ -14,9 +14,9 @@ // import { ChannelItem } from '@hcengineering/contact' -import type { AttachedDoc, Class, Doc, Ref, Timestamp } from '@hcengineering/core' -import { NotificationType } from '@hcengineering/notification' -import type { Plugin } from '@hcengineering/platform' +import { Account, AttachedDoc, Class, Doc, Ref, Timestamp } from '@hcengineering/core' +import { InboxNotification, NotificationProvider, NotificationType } from '@hcengineering/notification' +import type { IntlString, Plugin } from '@hcengineering/platform' import { Metadata, plugin } from '@hcengineering/platform' import type { Handler, IntegrationType } from '@hcengineering/setting' import { TemplateField } from '@hcengineering/templates' @@ -58,6 +58,16 @@ export interface SharedTelegramMessages extends AttachedDoc { messages: SharedTelegramMessage[] } +export interface TelegramNotificationRecord { + notificationId: Ref + workspace: string + account: Ref + title: string + quote?: string + body: string + sender: string +} + /** * @public */ @@ -91,6 +101,28 @@ export default plugin(telegramId, { IntegrationOwnerTG: '' as Ref }, metadata: { - TelegramURL: '' as Metadata + TelegramURL: '' as Metadata, + BotUrl: '' as Metadata + }, + providers: { + TelegramNotificationProvider: '' as Ref + }, + string: { + BotDescription: '' as IntlString, + BotShortDescription: '' as IntlString, + WelcomeMessage: '' as IntlString, + TestMessage: '' as IntlString, + ConnectMessage: '' as IntlString, + StopMessage: '' as IntlString, + StartBot: '' as IntlString, + ConnectAccount: '' as IntlString, + ShowCommandsDetails: '' as IntlString, + TurnNotificationsOff: '' as IntlString, + ConnectedDescriptionHtml: '' as IntlString, + AccountAlreadyConnectedHtml: '' as IntlString, + AccountConnectedHtml: '' as IntlString, + AccountAlreadyConnected: '' as IntlString, + InvalidCode: '' as IntlString, + SomethingWentWrong: '' as IntlString } }) diff --git a/products/tracker/src/platform.ts b/products/tracker/src/platform.ts index f628ee5d0b..5a1328da7c 100644 --- a/products/tracker/src/platform.ts +++ b/products/tracker/src/platform.ts @@ -66,6 +66,7 @@ export async function configurePlatform() { setMetadata(presentation.metadata.RequiredVersion, config.MODEL_VERSION) } setMetadata(telegram.metadata.TelegramURL, process.env.TELEGRAM_URL ?? 'http://localhost:8086') + setMetadata(telegram.metadata.BotUrl, process.TELEGRAM_BOT_URL ?? 'http://localhost:4020') setMetadata(gmail.metadata.GmailURL, process.env.GMAIL_URL ?? 'http://localhost:8087') setMetadata(uiPlugin.metadata.DefaultApplication, workbench.component.WorkbenchApp) diff --git a/server-plugins/activity-resources/src/index.ts b/server-plugins/activity-resources/src/index.ts index d2da99eea2..b4c5992a7f 100644 --- a/server-plugins/activity-resources/src/index.ts +++ b/server-plugins/activity-resources/src/index.ts @@ -13,23 +13,30 @@ // limitations under the License. // -import activity, { ActivityMessage, ActivityMessageControl, DocUpdateMessage, Reaction } from '@hcengineering/activity' +import activity, { + ActivityMessage, + ActivityMessageControl, + DocAttributeUpdates, + DocUpdateMessage, + Reaction +} from '@hcengineering/activity' import core, { Account, AttachedDoc, Class, + Collection, Data, Doc, + Hierarchy, + matchQuery, MeasureContext, Ref, + Space, Tx, - TxCUD, TxCollectionCUD, TxCreateDoc, - TxProcessor, - matchQuery, - Hierarchy, - Space + TxCUD, + TxProcessor } from '@hcengineering/core' import { ActivityControl, DocObjectCache } from '@hcengineering/server-activity' import type { TriggerControl } from '@hcengineering/server-core' @@ -43,7 +50,7 @@ import { PersonAccount } from '@hcengineering/contact' import { NotificationContent } from '@hcengineering/notification' import { getResource, translate } from '@hcengineering/platform' -import { getDocUpdateAction, getTxAttributesUpdates } from './utils' +import { getAttrName, getCollectionAttribute, getDocUpdateAction, getTxAttributesUpdates } from './utils' import { ReferenceTrigger } from './references' export async function OnReactionChanged (originTx: Tx, control: TriggerControl): Promise { @@ -443,6 +450,7 @@ async function ReactionNotificationContentProvider ( return { title: activity.string.ReactionNotificationTitle, body: activity.string.ReactionNotificationBody, + data: reaction.emoji, intlParams: { title: text, reaction: reaction.emoji @@ -450,6 +458,70 @@ async function ReactionNotificationContentProvider ( } } +async function getAttributesUpdatesText ( + attributeUpdates: DocAttributeUpdates, + objectClass: Ref>, + hierarchy: Hierarchy +): Promise { + const attrName = await getAttrName(attributeUpdates, objectClass, hierarchy) + + if (attrName === undefined) { + return undefined + } + + if (attributeUpdates.added.length > 0) { + return await translate(activity.string.NewObject, { object: attrName }) + } + if (attributeUpdates.removed.length > 0) { + return await translate(activity.string.RemovedObject, { object: attrName }) + } + + if (attributeUpdates.set.length > 0) { + const values = attributeUpdates.set + const isUnset = values.length > 0 && !values.some((value) => value !== null && value !== '') + + if (isUnset) { + return await translate(activity.string.UnsetObject, { object: attrName }) + } else { + return await translate(activity.string.ChangedObject, { object: attrName }) + } + } + + return undefined +} + +export async function DocUpdateMessageTextPresenter (doc: DocUpdateMessage, control: TriggerControl): Promise { + const { hierarchy } = control + const { attachedTo, attachedToClass, objectClass, objectId, action, updateCollection, attributeUpdates } = doc + const isOwn = attachedTo === objectId + + const collectionAttribute = getCollectionAttribute(hierarchy, attachedToClass, updateCollection) + const clazz = hierarchy.getClass(objectClass) + const objectName = (collectionAttribute?.type as Collection)?.itemLabel ?? clazz.label + const collectionName = collectionAttribute?.label + + const name = + isOwn || collectionName === undefined ? await translate(objectName, {}) : await translate(collectionName, {}) + + if (action === 'create') { + return await translate(activity.string.NewObject, { object: name }) + } + + if (action === 'remove') { + return await translate(activity.string.RemovedObject, { object: name }) + } + + if (action === 'update' && attributeUpdates !== undefined) { + const text = await getAttributesUpdatesText(attributeUpdates, objectClass, hierarchy) + + if (text !== undefined) { + return text + } + } + + return await translate(activity.string.UpdatedObject, { object: name }) +} + export * from './references' // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -461,6 +533,7 @@ export default async () => ({ OnReactionChanged }, function: { - ReactionNotificationContentProvider + ReactionNotificationContentProvider, + DocUpdateMessageTextPresenter } }) diff --git a/server-plugins/activity-resources/src/utils.ts b/server-plugins/activity-resources/src/utils.ts index 632f5ce0e8..23070abd10 100644 --- a/server-plugins/activity-resources/src/utils.ts +++ b/server-plugins/activity-resources/src/utils.ts @@ -1,7 +1,9 @@ import { Account, AttachedDoc, + type Attribute, Class, + Collection, Doc, Hierarchy, Mixin, @@ -20,6 +22,7 @@ import { ActivityControl, DocObjectCache, getAllObjectTransactions } from '@hcen import { getDocCollaborators } from '@hcengineering/server-notification-resources' import notification from '@hcengineering/notification' import { TriggerControl } from '@hcengineering/server-core' +import { translate } from '@hcengineering/platform' function getAvailableAttributesKeys (tx: TxCUD, hierarchy: Hierarchy): string[] { if (hierarchy.isDerived(tx._class, core.class.TxUpdateDoc)) { @@ -345,3 +348,59 @@ function getHiddenAttrs (hierarchy: Hierarchy, _class: Ref>): Set attr.hidden === true).map(([k]) => k) ) } + +export async function getAttrName ( + attributeUpdates: DocAttributeUpdates, + objectClass: Ref>, + hierarchy: Hierarchy +): Promise { + const { attrKey, attrClass, isMixin } = attributeUpdates + let attrObjectClass = objectClass + + try { + if (isMixin) { + const keyedAttribute = [...hierarchy.getAllAttributes(attrClass).entries()] + .filter(([, value]) => value.hidden !== true) + .map(([key, attr]) => ({ key, attr })) + .find(({ key }) => key === attrKey) + if (keyedAttribute === undefined) { + return undefined + } + attrObjectClass = keyedAttribute.attr.attributeOf + } + + const attribute = hierarchy.getAttribute(attrObjectClass, attrKey) + + const label = attribute.shortLabel ?? attribute.label + + if (label === undefined) { + return undefined + } + + return await translate(label, {}) + } catch (e) { + console.error(e) + return undefined + } +} + +export function getCollectionAttribute ( + hierarchy: Hierarchy, + objectClass: Ref>, + collection?: string +): Attribute> | undefined { + if (collection === undefined) { + return undefined + } + + const descendants = hierarchy.getDescendants(objectClass) + + for (const descendant of descendants) { + const collectionAttribute = hierarchy.findAttribute(descendant, collection) + if (collectionAttribute !== undefined) { + return collectionAttribute + } + } + + return undefined +} diff --git a/server-plugins/activity/src/index.ts b/server-plugins/activity/src/index.ts index 5be21e4bb8..f8773449b6 100644 --- a/server-plugins/activity/src/index.ts +++ b/server-plugins/activity/src/index.ts @@ -15,7 +15,7 @@ import { Plugin, Resource, plugin } from '@hcengineering/platform' import type { TriggerFunc } from '@hcengineering/server-core' -import { NotificationContentProvider } from '@hcengineering/server-notification' +import { NotificationContentProvider, Presenter } from '@hcengineering/server-notification' export * from './types' export * from './utils' @@ -36,6 +36,7 @@ export default plugin(serverActivityId, { ReferenceTrigger: '' as Resource }, function: { - ReactionNotificationContentProvider: '' as Resource + ReactionNotificationContentProvider: '' as Resource, + DocUpdateMessageTextPresenter: '' as Resource } }) diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts index 0ff3d2fdf8..13ae584fc8 100644 --- a/server-plugins/chunter-resources/src/index.ts +++ b/server-plugins/chunter-resources/src/index.ts @@ -84,7 +84,7 @@ export async function channelTextPresenter (doc: Doc): Promise { return await translate(chunter.string.Direct, {}) } - return `${channel.name}` + return `#${channel.name}` } export async function ChatMessageTextPresenter (doc: ChatMessage): Promise { @@ -350,6 +350,7 @@ export async function getChunterNotificationContent ( return { title, body, + data: message, intlParams, intlParamsNotLocalized } diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index c7eb273f78..a05d672037 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -472,7 +472,7 @@ async function mentionInboxNotificationToText ( return await commonInboxNotificationToText(doc) } -async function getTranslatedNotificationContent ( +export async function getTranslatedNotificationContent ( data: Data, _class: Ref>, control: TriggerControl @@ -617,7 +617,7 @@ export async function pushActivityInboxNotifications ( return } - const content = await getNotificationContent(originTx, target.account, sender, object, control) + const content = await getNotificationContent(originTx, target.account, sender, object, control, activityMessage) const data: Partial> = { ...content, attachedTo: activityMessage._id, diff --git a/server-plugins/notification-resources/src/utils.ts b/server-plugins/notification-resources/src/utils.ts index 42a582c892..acbd9da66a 100644 --- a/server-plugins/notification-resources/src/utils.ts +++ b/server-plugins/notification-resources/src/utils.ts @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. // -import { DocUpdateMessage } from '@hcengineering/activity' import { Analytics } from '@hcengineering/analytics' import contact, { formatName, PersonAccount } from '@hcengineering/contact' import core, { @@ -50,6 +49,7 @@ import serverNotification, { TextPresenter, UserInfo } from '@hcengineering/server-notification' +import { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity' import { NotifyResult } from './types' @@ -340,10 +340,13 @@ async function getFallbackNotificationFullfillment ( object: Doc, originTx: TxCUD, control: TriggerControl, - sender: UserInfo + sender: UserInfo, + message?: ActivityMessage ): Promise { const title: IntlString = notification.string.CommonNotificationTitle let body: IntlString = notification.string.CommonNotificationBody + let data: string | undefined + const intlParams: Record = {} const intlParamsNotLocalized: Record = {} @@ -353,6 +356,15 @@ async function getFallbackNotificationFullfillment ( intlParams.title = await textPresenterFunc(object, control) } + if (message !== undefined) { + const dataPresenter = getTextPresenter(message._class, control.hierarchy) + + if (dataPresenter !== undefined) { + const textPresenterFunc = await getResource(dataPresenter.presenter) + data = await textPresenterFunc(message, control) + } + } + const tx = TxProcessor.extractTx(originTx) intlParams.senderName = await getSenderName(control, sender) @@ -397,7 +409,7 @@ async function getFallbackNotificationFullfillment ( } } - return { title, body, intlParams, intlParamsNotLocalized } + return { title, body, data, intlParams, intlParamsNotLocalized } } function getNotificationPresenter (_class: Ref>, hierarchy: Hierarchy): NotificationPresenter | undefined { @@ -409,30 +421,34 @@ export async function getNotificationContent ( targetUser: PersonAccount, sender: UserInfo, object: Doc, - control: TriggerControl + control: TriggerControl, + message?: ActivityMessage ): Promise { - let { title, body, intlParams, intlParamsNotLocalized } = await getFallbackNotificationFullfillment( + let { title, body, data, intlParams, intlParamsNotLocalized } = await getFallbackNotificationFullfillment( object, originTx, control, - sender + sender, + message ) - const actualTx = TxProcessor.extractTx(originTx) - const notificationPresenter = getNotificationPresenter((actualTx as TxCUD).objectClass, control.hierarchy) + const actualTx = TxProcessor.extractTx(originTx) as TxCUD + const notificationPresenter = getNotificationPresenter(actualTx.objectClass, control.hierarchy) + if (notificationPresenter !== undefined) { const getFuillfillmentParams = await getResource(notificationPresenter.presenter) - const updateIntlParams = await getFuillfillmentParams(object, originTx, targetUser._id, control) - title = updateIntlParams.title - body = updateIntlParams.body + const updateParams = await getFuillfillmentParams(object, originTx, targetUser._id, control) + title = updateParams.title + body = updateParams.body + data = updateParams?.data ?? data intlParams = { ...intlParams, - ...updateIntlParams.intlParams + ...updateParams.intlParams } - if (updateIntlParams.intlParamsNotLocalized != null) { + if (updateParams.intlParamsNotLocalized != null) { intlParamsNotLocalized = { ...intlParamsNotLocalized, - ...updateIntlParams.intlParamsNotLocalized + ...updateParams.intlParamsNotLocalized } } } @@ -440,6 +456,7 @@ export async function getNotificationContent ( const content: NotificationContent = { title, body, + data, intlParams } diff --git a/server-plugins/notification/src/index.ts b/server-plugins/notification/src/index.ts index 331cf45822..86329693b1 100644 --- a/server-plugins/notification/src/index.ts +++ b/server-plugins/notification/src/index.ts @@ -156,7 +156,7 @@ export interface NotificationProviderResources extends Doc { } export const NOTIFICATION_BODY_SIZE = 50 -export const NOTIFICATION_TITLE_SIZE = 30 +export const NOTIFICATION_TITLE_SIZE = 50 /** * @public diff --git a/server-plugins/telegram-resources/package.json b/server-plugins/telegram-resources/package.json index 9bba505746..80f8f91b6c 100644 --- a/server-plugins/telegram-resources/package.json +++ b/server-plugins/telegram-resources/package.json @@ -37,12 +37,19 @@ "@types/jest": "^29.5.5" }, "dependencies": { + "@hcengineering/activity": "^0.6.0", + "@hcengineering/contact": "^0.6.24", + "@hcengineering/chunter": "^0.6.20", "@hcengineering/core": "^0.6.32", + "@hcengineering/notification": "^0.6.23", "@hcengineering/platform": "^0.6.11", "@hcengineering/server-core": "^0.6.1", - "@hcengineering/contact": "^0.6.24", - "@hcengineering/notification": "^0.6.23", + "@hcengineering/server-token": "^0.6.11", + "@hcengineering/server-notification": "^0.6.1", + "@hcengineering/server-telegram": "^0.6.0", + "@hcengineering/server-notification-resources": "^0.6.0", + "@hcengineering/setting": "^0.6.17", "@hcengineering/telegram": "^0.6.21", - "@hcengineering/setting": "^0.6.17" + "@hcengineering/text": "^0.6.5" } } diff --git a/server-plugins/telegram-resources/src/index.ts b/server-plugins/telegram-resources/src/index.ts index 29106007ea..c5d5a81efb 100644 --- a/server-plugins/telegram-resources/src/index.ts +++ b/server-plugins/telegram-resources/src/index.ts @@ -13,24 +13,34 @@ // limitations under the License. // -import contact, { Channel, ChannelProvider, Contact, Employee, PersonAccount } from '@hcengineering/contact' +import contact, { Channel, ChannelProvider, Contact, Employee, formatName, PersonAccount } from '@hcengineering/contact' import { Account, Class, + concatLink, Doc, DocumentQuery, FindOptions, FindResult, Hierarchy, Ref, + toWorkspaceString, Tx, TxCreateDoc, TxProcessor } from '@hcengineering/core' import { TriggerControl } from '@hcengineering/server-core' -import telegram, { TelegramMessage } from '@hcengineering/telegram' -import notification, { NotificationType } from '@hcengineering/notification' +import telegram, { TelegramMessage, TelegramNotificationRecord } from '@hcengineering/telegram' +import notification, { BaseNotificationType, InboxNotification, NotificationType } from '@hcengineering/notification' import setting, { Integration } from '@hcengineering/setting' +import { NotificationProviderFunc, UserInfo } from '@hcengineering/server-notification' +import { getMetadata, getResource } from '@hcengineering/platform' +import serverTelegram from '@hcengineering/server-telegram' +import { getTranslatedNotificationContent, getTextPresenter } from '@hcengineering/server-notification-resources' +import { generateToken } from '@hcengineering/server-token' +import chunter, { ChatMessage } from '@hcengineering/chunter' +import { markupToHTML } from '@hcengineering/text' +import activity from '@hcengineering/activity' /** * @public @@ -174,6 +184,107 @@ async function getContactChannel ( return res?.value ?? '' } +async function getTranslatedData ( + data: InboxNotification, + doc: Doc, + control: TriggerControl +): Promise<{ + title: string + quote: string | undefined + body: string + }> { + const { hierarchy } = control + + let { title, body } = await getTranslatedNotificationContent(data, data._class, control) + let quote: string | undefined + + if (hierarchy.isDerived(doc._class, chunter.class.ChatMessage)) { + const chatMessage = doc as ChatMessage + title = '' + quote = markupToHTML(chatMessage.message) + } else if (hierarchy.isDerived(doc._class, activity.class.ActivityMessage)) { + const resource = getTextPresenter(doc._class, control.hierarchy) + + if (resource !== undefined) { + const fn = await getResource(resource.presenter) + const textData = await fn(doc, control) + if (textData !== undefined && textData !== '') { + title = '' + quote = markupToHTML(textData) + } + } + } + body = data.data !== undefined ? `${markupToHTML(data.data)}` : body + + return { + title, + quote, + body + } +} + +const SendTelegramNotifications: NotificationProviderFunc = async ( + control: TriggerControl, + types: BaseNotificationType[], + doc: Doc, + data: InboxNotification, + receiver: UserInfo, + sender: UserInfo +): Promise => { + if (types.length === 0) { + return [] + } + + if (receiver.person === undefined || receiver.account?.email === undefined) { + return [] + } + + const botUrl = getMetadata(serverTelegram.metadata.BotUrl) + + if (botUrl === undefined || botUrl === '') { + console.log('Please provide telegram bot service url to enable telegram notifications.') + return [] + } + + const isEmployee = control.hierarchy.hasMixin(receiver.person, contact.mixin.Employee) + + if (!isEmployee) { + return [] + } + + const employee = control.hierarchy.as(receiver.person, contact.mixin.Employee) + + if (!employee.active) { + return [] + } + + try { + const { title, body, quote } = await getTranslatedData(data, doc, control) + const record: TelegramNotificationRecord = { + notificationId: data._id, + account: receiver._id, + workspace: toWorkspaceString(control.workspace), + sender: data.intlParams?.senderName?.toString() ?? formatName(sender.person?.name ?? 'System'), + title, + quote, + body + } + + await fetch(concatLink(botUrl, '/notify'), { + method: 'POST', + headers: { + Authorization: 'Bearer ' + generateToken(receiver.account.email, control.workspace), + 'Content-Type': 'application/json' + }, + body: JSON.stringify([record]) + }) + } catch (err) { + console.log('Could not send telegram notification', err) + } + + return [] +} + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { @@ -183,6 +294,7 @@ export default async () => ({ IsIncomingMessage, FindMessages, GetCurrentEmployeeTG, - GetIntegrationOwnerTG + GetIntegrationOwnerTG, + SendTelegramNotifications } }) diff --git a/server-plugins/telegram/src/index.ts b/server-plugins/telegram/src/index.ts index 1cafa9cf90..1b57fc9fbd 100644 --- a/server-plugins/telegram/src/index.ts +++ b/server-plugins/telegram/src/index.ts @@ -14,10 +14,9 @@ // limitations under the License. // -import type { Plugin, Resource } from '@hcengineering/platform' -import { plugin } from '@hcengineering/platform' +import { Metadata, Plugin, Resource, plugin } from '@hcengineering/platform' import { ObjectDDParticipantFunc, TriggerFunc } from '@hcengineering/server-core' -import { TypeMatchFunc } from '@hcengineering/server-notification' +import { NotificationProviderFunc, TypeMatchFunc } from '@hcengineering/server-notification' import { TemplateFieldServerFunc } from '@hcengineering/server-templates' /** @@ -29,6 +28,9 @@ export const serverTelegramId = 'server-telegram' as Plugin * @public */ export default plugin(serverTelegramId, { + metadata: { + BotUrl: '' as Metadata + }, trigger: { OnMessageCreate: '' as Resource }, @@ -36,6 +38,7 @@ export default plugin(serverTelegramId, { IsIncomingMessage: '' as TypeMatchFunc, FindMessages: '' as Resource, GetCurrentEmployeeTG: '' as Resource, - GetIntegrationOwnerTG: '' as Resource + GetIntegrationOwnerTG: '' as Resource, + SendTelegramNotifications: '' as Resource } }) From 1c1d0bc538e0e5d73ddf44119483ad3f13b6ecbe Mon Sep 17 00:00:00 2001 From: Kristina Date: Tue, 6 Aug 2024 13:44:23 +0400 Subject: [PATCH 10/20] Move notifications to person spaces (#6263) Signed-off-by: Kristina Fefelova --- .vscode/launch.json | 1 + desktop-package/package.json | 2 +- desktop/package.json | 2 +- models/chunter/src/migration.ts | 25 ++-- models/contact/src/index.ts | 14 +- models/contact/src/migration.ts | 81 ++++++++--- models/notification/package.json | 5 +- models/notification/src/index.ts | 16 ++- models/notification/src/migration.ts | 116 ++++++++++++++++ packages/ui/src/components/ListView.svelte | 61 ++++---- .../src/components/Channel.svelte | 2 +- .../src/components/chat/ChatAside.svelte | 2 +- .../chat/create/CreateChannel.svelte | 21 ++- .../chat/create/CreateDirectChat.svelte | 21 +-- .../chat/navigator/ChatNavGroup.svelte | 14 +- .../chat/navigator/ChatNavSection.svelte | 4 +- .../chat/navigator/ChatNavigator.svelte | 2 +- .../src/components/chat/utils.ts | 2 +- .../ChatMessageNotificationLabel.svelte | 4 +- plugins/chunter-resources/src/utils.ts | 36 +++-- plugins/contact/src/index.ts | 7 +- plugins/notification-assets/lang/en.json | 3 +- plugins/notification-assets/lang/es.json | 3 +- plugins/notification-assets/lang/fr.json | 3 +- plugins/notification-assets/lang/pt.json | 3 +- plugins/notification-assets/lang/ru.json | 3 +- plugins/notification-assets/lang/zh.json | 3 +- .../components/DocNotifyContextCard.svelte | 64 +++++++-- .../DocNotifyContextPresenter.svelte | 14 +- .../src/components/NotifyContextIcon.svelte | 6 +- .../src/components/inbox/Inbox.svelte | 14 +- .../inbox/InboxGroupedListView.svelte | 1 + .../src/inboxNotificationsClient.ts | 6 +- plugins/notification-resources/src/utils.ts | 45 +++--- plugins/notification/package.json | 1 + plugins/notification/src/index.ts | 16 ++- .../activity-resources/src/references.ts | 29 ++-- server-plugins/chunter-resources/src/index.ts | 32 +++-- server-plugins/contact-resources/src/index.ts | 44 +++++- server-plugins/gmail-resources/src/index.ts | 71 ++-------- .../notification-resources/src/index.ts | 131 +++++++----------- .../notification-resources/src/utils.ts | 65 +++++++-- server-plugins/notification/src/index.ts | 15 +- server-plugins/request-resources/src/index.ts | 10 +- .../telegram-resources/src/index.ts | 64 +-------- server-plugins/time-resources/src/index.ts | 55 ++++---- server/account/src/operations.ts | 2 +- server/mongo/src/storage.ts | 3 +- server/tool/src/index.ts | 2 +- .../github/pod-github/src/notifications.ts | 15 +- services/github/pod-github/src/platform.ts | 54 +++++--- services/github/pod-github/src/worker.ts | 14 +- 52 files changed, 729 insertions(+), 500 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 829d0722da..d28b8c8952 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -192,6 +192,7 @@ "MINIO_ACCESS_KEY": "minioadmin", "MINIO_SECRET_KEY": "minioadmin", "MINIO_ENDPOINT": "localhost", + "TRANSACTOR_URL": "ws://localhost:3333", "MONGO_URL": "mongodb://localhost:27017", "ACCOUNTS_URL": "http://localhost:3000", "TELEGRAM_DATABASE": "telegram-service", diff --git a/desktop-package/package.json b/desktop-package/package.json index 0b7e3cb98d..2291d5c053 100644 --- a/desktop-package/package.json +++ b/desktop-package/package.json @@ -1,6 +1,6 @@ { "name": "desktop", - "version": "0.6.266", + "version": "0.6.271", "main": "dist/main/electron.js", "author": "Hardcore Engineering ", "template": "@hcengineering/default-package", diff --git a/desktop/package.json b/desktop/package.json index 686f32fe53..b6aa591571 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@hcengineering/desktop", - "version": "0.6.266", + "version": "0.6.271", "main": "dist/main/electron.js", "template": "@hcengineering/webpack-package", "scripts": { diff --git a/models/chunter/src/migration.ts b/models/chunter/src/migration.ts index afdb2c97e3..ef1e2b4725 100644 --- a/models/chunter/src/migration.ts +++ b/models/chunter/src/migration.ts @@ -44,14 +44,14 @@ export const DOMAIN_COMMENT = 'comment' as Domain export async function createDocNotifyContexts ( client: MigrationUpgradeClient, tx: TxOperations, - attachedTo: Ref, - attachedToClass: Ref> + objectId: Ref, + objectClass: Ref>, + objectSpace: Ref ): Promise { const users = await client.findAll(core.class.Account, {}) const docNotifyContexts = await client.findAll(notification.class.DocNotifyContext, { user: { $in: users.map((it) => it._id) }, - attachedTo, - attachedToClass + objectId }) for (const user of users) { if (user._id === core.account.System) { @@ -62,8 +62,9 @@ export async function createDocNotifyContexts ( if (docNotifyContext === undefined) { await tx.createDoc(notification.class.DocNotifyContext, core.space.Space, { user: user._id, - attachedTo, - attachedToClass, + objectId, + objectClass, + objectSpace, isPinned: false }) } @@ -94,7 +95,7 @@ export async function createGeneral (client: MigrationUpgradeClient, tx: TxOpera topic: 'General Channel', private: false, archived: false, - members: await getAllEmployeeAccounts(tx), + members: await getAllPersonAccounts(tx), autoJoin: true }, chunter.space.General @@ -102,10 +103,10 @@ export async function createGeneral (client: MigrationUpgradeClient, tx: TxOpera } } - await createDocNotifyContexts(client, tx, chunter.space.General, chunter.class.Channel) + await createDocNotifyContexts(client, tx, chunter.space.General, chunter.class.Channel, core.space.Space) } -async function getAllEmployeeAccounts (tx: TxOperations): Promise[]> { +async function getAllPersonAccounts (tx: TxOperations): Promise[]> { const employees = await tx.findAll(contactPlugin.mixin.Employee, { active: true }) const accounts = await tx.findAll(contactPlugin.class.PersonAccount, { person: { $in: employees.map((it) => it._id) } @@ -114,7 +115,7 @@ async function getAllEmployeeAccounts (tx: TxOperations): Promise { - const accs = await getAllEmployeeAccounts(tx) + const accs = await getAllPersonAccounts(tx) const newMembers: Ref[] = [...current.members] for (const acc of accs) { if (!newMembers.includes(acc)) { @@ -150,7 +151,7 @@ export async function createRandom (client: MigrationUpgradeClient, tx: TxOperat topic: 'Random Talks', private: false, archived: false, - members: await getAllEmployeeAccounts(tx), + members: await getAllPersonAccounts(tx), autoJoin: true }, chunter.space.Random @@ -158,7 +159,7 @@ export async function createRandom (client: MigrationUpgradeClient, tx: TxOperat } } - await createDocNotifyContexts(client, tx, chunter.space.Random, chunter.class.Channel) + await createDocNotifyContexts(client, tx, chunter.space.Random, chunter.class.Channel, core.space.Space) } async function convertCommentsToChatMessages (client: MigrationClient): Promise { diff --git a/models/contact/src/index.ts b/models/contact/src/index.ts index 9bc7c59686..b4f6b18f28 100644 --- a/models/contact/src/index.ts +++ b/models/contact/src/index.ts @@ -29,7 +29,8 @@ import { type Organization, type Person, type PersonAccount, - type Status + type Status, + type PersonSpace } from '@hcengineering/contact' import { AccountRole, @@ -64,7 +65,7 @@ import { } from '@hcengineering/model' import attachment from '@hcengineering/model-attachment' import chunter from '@hcengineering/model-chunter' -import core, { TAccount, TAttachedDoc, TDoc } from '@hcengineering/model-core' +import core, { TAccount, TAttachedDoc, TDoc, TSpace } from '@hcengineering/model-core' import { createPublicLinkAction } from '@hcengineering/model-guest' import { generateClassNotificationTypes } from '@hcengineering/model-notification' import presentation from '@hcengineering/model-presentation' @@ -219,6 +220,12 @@ export class TContactsTab extends TDoc implements ContactsTab { index!: number } +@Model(contact.class.PersonSpace, core.class.Space) +export class TPersonSpace extends TSpace implements PersonSpace { + @Prop(TypeRef(contact.class.Person), contact.string.Person) + person!: Ref +} + export function createModel (builder: Builder): void { builder.createModel( TAvatarProvider, @@ -231,7 +238,8 @@ export function createModel (builder: Builder): void { TChannel, TStatus, TMember, - TContactsTab + TContactsTab, + TPersonSpace ) builder.mixin(contact.class.Contact, core.class.Class, activity.mixin.ActivityDoc, {}) diff --git a/models/contact/src/migration.ts b/models/contact/src/migration.ts index 506cc09d57..dc7989ea55 100644 --- a/models/contact/src/migration.ts +++ b/models/contact/src/migration.ts @@ -1,23 +1,32 @@ // -import { DOMAIN_TX, TxOperations, type Class, type Doc, type Domain, type Ref, type Space } from '@hcengineering/core' +import { + type Class, + type Doc, + type Domain, + DOMAIN_TX, + generateId, + type Ref, + type Space, + TxOperations +} from '@hcengineering/core' import { createDefaultSpace, - tryMigrate, - tryUpgrade, type MigrateOperation, type MigrateUpdate, type MigrationClient, type MigrationDocumentQuery, type MigrationUpgradeClient, - type ModelLogger + type ModelLogger, + tryMigrate, + tryUpgrade } from '@hcengineering/model' import activity, { DOMAIN_ACTIVITY } from '@hcengineering/model-activity' -import core from '@hcengineering/model-core' +import core, { DOMAIN_SPACE } from '@hcengineering/model-core' import { DOMAIN_VIEW } from '@hcengineering/model-view' +import { AvatarType, type Contact, type Person, type PersonSpace } from '@hcengineering/contact' -import { AvatarType, type Contact } from '@hcengineering/contact' -import contact, { DOMAIN_CONTACT, contactId } from './index' +import contact, { contactId, DOMAIN_CONTACT } from './index' async function createEmployeeEmail (client: TxOperations): Promise { const employees = await client.findAll(contact.mixin.Employee, {}) @@ -100,23 +109,55 @@ async function migrateAvatars (client: MigrationClient): Promise { ) } +async function createPersonSpaces (client: MigrationClient): Promise { + const spaces = await client.find(DOMAIN_SPACE, { _class: contact.class.PersonSpace }) + + if (spaces.length > 0) { + return + } + + const accounts = await client.model.findAll(contact.class.PersonAccount, {}) + const employees = await client.find(DOMAIN_CONTACT, { [contact.mixin.Employee]: { $exists: true } }) + + const newSpaces = new Map, PersonSpace>() + const now = Date.now() + + for (const account of accounts) { + const employee = employees.find(({ _id }) => _id === account.person) + if (employee === undefined) continue + + const space = newSpaces.get(account.person) + + if (space !== undefined) { + space.members.push(account._id) + } else { + newSpaces.set(account.person, { + _id: generateId(), + _class: contact.class.PersonSpace, + space: core.space.Space, + name: 'Personal space', + description: '', + private: true, + archived: false, + members: [account._id], + person: account.person, + modifiedBy: core.account.System, + createdBy: core.account.System, + modifiedOn: now, + createdOn: now + }) + } + } + + await client.create(DOMAIN_SPACE, Array.from(newSpaces.values())) +} + export const contactOperation: MigrateOperation = { async migrate (client: MigrationClient, logger: ModelLogger): Promise { await tryMigrate(client, contactId, [ { state: 'employees', func: async (client) => { - await client.update( - DOMAIN_TX, - { - objectClass: 'contact:class:EmployeeAccount' - }, - { - $rename: { 'attributes.employee': 'attributes.person' }, - $set: { objectClass: contact.class.PersonAccount } - } - ) - await client.update( DOMAIN_TX, { @@ -251,6 +292,10 @@ export const contactOperation: MigrateOperation = { { $rename: { avatarKind: 'avatarType' } } ) } + }, + { + state: 'create-person-spaces-v1', + func: createPersonSpaces } ]) }, diff --git a/models/notification/package.json b/models/notification/package.json index 11ecb5b4d5..12dbe6f086 100644 --- a/models/notification/package.json +++ b/models/notification/package.json @@ -30,6 +30,7 @@ "dependencies": { "@hcengineering/activity": "^0.6.0", "@hcengineering/chunter": "^0.6.20", + "@hcengineering/contact": "^0.6.24", "@hcengineering/core": "^0.6.32", "@hcengineering/model": "^0.6.11", "@hcengineering/model-attachment": "^0.6.0", @@ -39,10 +40,10 @@ "@hcengineering/model-workbench": "^0.6.1", "@hcengineering/notification": "^0.6.23", "@hcengineering/platform": "^0.6.11", + "@hcengineering/preference": "^0.6.13", "@hcengineering/setting": "^0.6.17", "@hcengineering/ui": "^0.6.15", "@hcengineering/view": "^0.6.13", - "@hcengineering/workbench": "^0.6.16", - "@hcengineering/preference": "^0.6.13" + "@hcengineering/workbench": "^0.6.16" } } diff --git a/models/notification/src/index.ts b/models/notification/src/index.ts index bef317510f..04a2b8233c 100644 --- a/models/notification/src/index.ts +++ b/models/notification/src/index.ts @@ -49,6 +49,7 @@ import { UX, type Builder } from '@hcengineering/model' +import { type PersonSpace } from '@hcengineering/contact' import core, { TClass, TDoc } from '@hcengineering/model-core' import preference, { TPreference } from '@hcengineering/model-preference' import view, { createAction, template } from '@hcengineering/model-view' @@ -195,13 +196,18 @@ export class TDocNotifyContext extends TDoc implements DocNotifyContext { @Index(IndexKind.Indexed) user!: Ref - @Prop(TypeRef(core.class.Doc), core.string.AttachedTo) + @Prop(TypeRef(core.class.Doc), core.string.Object) @Index(IndexKind.Indexed) - attachedTo!: Ref + objectId!: Ref - @Prop(TypeRef(core.class.Class), core.string.AttachedToClass) + @Prop(TypeRef(core.class.Class), core.string.Class) @Index(IndexKind.Indexed) - attachedToClass!: Ref> + objectClass!: Ref> + + @Prop(TypeRef(core.class.Space), core.string.Space) + objectSpace!: Ref + + declare space: Ref @Prop(TypeDate(), core.string.Date) lastViewedTimestamp?: Timestamp @@ -230,6 +236,8 @@ export class TInboxNotification extends TDoc implements InboxNotification { @Prop(TypeBoolean(), core.string.Boolean) archived!: boolean + declare space: Ref + title?: IntlString body?: IntlString intlParams?: Record diff --git a/models/notification/src/migration.ts b/models/notification/src/migration.ts index f65cf386ab..5bfca5fb32 100644 --- a/models/notification/src/migration.ts +++ b/models/notification/src/migration.ts @@ -29,8 +29,10 @@ import notification, { type InboxNotification } from '@hcengineering/notification' import { DOMAIN_PREFERENCE } from '@hcengineering/preference' +import contact, { type PersonSpace } from '@hcengineering/contact' import { DOMAIN_DOC_NOTIFY, DOMAIN_NOTIFICATION, DOMAIN_USER_NOTIFY } from './index' +import { DOMAIN_SPACE } from '@hcengineering/model-core' export async function removeNotifications ( client: MigrationClient, @@ -74,6 +76,115 @@ export async function removeNotifications ( } } +export async function migrateNotificationsSpace (client: MigrationClient): Promise { + const personSpaces = await client.find(DOMAIN_SPACE, { _class: contact.class.PersonSpace }, {}) + + await client.update( + DOMAIN_DOC_NOTIFY, + { + _class: notification.class.DocNotifyContext, + objectSpace: { $exists: false } + }, + { $rename: { space: 'objectSpace' } } + ) + + for (const space of personSpaces) { + await client.update( + DOMAIN_DOC_NOTIFY, + { + _class: notification.class.DocNotifyContext, + user: { $in: space.members } + }, + { space: space._id } + ) + await client.update( + DOMAIN_NOTIFICATION, + { + _class: notification.class.ActivityInboxNotification, + user: { $in: space.members } + }, + { space: space._id } + ) + await client.update( + DOMAIN_NOTIFICATION, + { + _class: notification.class.CommonInboxNotification, + user: { $in: space.members } + }, + { space: space._id } + ) + await client.update( + DOMAIN_NOTIFICATION, + { + _class: notification.class.MentionInboxNotification, + user: { $in: space.members } + }, + { space: space._id } + ) + } + + await client.deleteMany(DOMAIN_DOC_NOTIFY, { space: { $nin: personSpaces.map(({ _id }) => _id) } }) + await client.deleteMany(DOMAIN_NOTIFICATION, { + _class: notification.class.ActivityInboxNotification, + space: { $nin: personSpaces.map(({ _id }) => _id) } + }) + await client.deleteMany(DOMAIN_NOTIFICATION, { + _class: notification.class.CommonInboxNotification, + space: { $nin: personSpaces.map(({ _id }) => _id) } + }) + await client.deleteMany(DOMAIN_NOTIFICATION, { + _class: notification.class.MentionInboxNotification, + space: { $nin: personSpaces.map(({ _id }) => _id) } + }) + + while (true) { + const contexts = await client.find( + DOMAIN_DOC_NOTIFY, + { + _class: notification.class.DocNotifyContext, + attachedTo: { $exists: true } + }, + { limit: 500 } + ) + + if (contexts.length === 0) { + break + } + + const classesOfSpace = new Set>>() + + for (const context of contexts) { + const _class = (context as any).attachedToClass + if (client.hierarchy.isDerived(_class, core.class.Space)) { + classesOfSpace.add(_class) + } + } + if (classesOfSpace.size > 0) { + await client.update( + DOMAIN_DOC_NOTIFY, + { objectClass: { $in: Array.from(classesOfSpace) } }, + { objectSpace: core.space.Space } + ) + await client.update( + DOMAIN_DOC_NOTIFY, + { objectClass: { $in: Array.from(classesOfSpace) } }, + { $rename: { attachedTo: 'objectId', attachedToClass: 'objectClass' } } + ) + } + await client.update( + DOMAIN_DOC_NOTIFY, + { + _class: notification.class.DocNotifyContext, + _id: { $in: contexts.map(({ _id }) => _id) } + }, + { $rename: { attachedTo: 'objectId', attachedToClass: 'objectClass' } } + ) + } + + await client.deleteMany(DOMAIN_NOTIFICATION, { _class: notification.class.BrowserNotification }) + await client.deleteMany(DOMAIN_USER_NOTIFY, { _class: notification.class.BrowserNotification }) +} + export async function migrateSettings (client: MigrationClient): Promise { await client.update( DOMAIN_PREFERENCE, @@ -197,8 +308,13 @@ export const notificationOperation: MigrateOperation = { { isPinned: false } ) } + }, + { + state: 'migrate-notifications-space-v1', + func: migrateNotificationsSpace } ]) + await client.deleteMany(DOMAIN_USER_NOTIFY, { _class: notification.class.BrowserNotification, status: NotificationStatus.Notified diff --git a/packages/ui/src/components/ListView.svelte b/packages/ui/src/components/ListView.svelte index 9bec3b4876..0cd7be0cc2 100644 --- a/packages/ui/src/components/ListView.svelte +++ b/packages/ui/src/components/ListView.svelte @@ -26,6 +26,7 @@ export let colorsSchema: 'default' | 'lumia' = 'default' export let updateOnMouse = true export let lazy = false + export let minHeight: string | null = null export let highlightIndex: number | undefined = undefined const getKey: (index: number) => string = (index) => index.toString() @@ -76,35 +77,37 @@ > {#each Array(count) as _, row (getKey(row))} {#if lazy} - - dispatch('click', row)} - on:mouseover={mouseAttractor(() => { - if (updateOnMouse) { - onRow(row) - } - })} - on:mouseenter={mouseAttractor(() => { - if (updateOnMouse) { - onRow(row) - } - })} - > - - - - - - - - +
+ + dispatch('click', row)} + on:mouseover={mouseAttractor(() => { + if (updateOnMouse) { + onRow(row) + } + })} + on:mouseenter={mouseAttractor(() => { + if (updateOnMouse) { + onRow(row) + } + })} + > + + + + + + + + +
{:else} ).references ?? 0) > 0 diff --git a/plugins/chunter-resources/src/components/chat/ChatAside.svelte b/plugins/chunter-resources/src/components/chat/ChatAside.svelte index b27f58598f..9e3f94c7f5 100644 --- a/plugins/chunter-resources/src/components/chat/ChatAside.svelte +++ b/plugins/chunter-resources/src/components/chat/ChatAside.svelte @@ -36,7 +36,7 @@ $: threadId = context ? undefined : (_id as Ref) $: context && - objectQuery.query(context.attachedToClass, { _id: context.attachedTo }, (res) => { + objectQuery.query(context.objectClass, { _id: context.objectId }, (res) => { ;[object] = res }) diff --git a/plugins/chunter-resources/src/components/chat/create/CreateChannel.svelte b/plugins/chunter-resources/src/components/chat/create/CreateChannel.svelte index 9d48cb3d22..ca7cdddffe 100644 --- a/plugins/chunter-resources/src/components/chat/create/CreateChannel.svelte +++ b/plugins/chunter-resources/src/components/chat/create/CreateChannel.svelte @@ -18,6 +18,7 @@ import presentation, { getClient } from '@hcengineering/presentation' import core, { getCurrentAccount } from '@hcengineering/core' import notification from '@hcengineering/notification' + import contact, { PersonAccount } from '@hcengineering/contact' import Lock from '../../icons/Lock.svelte' import chunter from '../../../plugin' @@ -51,19 +52,27 @@ $: canSave = !!channelName async function save (): Promise { - const accountId = getCurrentAccount()._id + const account = getCurrentAccount() as PersonAccount + const space = await client.findOne( + contact.class.PersonSpace, + { person: account.person }, + { projection: { _id: 1 } } + ) + if (!space) return const channelId = await client.createDoc(chunter.class.Channel, core.space.Space, { name: channelName, description: '', private: selectedVisibilityId === 'private', archived: false, - members: [accountId], + members: [account._id], topic: description }) - await client.createDoc(notification.class.DocNotifyContext, channelId, { - user: accountId, - attachedTo: channelId, - attachedToClass: chunter.class.Channel, + + await client.createDoc(notification.class.DocNotifyContext, space._id, { + user: account._id, + objectId: channelId, + objectClass: chunter.class.Channel, + objectSpace: core.space.Space, isPinned: false }) diff --git a/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte b/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte index 8bcf323547..08bcb2400b 100644 --- a/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte +++ b/plugins/chunter-resources/src/components/chat/create/CreateDirectChat.svelte @@ -31,7 +31,7 @@ const dispatch = createEventDispatcher() const client = getClient() - const myAccId = getCurrentAccount()._id + const myAcc = getCurrentAccount() as PersonAccount const query = createQuery() let employeeIds: Ref[] = [] @@ -52,7 +52,7 @@ async function createDirectMessage (): Promise { const employeeAccounts = await client.findAll(contact.class.PersonAccount, { person: { $in: employeeIds } }) - const accIds = [myAccId, ...employeeAccounts.filter(({ _id }) => _id !== myAccId).map(({ _id }) => _id)].sort() + const accIds = [myAcc._id, ...employeeAccounts.filter(({ _id }) => _id !== myAcc._id).map(({ _id }) => _id)].sort() const existingDms = await client.findAll(chunter.class.DirectMessage, {}) @@ -75,9 +75,9 @@ })) const context = await client.findOne(notification.class.DocNotifyContext, { - user: myAccId, - attachedTo: dmId, - attachedToClass: chunter.class.DirectMessage + person: myAcc.person, + objectId: dmId, + objectClass: chunter.class.DirectMessage }) if (context !== undefined) { @@ -86,10 +86,13 @@ return } - await client.createDoc(notification.class.DocNotifyContext, dmId, { - user: myAccId, - attachedTo: dmId, - attachedToClass: chunter.class.DirectMessage, + const space = await client.findOne(contact.class.PersonSpace, { person: myAcc.person }, { projection: { _id: 1 } }) + if (!space) return + await client.createDoc(notification.class.DocNotifyContext, space._id, { + user: myAcc._id, + objectId: dmId, + objectClass: chunter.class.DirectMessage, + objectSpace: core.space.Space, isPinned: false }) diff --git a/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte b/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte index 33de29497f..a524e646bc 100644 --- a/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte +++ b/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte @@ -50,11 +50,11 @@ let sections: Section[] = [] - $: contexts = $contextsStore.filter(({ attachedToClass, isPinned }) => { + $: contexts = $contextsStore.filter(({ objectClass, isPinned }) => { if (model.isPinned !== isPinned) return false - if (model._class !== undefined && model._class !== attachedToClass) return false - if (model.skipClasses !== undefined && model.skipClasses.includes(attachedToClass)) return false - if (hierarchy.classHierarchyMixin(attachedToClass, activity.mixin.ActivityDoc) === undefined) return false + if (model._class !== undefined && model._class !== objectClass) return false + if (model.skipClasses !== undefined && model.skipClasses.includes(objectClass)) return false + if (hierarchy.classHierarchyMixin(objectClass, activity.mixin.ActivityDoc) === undefined) return false return true }) @@ -72,10 +72,10 @@ object !== undefined && getObjectGroup(object) === model.id && !$contextByDocStore.has(object._id) function loadObjects (contexts: DocNotifyContext[]): void { - const contextsByClass = groupByArray(contexts, ({ attachedToClass }) => attachedToClass) + const contextsByClass = groupByArray(contexts, ({ objectClass }) => objectClass) for (const [_class, ctx] of contextsByClass.entries()) { - const ids = ctx.map(({ attachedTo }) => attachedTo) + const ids = ctx.map(({ objectId }) => objectId) const { query, limit } = objectsQueryByClass.get(_class) ?? { query: createQuery(), limit: hierarchy.isDerived(_class, chunter.class.ChunterSpace) ? -1 : model.maxSectionItems ?? 5 @@ -187,7 +187,7 @@ if (_class === undefined) { return model.getActionsFn(contexts) } else { - return model.getActionsFn(contexts.filter(({ attachedToClass }) => attachedToClass === _class)) + return model.getActionsFn(contexts.filter(({ objectClass }) => objectClass === _class)) } } diff --git a/plugins/chunter-resources/src/components/chat/navigator/ChatNavSection.svelte b/plugins/chunter-resources/src/components/chat/navigator/ChatNavSection.svelte index 9dd3f67b76..5e35179cad 100644 --- a/plugins/chunter-resources/src/components/chat/navigator/ChatNavSection.svelte +++ b/plugins/chunter-resources/src/components/chat/navigator/ChatNavSection.svelte @@ -120,7 +120,7 @@ noDivider > {#each sortedItems as item (item.id)} - {@const context = contexts.find(({ attachedTo }) => attachedTo === item.id)} + {@const context = contexts.find(({ objectId }) => objectId === item.id)} {/each} {#if canShowMore} @@ -130,7 +130,7 @@ {/if} {#if visibleItem !== undefined && !isOpen} - {@const context = contexts.find(({ attachedTo }) => attachedTo === visibleItem?.id)} + {@const context = contexts.find(({ objectId }) => objectId === visibleItem?.id)} {/if} diff --git a/plugins/chunter-resources/src/components/chat/navigator/ChatNavigator.svelte b/plugins/chunter-resources/src/components/chat/navigator/ChatNavigator.svelte index f7d6ae1663..42049ec6ea 100644 --- a/plugins/chunter-resources/src/components/chat/navigator/ChatNavigator.svelte +++ b/plugins/chunter-resources/src/components/chat/navigator/ChatNavigator.svelte @@ -13,7 +13,7 @@ // limitations under the License. -->
{#if iconMixin && object} - {:else if !iconMixin} - + {:else} + {/if}
diff --git a/plugins/notification-resources/src/components/inbox/Inbox.svelte b/plugins/notification-resources/src/components/inbox/Inbox.svelte index 0d6004c343..ca8e3624d4 100644 --- a/plugins/notification-resources/src/components/inbox/Inbox.svelte +++ b/plugins/notification-resources/src/components/inbox/Inbox.svelte @@ -188,7 +188,7 @@ async function updateTabItems (inboxData: InboxData, notifyContexts: DocNotifyContext[]): Promise { const displayClasses = new Set( - notifyContexts.filter(({ _id }) => inboxData.has(_id)).map(({ attachedToClass }) => attachedToClass) + notifyContexts.filter(({ _id }) => inboxData.has(_id)).map(({ objectClass }) => objectClass) ) const classes = Array.from(displayClasses) @@ -249,8 +249,8 @@ return } - const isChunterChannel = hierarchy.isDerived(selectedContext.attachedToClass, chunter.class.ChunterSpace) - const panelComponent = hierarchy.classHierarchyMixin(selectedContext.attachedToClass, view.mixin.ObjectPanel) + const isChunterChannel = hierarchy.isDerived(selectedContext.objectClass, chunter.class.ChunterSpace) + const panelComponent = hierarchy.classHierarchyMixin(selectedContext.objectClass, view.mixin.ObjectPanel) selectedComponent = panelComponent?.component ?? view.component.EditDoc @@ -315,10 +315,10 @@ if ( selectedTabId === activity.class.ActivityMessage && - hierarchy.isDerived(context.attachedToClass, activity.class.ActivityMessage) + hierarchy.isDerived(context.objectClass, activity.class.ActivityMessage) ) { result.set(key, resNotifications) - } else if (context.attachedToClass === selectedTabId) { + } else if (context.objectClass === selectedTabId) { result.set(key, resNotifications) } } @@ -411,8 +411,8 @@ context === selectedContext)} noScroll + minHeight="5.625rem" kind="full-size" colorsSchema="lumia" lazy={true} diff --git a/plugins/notification-resources/src/inboxNotificationsClient.ts b/plugins/notification-resources/src/inboxNotificationsClient.ts index dce208794e..206f9b376b 100644 --- a/plugins/notification-resources/src/inboxNotificationsClient.ts +++ b/plugins/notification-resources/src/inboxNotificationsClient.ts @@ -90,6 +90,10 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient { private _contextByDoc = new Map, DocNotifyContext>() private constructor () { + void this.init() + } + + private async init (): Promise { this.contextsQuery.query( notification.class.DocNotifyContext, { @@ -97,7 +101,7 @@ export class InboxNotificationsClientImpl implements InboxNotificationsClient { }, (result: DocNotifyContext[]) => { this.contexts.set(result) - this._contextByDoc = new Map(result.map((updates) => [updates.attachedTo, updates])) + this._contextByDoc = new Map(result.map((updates) => [updates.objectId, updates])) this.contextByDoc.set(this._contextByDoc) } ) diff --git a/plugins/notification-resources/src/utils.ts b/plugins/notification-resources/src/utils.ts index 1e9b597515..4fd9a7ea6a 100644 --- a/plugins/notification-resources/src/utils.ts +++ b/plugins/notification-resources/src/utils.ts @@ -273,9 +273,9 @@ async function updateMeInCollaborators ( /** * @public */ -export async function unsubscribe (object: DocNotifyContext): Promise { +export async function unsubscribe (context: DocNotifyContext): Promise { const client = getClient() - await updateMeInCollaborators(client, object.attachedToClass, object.attachedTo, OpWithMe.Remove) + await updateMeInCollaborators(client, context.objectClass, context.objectId, OpWithMe.Remove) } /** @@ -586,50 +586,39 @@ export async function selectInboxContext ( ): Promise { const client = getClient() const hierarchy = client.getHierarchy() + const { objectId, objectClass } = context if (isMentionNotification(notification) && isActivityMessageClass(notification.mentionedInClass)) { const selectedMsg = notification.mentionedIn as Ref void navigateToInboxDoc( linkProviders, - context.attachedTo, - context.attachedToClass, - isActivityMessageClass(context.attachedToClass) ? (context.attachedTo as Ref) : undefined, + objectId, + objectClass, + isActivityMessageClass(objectClass) ? (objectId as Ref) : undefined, selectedMsg ) return } - if (hierarchy.isDerived(context.attachedToClass, activity.class.ActivityMessage)) { + if (hierarchy.isDerived(objectClass, activity.class.ActivityMessage)) { const message = (notification as WithLookup)?.$lookup?.attachedTo - if (context.attachedToClass === chunter.class.ThreadMessage) { + if (objectClass === chunter.class.ThreadMessage) { const thread = await client.findOne( chunter.class.ThreadMessage, { - _id: context.attachedTo as Ref + _id: objectId as Ref }, { projection: { _id: 1, attachedTo: 1 } } ) - void navigateToInboxDoc( - linkProviders, - context.attachedTo, - context.attachedToClass, - thread?.attachedTo, - thread?._id - ) + void navigateToInboxDoc(linkProviders, objectId, objectClass, thread?.attachedTo, thread?._id) return } if (isReactionMessage(message)) { - void navigateToInboxDoc( - linkProviders, - context.attachedTo, - context.attachedToClass, - undefined, - context.attachedTo as Ref - ) + void navigateToInboxDoc(linkProviders, objectId, objectClass, undefined, objectId as Ref) return } @@ -637,18 +626,18 @@ export async function selectInboxContext ( void navigateToInboxDoc( linkProviders, - context.attachedTo, - context.attachedToClass, - selectedMsg !== undefined ? (context.attachedTo as Ref) : undefined, - selectedMsg ?? (context.attachedTo as Ref) + objectId, + objectClass, + selectedMsg !== undefined ? (objectId as Ref) : undefined, + selectedMsg ?? (objectId as Ref) ) return } void navigateToInboxDoc( linkProviders, - context.attachedTo, - context.attachedToClass, + objectId, + objectClass, undefined, (notification as ActivityInboxNotification)?.attachedTo ) diff --git a/plugins/notification/package.json b/plugins/notification/package.json index 31d57b7d21..8c29582966 100644 --- a/plugins/notification/package.json +++ b/plugins/notification/package.json @@ -40,6 +40,7 @@ }, "dependencies": { "@hcengineering/activity": "^0.6.0", + "@hcengineering/contact": "^0.6.24", "@hcengineering/core": "^0.6.32", "@hcengineering/platform": "^0.6.11", "@hcengineering/preference": "^0.6.13", diff --git a/plugins/notification/src/index.ts b/plugins/notification/src/index.ts index 978fff22d7..393a9b8260 100644 --- a/plugins/notification/src/index.ts +++ b/plugins/notification/src/index.ts @@ -24,6 +24,7 @@ import { Markup, Mixin, Ref, + Space, Timestamp, Tx, TxOperations @@ -34,6 +35,8 @@ import { Preference } from '@hcengineering/preference' import { IntegrationType } from '@hcengineering/setting' import { AnyComponent, Location, ResolvedLocation } from '@hcengineering/ui' import { Action } from '@hcengineering/view' +import { PersonSpace } from '@hcengineering/contact' + import { Readable, Writable } from './types' export * from './types' @@ -226,7 +229,7 @@ export interface NotificationContextPresenter extends Class { /** * @public */ -export interface InboxNotification extends Doc { +export interface InboxNotification extends Doc { user: Ref isViewed: boolean @@ -273,12 +276,12 @@ export type DisplayInboxNotification = DisplayActivityInboxNotification | InboxN /** * @public */ -export interface DocNotifyContext extends Doc { +export interface DocNotifyContext extends Doc { user: Ref - // Context - attachedTo: Ref - attachedToClass: Ref> + objectId: Ref + objectClass: Ref> + objectSpace: Ref isPinned: boolean lastViewedTimestamp?: Timestamp @@ -423,7 +426,8 @@ const notification = plugin(notificationId, { CommonNotificationCollectionAdded: '' as IntlString, CommonNotificationCollectionRemoved: '' as IntlString, SoundNotificationsDescription: '' as IntlString, - Sound: '' as IntlString + Sound: '' as IntlString, + NoAccessToObject: '' as IntlString }, function: { Notify: '' as Resource, diff --git a/server-plugins/activity-resources/src/references.ts b/server-plugins/activity-resources/src/references.ts index f2021a96cc..407141e9c8 100644 --- a/server-plugins/activity-resources/src/references.ts +++ b/server-plugins/activity-resources/src/references.ts @@ -46,7 +46,7 @@ import { } from '@hcengineering/text' import { StorageAdapter, TriggerControl } from '@hcengineering/server-core' import activity, { ActivityMessage, ActivityReference, UserMentionInfo } from '@hcengineering/activity' -import contact, { Person, PersonAccount } from '@hcengineering/contact' +import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact' import { getCommonNotificationTxes, getPushCollaboratorTx, @@ -54,7 +54,8 @@ import { isShouldNotifyTx, NotifyResult, applyNotificationProviders, - getNotificationContent + getNotificationContent, + toReceiverInfo } from '@hcengineering/server-notification-resources' async function getPersonAccount (person: Ref, control: TriggerControl): Promise { @@ -118,6 +119,14 @@ export async function getPersonNotificationTxes ( const doc = (await control.findAll(reference.srcDocClass, { _id: reference.srcDocId }))[0] + const receiverPerson = ( + await control.findAll(contact.mixin.Employee, { _id: receiver.person as Ref, active: true }, { limit: 1 }) + )[0] + if (receiverPerson === undefined) return res + + const receiverSpace = (await control.findAll(contact.class.PersonSpace, { person: receiver.person }, { limit: 1 }))[0] + if (receiverSpace === undefined) return res + const collaboratorsTx = await getCollaboratorsTxes(reference, control, receiver, doc) res.push(...collaboratorsTx) @@ -164,17 +173,19 @@ export async function getPersonNotificationTxes ( const sender = ( await control.modelDb.findAll(contact.class.PersonAccount, { _id: senderId as Ref }, { limit: 1 }) )[0] - const receiverPerson = (await control.findAll(contact.class.Person, { _id: receiver.person }, { limit: 1 }))[0] + const senderPerson = sender !== undefined ? (await control.findAll(contact.class.Person, { _id: sender.person }, { limit: 1 }))[0] : undefined - const receiverInfo = { + const receiverInfo = toReceiverInfo(control.hierarchy, { _id: receiver._id, account: receiver, - person: receiverPerson - } + person: receiverPerson, + space: receiverSpace._id + }) + if (receiverInfo === undefined) return res const senderInfo = { _id: senderId, @@ -200,7 +211,7 @@ export async function getPersonNotificationTxes ( senderInfo, reference.srcDocId, reference.srcDocClass, - space, + doc.space, originTx.modifiedOn, notifyResult, notification.class.MentionInboxNotification @@ -210,7 +221,7 @@ export async function getPersonNotificationTxes ( const context = ( await control.findAll( notification.class.DocNotifyContext, - { attachedTo: reference.srcDocId, user: receiver._id }, + { objectId: reference.srcDocId, user: receiver._id }, { projection: { _id: 1 } } ) )[0] @@ -222,7 +233,7 @@ export async function getPersonNotificationTxes ( docNotifyContext: context._id, _id: generateId(), _class: notification.class.CommonInboxNotification, - space, + space: receiverSpace._id, modifiedOn: originTx.modifiedOn, modifiedBy: sender._id } diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts index 13ae584fc8..5ef6cfbbd0 100644 --- a/server-plugins/chunter-resources/src/index.ts +++ b/server-plugins/chunter-resources/src/index.ts @@ -394,7 +394,7 @@ async function OnChannelMembersChanged (tx: TxUpdateDoc, control: Trigg const removed = combineAttributes([tx.operations], 'members', '$pull', '$in') const res: Tx[] = [] - const allContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: tx.objectId }) + const allContexts = await control.findAll(notification.class.DocNotifyContext, { objectId: tx.objectId }) if (removed.length > 0) { res.push( @@ -416,13 +416,25 @@ async function OnChannelMembersChanged (tx: TxUpdateDoc, control: Trigg ) } + const accounts = + added.length > 0 ? await control.modelDb.findAll(contact.class.PersonAccount, { _id: { $in: added } }) : [] + const spaces = + accounts.length > 0 + ? await control.findAll(contact.class.PersonSpace, { person: { $in: accounts.map((x) => x.person) } }) + : [] + for (const addedMember of added) { const context = allContexts.find(({ user }) => user === addedMember) if (context === undefined) { - const createTx = control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, tx.objectSpace, { - attachedTo: tx.objectId, - attachedToClass: tx.objectClass, + const account = accounts.find(({ _id }) => _id === addedMember) + if (account === undefined) continue + const space = spaces.find(({ person }) => person === account.person) + if (space === undefined) continue + const createTx = control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, space._id, { + objectId: tx.objectId, + objectClass: tx.objectClass, + objectSpace: tx.objectSpace, user: addedMember, lastViewedTimestamp: tx.modifiedOn, isPinned: false @@ -563,14 +575,14 @@ export async function updateChatInfo (control: TriggerControl, status: UserStatu const { hierarchy } = control const res: Tx[] = [] - const directContexts = contexts.filter(({ attachedToClass }) => - hierarchy.isDerived(attachedToClass, chunter.class.DirectMessage) + const directContexts = contexts.filter(({ objectClass }) => + hierarchy.isDerived(objectClass, chunter.class.DirectMessage) ) const activityContexts = contexts.filter( - ({ attachedToClass }) => - !hierarchy.isDerived(attachedToClass, chunter.class.DirectMessage) && - !hierarchy.isDerived(attachedToClass, chunter.class.Channel) && - !hierarchy.isDerived(attachedToClass, chunter.class.Channel) + ({ objectClass }) => + !hierarchy.isDerived(objectClass, chunter.class.DirectMessage) && + !hierarchy.isDerived(objectClass, chunter.class.Channel) && + !hierarchy.isDerived(objectClass, chunter.class.Channel) ) const directTxes = await hideOldDirects(directContexts, control, date) diff --git a/server-plugins/contact-resources/src/index.ts b/server-plugins/contact-resources/src/index.ts index d42140bf38..836648cc4f 100644 --- a/server-plugins/contact-resources/src/index.ts +++ b/server-plugins/contact-resources/src/index.ts @@ -26,7 +26,8 @@ import contact, { formatName, getFirstName, getLastName, - getName + getName, + PersonSpace } from '@hcengineering/contact' import core, { Account, @@ -41,7 +42,8 @@ import core, { TxProcessor, TxRemoveDoc, TxUpdateDoc, - concatLink + concatLink, + TxCUD } from '@hcengineering/core' import notification, { Collaborators } from '@hcengineering/notification' import { getMetadata } from '@hcengineering/platform' @@ -87,6 +89,10 @@ export async function OnEmployeeCreate (tx: Tx, control: TriggerControl): Promis if (acc === undefined) return [] const spaces = await control.findAll(core.class.Space, { autoJoin: true }) const result: Tx[] = [] + + const txes = await createPersonSpace(acc._id, mixinTx.objectId, control) + result.push(...txes) + for (const space of spaces) { if (space.members.includes(acc._id)) continue const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { @@ -99,6 +105,35 @@ export async function OnEmployeeCreate (tx: Tx, control: TriggerControl): Promis return result } +async function createPersonSpace ( + account: Ref, + person: Ref, + control: TriggerControl +): Promise[]> { + const personSpace = (await control.findAll(contact.class.PersonSpace, { person }, { limit: 1 })).shift() + if (personSpace !== undefined) { + if (personSpace.members.includes(account)) return [] + return [ + control.txFactory.createTxUpdateDoc(personSpace._class, personSpace.space, personSpace._id, { + $push: { + members: account + } + }) + ] + } + + return [ + control.txFactory.createTxCreateDoc(contact.class.PersonSpace, core.space.Space, { + name: 'Personal space', + description: '', + private: true, + archived: false, + person, + members: [account] + }) + ] +} + export async function OnPersonAccountCreate (tx: Tx, control: TriggerControl): Promise { const acc = TxProcessor.createDoc2Doc(tx as TxCreateDoc) const person = ( @@ -106,7 +141,12 @@ export async function OnPersonAccountCreate (tx: Tx, control: TriggerControl): P )[0] if (person === undefined) return [] const spaces = await control.findAll(core.class.Space, { autoJoin: true }) + const result: Tx[] = [] + const txes = await createPersonSpace(acc._id, person._id, control) + + result.push(...txes) + for (const space of spaces) { if (space.members.includes(acc._id)) continue const pushTx = control.txFactory.createTxUpdateDoc(space._class, space.space, space._id, { diff --git a/server-plugins/gmail-resources/src/index.ts b/server-plugins/gmail-resources/src/index.ts index e22563d6fe..8ad1e0d1d6 100644 --- a/server-plugins/gmail-resources/src/index.ts +++ b/server-plugins/gmail-resources/src/index.ts @@ -30,8 +30,12 @@ import { } from '@hcengineering/core' import gmail, { Message } from '@hcengineering/gmail' import { TriggerControl } from '@hcengineering/server-core' -import notification, { BaseNotificationType, InboxNotification, NotificationType } from '@hcengineering/notification' -import serverNotification, { NotificationProviderFunc, UserInfo } from '@hcengineering/server-notification' +import { BaseNotificationType, InboxNotification, NotificationType } from '@hcengineering/notification' +import serverNotification, { + NotificationProviderFunc, + ReceiverInfo, + SenderInfo +} from '@hcengineering/server-notification' import { getContentByTemplate } from '@hcengineering/server-notification-resources' import { getMetadata } from '@hcengineering/platform' @@ -74,47 +78,6 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise }) res.push(tx) } - if (message.incoming) { - const docs = await control.findAll(notification.class.DocNotifyContext, { - attachedTo: channel._id, - user: message.modifiedBy - }) - for (const doc of docs) { - // TODO: push inbox notification - // res.push( - // control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { - // $push: { - // txes: { - // _id: tx._id as Ref>, - // modifiedOn: tx.modifiedOn, - // modifiedBy: tx.modifiedBy, - // isNew: true - // } - // } - // }) - // ) - res.push( - control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { - lastUpdateTimestamp: tx.modifiedOn - }) - ) - } - if (docs.length === 0) { - res.push( - control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, channel.space, { - user: tx.modifiedBy, - attachedTo: channel._id, - attachedToClass: channel._class, - lastUpdateTimestamp: tx.modifiedOn, - isPinned: false - // TODO: push inbox notification - // txes: [ - // { _id: tx._id as Ref>, modifiedOn: tx.modifiedOn, modifiedBy: tx.modifiedBy, isNew: true } - // ] - }) - ) - } - } } return res @@ -167,8 +130,8 @@ async function notifyByEmail ( control: TriggerControl, type: Ref, doc: Doc | undefined, - sender: UserInfo, - receiver: UserInfo, + sender: SenderInfo, + receiver: ReceiverInfo, data: InboxNotification ): Promise { const account = receiver.account @@ -192,26 +155,14 @@ const SendEmailNotifications: NotificationProviderFunc = async ( types: BaseNotificationType[], object: Doc, data: InboxNotification, - receiver: UserInfo, - sender: UserInfo + receiver: ReceiverInfo, + sender: SenderInfo ): Promise => { if (types.length === 0) { return [] } - if (receiver.person === undefined) { - return [] - } - - const isEmployee = control.hierarchy.hasMixin(receiver.person, contact.mixin.Employee) - - if (!isEmployee) { - return [] - } - - const employee = control.hierarchy.as(receiver.person, contact.mixin.Employee) - - if (!employee.active) { + if (!receiver.person.active) { return [] } diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index a05d672037..e6a2b77002 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -76,7 +76,8 @@ import serverNotification, { getPersonAccountById, NOTIFICATION_BODY_SIZE, NOTIFICATION_TITLE_SIZE, - UserInfo + ReceiverInfo, + SenderInfo } from '@hcengineering/server-notification' import serverView from '@hcengineering/server-view' import { stripTags } from '@hcengineering/text' @@ -96,6 +97,7 @@ import { isUserEmployeeInFieldValue, isUserInFieldValue, replaceAll, + toReceiverInfo, updateNotifyContextsSpace } from './utils' @@ -121,8 +123,8 @@ export async function getCommonNotificationTxes ( control: TriggerControl, doc: Doc, data: Partial>, - receiver: UserInfo, - sender: UserInfo, + receiver: ReceiverInfo, + sender: SenderInfo, attachedTo: Ref, attachedToClass: Ref>, space: Ref, @@ -135,7 +137,7 @@ export async function getCommonNotificationTxes ( } const res: Tx[] = [] - const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo }) + const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { objectId: attachedTo }) const notificationTx = await pushInboxNotifications( control, @@ -306,70 +308,38 @@ export async function getDocCollaborators ( return Array.from(collaborators.values()) } -function getDocNotifyContext ( - docNotifyContexts: DocNotifyContext[], - targetUser: Ref, - attachedTo: Ref, - res: Tx[] -): DocNotifyContext | undefined { - const context = docNotifyContexts.find((context) => context.user === targetUser && context.attachedTo === attachedTo) - - if (context !== undefined) { - return context - } - - const contextTx = (res as TxCUD[]).find((tx) => { - if (tx._class === core.class.TxCreateDoc && tx.objectClass === notification.class.DocNotifyContext) { - const createTx = tx as TxCreateDoc - - return createTx.attributes.attachedTo === attachedTo && createTx.attributes.user === targetUser - } - - return false - }) as TxCreateDoc | undefined - - if (contextTx !== undefined) { - return TxProcessor.createDoc2Doc(contextTx) - } - - return undefined -} - export async function pushInboxNotifications ( control: TriggerControl, res: Tx[], - target: UserInfo, - attachedTo: Ref, - attachedToClass: Ref>, - space: Ref, + receiver: ReceiverInfo, + objectId: Ref, + objectClass: Ref>, + objectSpace: Ref, contexts: DocNotifyContext[], data: Partial>, _class: Ref>, modifiedOn: Timestamp, shouldUpdateTimestamp = true ): Promise | undefined> { - const account = target.account - - if (account === undefined) { - return - } - const context = getDocNotifyContext(contexts, account._id, attachedTo, res) + const account = receiver.account + const context = contexts.find((context) => context.user === receiver._id && context.objectId === objectId) let docNotifyContextId: Ref if (context === undefined) { - const createContextTx = control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, space, { - user: account._id, - attachedTo, - attachedToClass, - lastUpdateTimestamp: shouldUpdateTimestamp ? modifiedOn : undefined, - isPinned: false + const createContextTx = control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, receiver.space, { + user: receiver._id, + objectId, + objectClass, + objectSpace, + isPinned: false, + lastUpdateTimestamp: shouldUpdateTimestamp ? modifiedOn : undefined }) await control.apply([createContextTx]) - if (target.account?.email !== undefined) { + if (receiver.account?.email !== undefined) { control.operationContext.derived.targets['docNotifyContext' + createContextTx._id] = (it) => { if (it._id === createContextTx._id) { - return [target.account?.email as string] + return [receiver.account?.email] } } } @@ -385,7 +355,7 @@ export async function pushInboxNotifications ( archived: false, ...data } - const notificationTx = control.txFactory.createTxCreateDoc(_class, space, notificationData) + const notificationTx = control.txFactory.createTxCreateDoc(_class, receiver.space, notificationData) res.push(notificationTx) return notificationTx @@ -490,12 +460,12 @@ export async function getTranslatedNotificationContent ( export async function createPushFromInbox ( control: TriggerControl, - target: UserInfo, + receiver: ReceiverInfo, attachedTo: Ref, attachedToClass: Ref>, data: Data, _class: Ref>, - sender: UserInfo, + sender: SenderInfo, _id: Ref, cache: Map, Doc> = new Map, Doc>() ): Promise { @@ -524,9 +494,9 @@ export async function createPushFromInbox ( } const path = [workbenchId, control.workspace.workspaceUrl, notificationId, encodeObjectURI(id, attachedToClass)] - await createPushNotification(control, target._id as Ref, title, body, _id, senderPerson, path) - return control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, core.space.Workspace, { - user: target._id, + await createPushNotification(control, receiver._id as Ref, title, body, _id, senderPerson, path) + return control.txFactory.createTxCreateDoc(notification.class.BrowserNotification, receiver.space, { + user: receiver._id, status: NotificationStatus.New, title, body, @@ -606,18 +576,14 @@ export async function pushActivityInboxNotifications ( originTx: TxCUD, control: TriggerControl, res: Tx[], - target: UserInfo, - sender: UserInfo, + receiver: ReceiverInfo, + sender: SenderInfo, object: Doc, docNotifyContexts: DocNotifyContext[], activityMessage: ActivityMessage, shouldUpdateTimestamp: boolean ): Promise | undefined> { - if (target.account === undefined) { - return - } - - const content = await getNotificationContent(originTx, target.account, sender, object, control, activityMessage) + const content = await getNotificationContent(originTx, receiver.account, sender, object, control, activityMessage) const data: Partial> = { ...content, attachedTo: activityMessage._id, @@ -627,10 +593,10 @@ export async function pushActivityInboxNotifications ( return await pushInboxNotifications( control, res, - target, + receiver, activityMessage.attachedTo, activityMessage.attachedToClass, - activityMessage.space, + object.space, docNotifyContexts, data, notification.class.ActivityInboxNotification, @@ -647,8 +613,8 @@ export async function applyNotificationProviders ( control: TriggerControl, res: Tx[], object: Doc, - receiver: UserInfo, - sender: UserInfo + receiver: ReceiverInfo, + sender: SenderInfo ): Promise { const resources = await control.modelDb.findAll(serverNotification.class.NotificationProviderResources, {}) for (const [provider, types] of notifyResult.entries()) { @@ -690,8 +656,8 @@ export async function getNotificationTxes ( object: Doc, tx: TxCUD, originTx: TxCUD, - receiver: UserInfo, - sender: UserInfo, + receiver: ReceiverInfo, + sender: SenderInfo, params: NotifyParams, docNotifyContexts: DocNotifyContext[], activityMessages: ActivityMessage[] @@ -805,7 +771,7 @@ export async function createCollabDocInfo ( return res } - const notifyContexts = await control.findAllCtx(ctx, notification.class.DocNotifyContext, { attachedTo: object._id }) + const notifyContexts = await control.findAllCtx(ctx, notification.class.DocNotifyContext, { objectId: object._id }) await updateContextsTimestamp(notifyContexts, originTx.modifiedOn, control, originTx.modifiedBy) @@ -833,12 +799,15 @@ export async function createCollabDocInfo ( {}, async (ctx) => await getUsersInfo(ctx, [...Array.from(targets), originTx.modifiedBy as Ref], control) ) - const sender = usersInfo.find(({ _id }) => _id === originTx.modifiedBy) ?? { + const sender: SenderInfo = usersInfo.find(({ _id }) => _id === originTx.modifiedBy) ?? { _id: originTx.modifiedBy } for (const target of targets) { - const info = usersInfo.find(({ _id }) => _id === target) + const info: ReceiverInfo | undefined = toReceiverInfo( + control.hierarchy, + usersInfo.find(({ _id }) => _id === target) + ) if (info === undefined) continue @@ -858,7 +827,7 @@ export async function createCollabDocInfo ( const id = generateId() as string control.operationContext.derived.targets[id] = (it) => { if (ids.has(it._id)) { - return [info.account?.email as string] + return [info.account?.email] } } } @@ -1048,7 +1017,7 @@ async function updateCollaboratorsMixin ( if (newCollabs.length > 0) { const docNotifyContexts = await control.findAllCtx(ctx, notification.class.DocNotifyContext, { user: { $in: newCollabs }, - attachedTo: tx.objectId + objectId: tx.objectId }) const infos = await ctx.with( @@ -1056,11 +1025,13 @@ async function updateCollaboratorsMixin ( {}, async (ctx) => await getUsersInfo(ctx, [...newCollabs, originTx.modifiedBy] as Ref[], control) ) - const sender = infos.find(({ _id }) => _id === originTx.modifiedBy) ?? { _id: originTx.modifiedBy } + const sender: SenderInfo = infos.find(({ _id }) => _id === originTx.modifiedBy) ?? { _id: originTx.modifiedBy } for (const collab of newCollabs) { - const target = infos.find(({ _id }) => _id === collab) - + const target = toReceiverInfo( + hierarchy, + infos.find(({ _id }) => _id === collab) + ) if (target === undefined) continue for (const message of activityMessages) { @@ -1154,7 +1125,7 @@ async function removeCollaboratorDoc (tx: TxRemoveDoc, control: TriggerCont const res: Tx[] = [] const notifyContexts = await control.findAll( notification.class.DocNotifyContext, - { attachedTo: tx.objectId }, + { objectId: tx.objectId }, { projection: { _id: 1, @@ -1545,7 +1516,7 @@ async function OnActivityMessageRemove (message: ActivityMessage, control: Trigg return [] } - const contexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: message.attachedTo }) + const contexts = await control.findAll(notification.class.DocNotifyContext, { objectId: message.attachedTo }) if (contexts.length === 0) return [] const isLastUpdate = contexts.some((context) => { diff --git a/server-plugins/notification-resources/src/utils.ts b/server-plugins/notification-resources/src/utils.ts index acbd9da66a..d34f1ac3fa 100644 --- a/server-plugins/notification-resources/src/utils.ts +++ b/server-plugins/notification-resources/src/utils.ts @@ -46,8 +46,9 @@ import serverNotification, { getPersonAccountById, HTMLPresenter, NotificationPresenter, - TextPresenter, - UserInfo + ReceiverInfo, + SenderInfo, + TextPresenter } from '@hcengineering/server-notification' import { ActivityMessage, DocUpdateMessage } from '@hcengineering/activity' @@ -300,10 +301,12 @@ export async function updateNotifyContextsSpace ( return [] } - const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: tx.objectId }) + const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { objectId: tx.objectId }) return notifyContexts.map((value) => - control.txFactory.createTxUpdateDoc(value._class, value.space, value._id, { space: updateTx.operations.space }) + control.txFactory.createTxUpdateDoc(value._class, value.space, value._id, { + objectSpace: updateTx.operations.space + }) ) } @@ -319,7 +322,7 @@ export function getTextPresenter (_class: Ref>, hierarchy: Hierarchy) return hierarchy.classHierarchyMixin(_class, serverNotification.mixin.TextPresenter) } -async function getSenderName (control: TriggerControl, sender: UserInfo): Promise { +async function getSenderName (control: TriggerControl, sender: SenderInfo): Promise { if (sender._id === core.account.System) { return await translate(core.string.System, {}) } @@ -340,7 +343,7 @@ async function getFallbackNotificationFullfillment ( object: Doc, originTx: TxCUD, control: TriggerControl, - sender: UserInfo, + sender: SenderInfo, message?: ActivityMessage ): Promise { const title: IntlString = notification.string.CommonNotificationTitle @@ -419,7 +422,7 @@ function getNotificationPresenter (_class: Ref>, hierarchy: Hierarchy export async function getNotificationContent ( originTx: TxCUD, targetUser: PersonAccount, - sender: UserInfo, + sender: SenderInfo, object: Doc, control: TriggerControl, message?: ActivityMessage @@ -471,19 +474,51 @@ export async function getUsersInfo ( ctx: MeasureContext, ids: Ref[], control: TriggerControl -): Promise { +): Promise<(ReceiverInfo | SenderInfo)[]> { const accounts = await control.modelDb.findAll(contact.class.PersonAccount, { _id: { $in: ids } }) + const personIds = accounts.map((it) => it.person) + const accountById = toIdMap(accounts) const persons = toIdMap( await ctx.with( - 'query-find', + 'find-persons', {}, - async () => await control.findAll(contact.class.Person, { _id: { $in: accounts.map((it) => it.person) } }) + async () => await control.findAll(contact.class.Person, { _id: { $in: personIds } }) ) ) + const spaces = await ctx.with('find-person-spaces', {}, async () => { + const res = await control.findAll(contact.class.PersonSpace, { person: { $in: personIds } }) - return accounts.map((account) => ({ - _id: account._id, - account, - person: persons.get(account.person) - })) + return new Map(res.map((s) => [s.person, s])) + }) + + return ids.map((_id) => { + const account = accountById.get(_id) + return { + _id, + account, + person: account !== undefined ? persons.get(account.person) : undefined, + space: account !== undefined ? spaces.get(account.person)?._id : undefined + } + }) +} + +export function toReceiverInfo (hierarchy: Hierarchy, info?: SenderInfo | ReceiverInfo): ReceiverInfo | undefined { + if (info === undefined) return undefined + if (info.person === undefined) return undefined + if (info.account === undefined) return undefined + if (!('space' in info)) return undefined + if (info.space === undefined) return undefined + + const isEmployee = hierarchy.hasMixin(info.person, contact.mixin.Employee) + if (!isEmployee) return undefined + + const employee = hierarchy.as(info.person, contact.mixin.Employee) + if (!employee.active) return undefined + + return { + _id: info._id, + account: info.account, + person: employee, + space: info.space + } } diff --git a/server-plugins/notification/src/index.ts b/server-plugins/notification/src/index.ts index 86329693b1..9101638965 100644 --- a/server-plugins/notification/src/index.ts +++ b/server-plugins/notification/src/index.ts @@ -14,7 +14,7 @@ // limitations under the License. // -import contact, { Employee, Person, PersonAccount } from '@hcengineering/contact' +import contact, { Employee, Person, PersonAccount, PersonSpace } from '@hcengineering/contact' import { Account, Class, Doc, Mixin, Ref, Tx, TxCUD } from '@hcengineering/core' import { BaseNotificationType, @@ -135,7 +135,14 @@ export interface NotificationPresenter extends Class { presenter: Resource } -export interface UserInfo { +export interface ReceiverInfo { + _id: Ref + account: PersonAccount + person: Employee + space: Ref +} + +export interface SenderInfo { _id: Ref account?: PersonAccount person?: Person @@ -146,8 +153,8 @@ export type NotificationProviderFunc = ( types: BaseNotificationType[], object: Doc, data: InboxNotification, - receiver: UserInfo, - sender: UserInfo + receiver: ReceiverInfo, + sender: SenderInfo ) => Promise export interface NotificationProviderResources extends Doc { diff --git a/server-plugins/request-resources/src/index.ts b/server-plugins/request-resources/src/index.ts index 94e5c64bc0..03c5b39d7c 100644 --- a/server-plugins/request-resources/src/index.ts +++ b/server-plugins/request-resources/src/index.ts @@ -24,7 +24,8 @@ import { getNotificationTxes, getCollaborators, getTextPresenter, - getUsersInfo + getUsersInfo, + toReceiverInfo } from '@hcengineering/server-notification-resources' import { PersonAccount } from '@hcengineering/contact' @@ -144,7 +145,7 @@ async function getRequestNotificationTx (tx: TxCollectionCUD, cont if (collaborators.length === 0) return res const notifyContexts = await control.findAll(notification.class.DocNotifyContext, { - attachedTo: doc._id + objectId: doc._id }) const usersInfo = await getUsersInfo(control.ctx, [...collaborators, tx.modifiedBy] as Ref[], control) const senderInfo = usersInfo.find(({ _id }) => _id === tx.modifiedBy) ?? { @@ -152,7 +153,10 @@ async function getRequestNotificationTx (tx: TxCollectionCUD, cont } for (const target of collaborators) { - const targetInfo = usersInfo.find(({ _id }) => _id === target) + const targetInfo = toReceiverInfo( + control.hierarchy, + usersInfo.find(({ _id }) => _id === target) + ) if (targetInfo === undefined) continue const txes = await getNotificationTxes( diff --git a/server-plugins/telegram-resources/src/index.ts b/server-plugins/telegram-resources/src/index.ts index c5d5a81efb..c39226baac 100644 --- a/server-plugins/telegram-resources/src/index.ts +++ b/server-plugins/telegram-resources/src/index.ts @@ -31,9 +31,9 @@ import { } from '@hcengineering/core' import { TriggerControl } from '@hcengineering/server-core' import telegram, { TelegramMessage, TelegramNotificationRecord } from '@hcengineering/telegram' -import notification, { BaseNotificationType, InboxNotification, NotificationType } from '@hcengineering/notification' +import { BaseNotificationType, InboxNotification, NotificationType } from '@hcengineering/notification' import setting, { Integration } from '@hcengineering/setting' -import { NotificationProviderFunc, UserInfo } from '@hcengineering/server-notification' +import { NotificationProviderFunc, ReceiverInfo, SenderInfo } from '@hcengineering/server-notification' import { getMetadata, getResource } from '@hcengineering/platform' import serverTelegram from '@hcengineering/server-telegram' import { getTranslatedNotificationContent, getTextPresenter } from '@hcengineering/server-notification-resources' @@ -78,48 +78,6 @@ export async function OnMessageCreate (tx: Tx, control: TriggerControl): Promise }) res.push(tx) } - - if (message.incoming) { - const docs = await control.findAll(notification.class.DocNotifyContext, { - attachedTo: channel._id, - user: message.modifiedBy - }) - for (const doc of docs) { - // TODO: push inbox notifications - // res.push( - // control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { - // $push: { - // txes: { - // _id: tx._id as Ref>, - // modifiedOn: tx.modifiedOn, - // modifiedBy: tx.modifiedBy, - // isNew: true - // } - // } - // }) - // ) - res.push( - control.txFactory.createTxUpdateDoc(doc._class, doc.space, doc._id, { - lastUpdateTimestamp: tx.modifiedOn - }) - ) - } - if (docs.length === 0) { - res.push( - control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, channel.space, { - user: tx.modifiedBy, - attachedTo: channel._id, - attachedToClass: channel._class, - lastUpdateTimestamp: tx.modifiedOn, - isPinned: false - // TODO: push inbox notifications - // txes: [ - // { _id: tx._id as Ref>, modifiedOn: tx.modifiedOn, modifiedBy: tx.modifiedBy, isNew: true } - // ] - }) - ) - } - } } return res @@ -228,17 +186,13 @@ const SendTelegramNotifications: NotificationProviderFunc = async ( types: BaseNotificationType[], doc: Doc, data: InboxNotification, - receiver: UserInfo, - sender: UserInfo + receiver: ReceiverInfo, + sender: SenderInfo ): Promise => { if (types.length === 0) { return [] } - if (receiver.person === undefined || receiver.account?.email === undefined) { - return [] - } - const botUrl = getMetadata(serverTelegram.metadata.BotUrl) if (botUrl === undefined || botUrl === '') { @@ -246,15 +200,7 @@ const SendTelegramNotifications: NotificationProviderFunc = async ( return [] } - const isEmployee = control.hierarchy.hasMixin(receiver.person, contact.mixin.Employee) - - if (!isEmployee) { - return [] - } - - const employee = control.hierarchy.as(receiver.person, contact.mixin.Employee) - - if (!employee.active) { + if (!receiver.person.active) { return [] } diff --git a/server-plugins/time-resources/src/index.ts b/server-plugins/time-resources/src/index.ts index 199a18c2e8..0301f5bb3b 100644 --- a/server-plugins/time-resources/src/index.ts +++ b/server-plugins/time-resources/src/index.ts @@ -44,7 +44,7 @@ import { jsonToMarkup, nodeDoc, nodeParagraph, nodeText } from '@hcengineering/t import tracker, { Issue, IssueStatus, Project, TimeSpendReport } from '@hcengineering/tracker' import serverTime, { OnToDo, ToDoFactory } from '@hcengineering/server-time' import time, { ProjectToDo, ToDo, ToDoPriority, TodoAutomationHelper, WorkSlot } from '@hcengineering/time' -import { UserInfo } from '@hcengineering/server-notification' +import { ReceiverInfo, SenderInfo } from '@hcengineering/server-notification' /** * @public @@ -186,9 +186,23 @@ export async function OnToDoCreate (tx: TxCUD, control: TriggerControl): Pr } const object = (await control.findAll(todo.attachedToClass, { _id: todo.attachedTo }))[0] - if (object === undefined) return [] + const person = ( + await control.findAll(contact.mixin.Employee, { _id: account.person as Ref, active: true }, { limit: 1 }) + )[0] + if (person === undefined) return [] + + const personSpace = (await control.findAll(contact.class.PersonSpace, { person: account.person }, { limit: 1 }))[0] + if (personSpace === undefined) return [] + + const receiverInfo: ReceiverInfo = { + _id: account._id, + account, + person, + space: personSpace._id + } + const senderAccount = await control.modelDb.findOne(contact.class.PersonAccount, { _id: tx.modifiedBy as Ref }) @@ -197,13 +211,12 @@ export async function OnToDoCreate (tx: TxCUD, control: TriggerControl): Pr ? (await control.findAll(contact.class.Person, { _id: senderAccount.person }))[0] : undefined - const senderInfo: UserInfo = { + const senderInfo: SenderInfo = { _id: tx.modifiedBy, account: senderAccount, person: senderPerson } - const res: Tx[] = [] const notifyResult = await isShouldNotifyTx(control, createTx, tx, todo, account, true, false) const content = await getNotificationContent(tx, account, senderInfo, todo, control) const data: Partial> = { @@ -215,30 +228,18 @@ export async function OnToDoCreate (tx: TxCUD, control: TriggerControl): Pr messageHtml: jsonToMarkup(nodeDoc(nodeParagraph(nodeText(todo.title)))) } - const person = (await control.modelDb.findAll(contact.class.Person, { _id: account.person }))[0] - - const receiverInfo: UserInfo = { - _id: account._id, - account, - person - } - - res.push( - ...(await getCommonNotificationTxes( - control, - object, - data, - receiverInfo, - senderInfo, - object._id, - object._class, - object.space, - createTx.modifiedOn, - notifyResult - )) + return await getCommonNotificationTxes( + control, + object, + data, + receiverInfo, + senderInfo, + object._id, + object._class, + object.space, + createTx.modifiedOn, + notifyResult ) - - return res } /** diff --git a/server/account/src/operations.ts b/server/account/src/operations.ts index 863208e1ac..a2c621bbef 100644 --- a/server/account/src/operations.ts +++ b/server/account/src/operations.ts @@ -1953,7 +1953,7 @@ async function createPersonAccount ( const ops = new TxOperations(connection, core.account.System) const name = combineName(account.first, account.last) - // Check if EmployeeAccount is not exists + // Check if PersonAccount is not exists if (shouldReplaceCurrent) { const currentAccount = await ops.findOne(contact.class.PersonAccount, {}) if (currentAccount !== undefined) { diff --git a/server/mongo/src/storage.ts b/server/mongo/src/storage.ts index a011c93566..89b96d1f0e 100644 --- a/server/mongo/src/storage.ts +++ b/server/mongo/src/storage.ts @@ -1520,8 +1520,7 @@ class MongoTxAdapter extends MongoAdapterBase implements TxAdapter { (tx._class === core.class.TxCreateDoc || tx._class === core.class.TxUpdateDoc || tx._class === core.class.TxRemoveDoc) && - ((tx as TxCUD).objectClass === 'contact:class:PersonAccount' || - (tx as TxCUD).objectClass === 'contact:class:EmployeeAccount') + (tx as TxCUD).objectClass === 'contact:class:PersonAccount' ) } model.forEach((tx) => (tx.modifiedBy === core.account.System && !isPersonAccount(tx) ? systemTx : userTx).push(tx)) diff --git a/server/tool/src/index.ts b/server/tool/src/index.ts index 58c7b4f186..e6e3c503eb 100644 --- a/server/tool/src/index.ts +++ b/server/tool/src/index.ts @@ -332,7 +332,7 @@ export async function upgradeModel ( (it) => it.modifiedBy !== core.account.System || (it as TxCUD).objectClass === contact.class.Person || - (it as TxCUD).objectClass === 'contact:class:EmployeeAccount' + (it as TxCUD).objectClass === 'contact:class:PersonAccount' ) ) ] diff --git a/services/github/pod-github/src/notifications.ts b/services/github/pod-github/src/notifications.ts index 65be33bb17..a7884d06ac 100644 --- a/services/github/pod-github/src/notifications.ts +++ b/services/github/pod-github/src/notifications.ts @@ -1,19 +1,21 @@ import { Account, Doc, Ref, TxOperations } from '@hcengineering/core' import notification, { DocNotifyContext } from '@hcengineering/notification' import { IntlString } from '@hcengineering/platform' +import { PersonSpace } from '@hcengineering/contact' import github from '@hcengineering/github' export async function createNotification ( client: TxOperations, forDoc: Doc, - data: { user: Ref, message: IntlString, props: Record } + data: { user: Ref, space: Ref, message: IntlString, props: Record } ): Promise { - let docNotifyContext = await client.findOne(notification.class.DocNotifyContext, { attachedTo: forDoc._id }) + let docNotifyContext = await client.findOne(notification.class.DocNotifyContext, { objectId: forDoc._id }) if (docNotifyContext?._id === undefined) { - const docNotifyContextId = await client.createDoc(notification.class.DocNotifyContext, forDoc.space, { - attachedTo: forDoc._id, - attachedToClass: forDoc._class, + const docNotifyContextId = await client.createDoc(notification.class.DocNotifyContext, data.space, { + objectId: forDoc._id, + objectClass: forDoc._class, + objectSpace: forDoc.space, user: data.user, isPinned: false }) @@ -21,7 +23,6 @@ export async function createNotification ( } // Check if we had already same notification send, and just unmark it viewed. - const existing = await client.findOne(notification.class.CommonInboxNotification, { user: data.user, message: data.message, @@ -32,7 +33,7 @@ export async function createNotification ( isViewed: false }) } else { - await client.createDoc(notification.class.CommonInboxNotification, forDoc.space, { + await client.createDoc(notification.class.CommonInboxNotification, data.space, { user: data.user, icon: github.icon.Github, message: data.message, diff --git a/services/github/pod-github/src/platform.ts b/services/github/pod-github/src/platform.ts index 4bc9f17347..292a7cdc09 100644 --- a/services/github/pod-github/src/platform.ts +++ b/services/github/pod-github/src/platform.ts @@ -409,13 +409,17 @@ export class PlatformWorker { const person = (await client.findOne(contact.class.Person, { _id: account.person })) as Person if (person !== undefined) { if (!revoke) { - await createNotification(client, person, { - user: account._id, - message: github.string.AuthenticatedWithGithub, - props: { - login: update.login - } - }) + const personSpace = await client.findOne(contact.class.PersonSpace, { person: person._id }) + if (personSpace !== undefined) { + await createNotification(client, person, { + user: account._id, + space: personSpace._id, + message: github.string.AuthenticatedWithGithub, + props: { + login: update.login + } + }) + } const githubAccount = (await client.findOne(core.class.Account, { email: 'github:' + update.login @@ -427,23 +431,31 @@ export class PlatformWorker { const dPerson = (await client.findOne(contact.class.Person, { _id: dummyPerson })) as Person if (person !== undefined && dPerson !== undefined) { - await createNotification(client, dPerson, { - user: account._id, - message: github.string.AuthenticatedWithGithubEmployee, - props: { - login: update.login - } - }) + const personSpace = await client.findOne(contact.class.PersonSpace, { person: person._id }) + if (personSpace !== undefined) { + await createNotification(client, dPerson, { + user: githubAccount._id, + space: personSpace._id, + message: github.string.AuthenticatedWithGithubEmployee, + props: { + login: update.login + } + }) + } } } } else { - await createNotification(client, person, { - user: account._id, - message: github.string.AuthenticationRevokedGithub, - props: { - login: update.login - } - }) + const personSpace = await client.findOne(contact.class.PersonSpace, { person: person._id }) + if (personSpace !== undefined) { + await createNotification(client, person, { + user: account._id, + space: personSpace._id, + message: github.string.AuthenticationRevokedGithub, + props: { + login: update.login + } + }) + } } } diff --git a/services/github/pod-github/src/worker.ts b/services/github/pod-github/src/worker.ts index dc6c4f1a74..cf4aff8570 100644 --- a/services/github/pod-github/src/worker.ts +++ b/services/github/pod-github/src/worker.ts @@ -543,11 +543,15 @@ export class GithubWorker implements IntegrationManager { if (accountRef !== undefined) { const person = await this.client.findOne(contact.class.Person, { _id: accountRef.person }) if (person !== undefined) { - await createNotification(this._client, person, { - user: account, - message: github.string.AuthenticatedWithGithubRequired, - props: {} - }) + const personSpace = await this.client.findOne(contact.class.PersonSpace, { person: person._id }) + if (personSpace !== undefined) { + await createNotification(this._client, person, { + user: account, + space: personSpace._id, + message: github.string.AuthenticatedWithGithubRequired, + props: {} + }) + } } } this.ctx.info('get octokit: return bot', { account }) From 949f33380c6c0305d1876001451cf3b825bd6320 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Tue, 6 Aug 2024 17:37:26 +0700 Subject: [PATCH 11/20] UBERF-7817: Fix tag element query (#6267) Signed-off-by: Andrey Sobolev --- .../DocUpdateMessageContent.svelte | 22 ++++++++++----- .../DocUpdateMessageObjectValue.svelte | 27 ++++++++++--------- .../DocUpdateMessagePresenter.svelte | 14 +++++----- .../tags-resources/src/components/Tags.svelte | 9 +------ .../src/components/TagsEditor.svelte | 19 ++++++++++--- 5 files changed, 55 insertions(+), 36 deletions(-) diff --git a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessageContent.svelte b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessageContent.svelte index 8ec5e747e0..222306696f 100644 --- a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessageContent.svelte +++ b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessageContent.svelte @@ -58,30 +58,40 @@ {@const removeMessages = valueMessages.filter(({ action }) => action === 'remove')} {@const createMessages = valueMessages.filter(({ action }) => action === 'create')} + {@const createMessagesLen = createMessages.length} + {@const removeMessagesLen = removeMessages.length} + {#each createMessages as valueMessage, index} 1 && index !== createMessages.length - 1} + hasSeparator={createMessagesLen > 1 && index !== createMessagesLen - 1} {preview} /> {/each} {#each removeMessages as valueMessage, index} 1 && index !== removeMessages.length - 1} + hasSeparator={removeMessagesLen > 1 && index !== removeMessagesLen - 1} {preview} /> {/each} {:else} + {@const len = valueMessages.length} {#each valueMessages as valueMessage, index} 1 && index !== valueMessages.length - 1} + hasSeparator={len > 1 && index !== len - 1} {preview} /> {/each} diff --git a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessageObjectValue.svelte b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessageObjectValue.svelte index 346c23a44f..d2ce1ee29a 100644 --- a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessageObjectValue.svelte +++ b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessageObjectValue.svelte @@ -13,14 +13,16 @@ // limitations under the License. --> {#if object} - {#if withIcon && message.action === 'create'} + {#if withIcon && action === 'create'} {/if} - {#if withIcon && message.action === 'remove'} + {#if withIcon && action === 'remove'} {/if} @@ -80,7 +83,7 @@ diff --git a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePresenter.svelte b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePresenter.svelte index 948ffef6a9..2c7e5b007c 100644 --- a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePresenter.svelte +++ b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePresenter.svelte @@ -26,7 +26,7 @@ import { Account, AttachedDoc, Class, Collection, Doc, Ref, Space } from '@hcengineering/core' import { IntlString } from '@hcengineering/platform' import { createQuery, getClient } from '@hcengineering/presentation' - import { Component, ShowMore, Action } from '@hcengineering/ui' + import { Action, Component, ShowMore } from '@hcengineering/ui' import { AttributeModel } from '@hcengineering/view' import { buildRemovedDoc, checkIsObjectRemoved } from '@hcengineering/view-resources' @@ -144,17 +144,17 @@ } async function loadParentObject ( - message: DocUpdateMessage, - parentMessage?: ActivityMessage, + message: Pick, + parentMessage?: Pick, doc?: Doc ): Promise { - if (!parentMessage && message.objectId === message.attachedTo) { + if (parentMessage === undefined && message.objectId === message.attachedTo) { return } - const _id = parentMessage ? parentMessage.attachedTo : message.attachedTo - const _class = parentMessage ? parentMessage.attachedToClass : message.attachedToClass - const space = parentMessage ? parentMessage.space : message.space + const _id = parentMessage !== undefined ? parentMessage.attachedTo : message.attachedTo + const _class = parentMessage !== undefined ? parentMessage.attachedToClass : message.attachedToClass + const space = parentMessage !== undefined ? parentMessage.space : message.space if (doc !== undefined && doc._id === _id) { parentObject = doc diff --git a/plugins/tags-resources/src/components/Tags.svelte b/plugins/tags-resources/src/components/Tags.svelte index b9ac7a793a..f33a451bf0 100644 --- a/plugins/tags-resources/src/components/Tags.svelte +++ b/plugins/tags-resources/src/components/Tags.svelte @@ -13,7 +13,7 @@ // limitations under the License. --> {#if presenter} - + {:else} {#if attributeUpdates.added.length} diff --git a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePresenter.svelte b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePresenter.svelte index 2c7e5b007c..4571dcaa6b 100644 --- a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePresenter.svelte +++ b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePresenter.svelte @@ -229,7 +229,14 @@ /> {:else if value.attributeUpdates && attributeModel} - + {/if} diff --git a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePreview.svelte b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePreview.svelte index 140ba05c86..1e3764af17 100644 --- a/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePreview.svelte +++ b/plugins/activity-resources/src/components/doc-update-message/DocUpdateMessagePreview.svelte @@ -130,6 +130,8 @@ {attributeModel} {space} {viewlet} + {object} + message={value} preview /> {/if} diff --git a/plugins/activity/src/index.ts b/plugins/activity/src/index.ts index 741311721c..256356380f 100644 --- a/plugins/activity/src/index.ts +++ b/plugins/activity/src/index.ts @@ -67,6 +67,7 @@ export interface ActivityMessageControl extends Doc { // Skip field activity operations. skipFields?: (keyof T)[] + allowedFields?: (keyof T)[] } /** diff --git a/plugins/chunter-assets/lang/en.json b/plugins/chunter-assets/lang/en.json index 47da2905af..72121e5360 100644 --- a/plugins/chunter-assets/lang/en.json +++ b/plugins/chunter-assets/lang/en.json @@ -111,6 +111,14 @@ "JoinChannelText": "Once you've joined, you'll be able to read all messages and contribute to the discussion.", "NoMessagesInChannel": "Currently there are no messages", "SendMessagesInChannel": "Send the first message to start the conversation", - "LatestMessages": "↓ Latest messages" + "LatestMessages": "↓ Latest messages", + "Joined": "Joined", + "Left": "Left", + "Added": "Added", + "Removed": "Removed", + "CreatedChannelOn": "Created this channel on {date}", + "ChannelMessages": "Channel messages", + "JoinChannel": "Join channel", + "YouJoinedChannel": "You have been joined to channel" } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/es.json b/plugins/chunter-assets/lang/es.json index aeb970861d..9cbd0877e7 100644 --- a/plugins/chunter-assets/lang/es.json +++ b/plugins/chunter-assets/lang/es.json @@ -111,6 +111,14 @@ "JoinChannelText": "Una vez que se haya unido, podrá leer todos los mensajes y contribuir a la discusión.", "NoMessagesInChannel": "No hay mensajes en este canal todavía.", "SendMessagesInChannel": "Envíe mensajes en este canal para comenzar la conversación.", - "LatestMessages": "↓ Últimos mensajes" + "LatestMessages": "↓ Últimos mensajes", + "Joined": "Unido", + "Left": "Abandonado", + "Added": "Añadido", + "Removed": "Eliminado", + "CreatedChannelOn": "Creó este canal el {date}", + "ChannelMessages": "Mensajes del canal", + "JoinChannel": "Unirse", + "YouJoinedChannel": "Te has unido al canal" } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/fr.json b/plugins/chunter-assets/lang/fr.json index 9a784ce568..fb42dd0389 100644 --- a/plugins/chunter-assets/lang/fr.json +++ b/plugins/chunter-assets/lang/fr.json @@ -111,6 +111,14 @@ "JoinChannelText": "Une fois que vous avez rejoint, vous pourrez lire tous les messages et participer à la discussion.", "NoMessagesInChannel": "Il n'y a pas encore de messages dans ce canal.", "SendMessagesInChannel": "Envoyez des messages pour commencer la conversation.", - "LatestMessages": "↓ Derniers messages" + "LatestMessages": "↓ Derniers messages", + "Joined": "Rejoint", + "Left": "Quitté", + "Added": "Ajouté", + "Removed": "Supprimé", + "CreatedChannelOn": "A créé ce canal le {date}", + "ChannelMessages": "Messages du canal", + "JoinChannel": "Rejoindre", + "YouJoinedChannel": "Vous avez rejoint le canal" } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/pt.json b/plugins/chunter-assets/lang/pt.json index 74633c8111..3758587fdf 100644 --- a/plugins/chunter-assets/lang/pt.json +++ b/plugins/chunter-assets/lang/pt.json @@ -111,6 +111,14 @@ "JoinChannelText": "Depois de entrar, você poderá ler todas as mensagens e contribuir na discussão.", "NoMessagesInChannel": "Ainda não existem mensagens neste canal.", "SendMessagesInChannel": "Envie a sua primeira mensagem!", - "LatestMessages": "↓ Últimas mensagens" + "LatestMessages": "↓ Últimas mensagens", + "Joined": "Entrou", + "Left": "Saiu", + "Added": "Adicionado(a)", + "Removed": "Removido(a)", + "CreatedChannelOn": "Criou este canal em {date}", + "ChannelMessages": "Mensagens do canal", + "JoinChannel": "Participar no canal", + "YouJoinedChannel": "Entrou no canal" } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/ru.json b/plugins/chunter-assets/lang/ru.json index e545f5bca9..f18301ff30 100644 --- a/plugins/chunter-assets/lang/ru.json +++ b/plugins/chunter-assets/lang/ru.json @@ -111,6 +111,14 @@ "JoinChannelText": "Присоединившись, вы сможете читать все сообщения и участвовать в обсуждении.", "NoMessagesInChannel": "В этом канале пока нет сообщений", "SendMessagesInChannel": "Отправьте первое сообщение, чтобы начать общение", - "LatestMessages": "↓ Последние сообщения" + "LatestMessages": "↓ Последние сообщения", + "Joined": "Присоединился", + "Left": "Покинул", + "Added": "Добавил(а)", + "Removed": "Исключил(а)", + "CreatedChannelOn": "Создал этот канал {date}", + "ChannelMessages": "Сообщения каналов", + "JoinChannel": "Приссоединение к каналу", + "YouJoinedChannel": "Вы присоединились к каналу" } } \ No newline at end of file diff --git a/plugins/chunter-assets/lang/zh.json b/plugins/chunter-assets/lang/zh.json index e182b46825..0da4296619 100644 --- a/plugins/chunter-assets/lang/zh.json +++ b/plugins/chunter-assets/lang/zh.json @@ -111,6 +111,14 @@ "JoinChannelText": "加入后,你将能够阅读所有消息并参与讨论。", "NoMessagesInChannel": "此频道中没有消息。", "SendMessagesInChannel": "在此频道中发送消息。", - "LatestMessages": "↓ 最新消息" + "LatestMessages": "↓ 最新消息", + "Joined": "已加入", + "Left": "已离开", + "Added": "已添加", + "Removed": "已移除", + "CreatedChannelOn": "于 {date} 创建此频道", + "ChannelMessages": "频道消息", + "JoinChannel": "加入频道", + "YouJoinedChannel": "你已加入频道" } } diff --git a/plugins/chunter-resources/src/channelDataProvider.ts b/plugins/chunter-resources/src/channelDataProvider.ts index 7334a5ad11..db3126ba62 100644 --- a/plugins/chunter-resources/src/channelDataProvider.ts +++ b/plugins/chunter-resources/src/channelDataProvider.ts @@ -29,11 +29,8 @@ import { derived, get, type Readable, writable } from 'svelte/store' import activity, { type ActivityMessage, type ActivityReference } from '@hcengineering/activity' import attachment from '@hcengineering/attachment' import { combineActivityMessages, sortActivityMessages } from '@hcengineering/activity-resources' -import { type ChatMessage } from '@hcengineering/chunter' import notification, { type DocNotifyContext } from '@hcengineering/notification' -import chunter from './plugin' - export type LoadMode = 'forward' | 'backward' export interface MessageMetadata { @@ -241,7 +238,7 @@ export class ChannelDataProvider implements IChannelDataProvider { if (loadAll) { this.isTailLoading.set(true) - this.loadTail(undefined, combineActivityMessages) + this.loadTail() } else if (isLoadingLatest) { const startIndex = Math.max(0, count - this.limit) this.isTailLoading.set(true) @@ -260,11 +257,7 @@ export class ChannelDataProvider implements IChannelDataProvider { this.isInitialLoadedStore.set(true) } - private loadTail ( - start?: Timestamp, - afterLoad?: (msgs: ActivityMessage[]) => Promise, - query?: DocumentQuery - ): void { + private loadTail (start?: Timestamp, query?: DocumentQuery): void { if (this.chatId === undefined) { this.isTailLoading.set(false) return @@ -283,12 +276,8 @@ export class ChannelDataProvider implements IChannelDataProvider { ...(this.tailStart !== undefined ? { createdOn: { $gte: this.tailStart } } : {}) }, async (res) => { - if (afterLoad !== undefined) { - const result = await afterLoad(res.reverse()) - this.tailStore.set(result) - } else { - this.tailStore.set(res.reverse()) - } + const result = await combineActivityMessages(res.reverse()) + this.tailStore.set(result) this.isTailLoaded.set(true) this.isTailLoading.set(false) @@ -325,7 +314,7 @@ export class ChannelDataProvider implements IChannelDataProvider { const skipIds = this.getChunkSkipIds(loadAfter) const messages = await client.findAll( - chunter.class.ChatMessage, + this.msgClass, { attachedTo: this.chatId, space: this.space, @@ -351,11 +340,11 @@ export class ChannelDataProvider implements IChannelDataProvider { return { from: from.createdOn ?? from.modifiedOn, to: to.createdOn ?? to.modifiedOn, - data: isBackward ? messages.reverse() : messages + data: isBackward ? await combineActivityMessages(messages.reverse()) : await combineActivityMessages(messages) } } - getChunkSkipIds (after: Timestamp, loadTail = false): Array> { + getChunkSkipIds (after: Timestamp, loadTail = false): Array> { const chunks = get(this.chunksStore) const metadata = get(this.metadataStore) const tail = get(this.tailStore) @@ -367,7 +356,7 @@ export class ChannelDataProvider implements IChannelDataProvider { .flat() .concat(loadTail ? [] : tailData) .filter(({ createdOn }) => createdOn === after) - .map(({ _id }) => _id) as Array> + .map(({ _id }) => _id) } async loadNext (mode: LoadMode, loadAfter?: Timestamp, limit?: number): Promise { @@ -467,7 +456,7 @@ export class ChannelDataProvider implements IChannelDataProvider { if (tailAfter !== undefined) { const skipIds = chunks[chunks.length - 1]?.data.map(({ _id }) => _id) ?? [] - this.loadTail(tailAfter, undefined, { _id: { $nin: skipIds } }) + this.loadTail(tailAfter, { _id: { $nin: skipIds } }) this.isLoadingMoreStore.set(false) return } diff --git a/plugins/chunter-resources/src/components/Channel.svelte b/plugins/chunter-resources/src/components/Channel.svelte index 656489358c..966c148705 100644 --- a/plugins/chunter-resources/src/components/Channel.svelte +++ b/plugins/chunter-resources/src/components/Channel.svelte @@ -13,7 +13,7 @@ // limitations under the License. --> + +{#if value} + + + +{/if} + + diff --git a/plugins/chunter-resources/src/components/activity/MembersChangedMessage.svelte b/plugins/chunter-resources/src/components/activity/MembersChangedMessage.svelte new file mode 100644 index 0000000000..650b271481 --- /dev/null +++ b/plugins/chunter-resources/src/components/activity/MembersChangedMessage.svelte @@ -0,0 +1,128 @@ + + + + + + {#if addedPersons.length > 0} + + {#if isJoined} + + + {object?.name} + {:else} + + {/if} + {#if differentActions} + + + diff --git a/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte b/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte index a524e646bc..83a6fc5255 100644 --- a/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte +++ b/plugins/chunter-resources/src/components/chat/navigator/ChatNavGroup.svelte @@ -14,7 +14,7 @@ --> + + diff --git a/plugins/chunter-resources/src/index.ts b/plugins/chunter-resources/src/index.ts index 9e7bb6719e..c053acb199 100644 --- a/plugins/chunter-resources/src/index.ts +++ b/plugins/chunter-resources/src/index.ts @@ -48,6 +48,9 @@ import ChatMessageNotificationLabel from './components/notification/ChatMessageN import ChatAside from './components/chat/ChatAside.svelte' import ThreadMessagePreview from './components/threads/ThreadMessagePreview.svelte' import ChatMessagePreview from './components/chat-message/ChatMessagePreview.svelte' +import ChannelCreatedMessage from './components/activity/ChannelCreatedMessage.svelte' +import MembersChangedMessage from './components/activity/MembersChangedMessage.svelte' +import JoinChannelNotificationPresenter from './components/notification/JoinChannelNotificationPresenter.svelte' import { ChannelTitleProvider, @@ -177,7 +180,12 @@ export default async (): Promise => ({ ThreadNotificationPresenter, ChatAside, ThreadMessagePreview, - ChatMessagePreview + ChatMessagePreview, + JoinChannelNotificationPresenter + }, + activity: { + ChannelCreatedMessage, + MembersChangedMessage }, function: { GetDmName: getDmName, diff --git a/plugins/chunter-resources/src/navigation.ts b/plugins/chunter-resources/src/navigation.ts index 06c02167aa..5dd14280c7 100644 --- a/plugins/chunter-resources/src/navigation.ts +++ b/plugins/chunter-resources/src/navigation.ts @@ -1,4 +1,11 @@ -import { getCurrentLocation, getCurrentResolvedLocation, getLocation, type Location, navigate } from '@hcengineering/ui' +import { + closePanel, + getCurrentLocation, + getCurrentResolvedLocation, + getLocation, + type Location, + navigate +} from '@hcengineering/ui' import { type Ref, type Doc, type Class } from '@hcengineering/core' import type { ActivityMessage } from '@hcengineering/activity' import { chunterId, type ChunterSpace, type ThreadMessage } from '@hcengineering/chunter' @@ -184,5 +191,6 @@ export async function resetChunterLocIfEqual (_id: Ref, _class: Ref, ThreadNotification: '' as Ref, ChannelNotification: '' as Ref, + JoinChannelNotification: '' as Ref, ThreadMessageViewlet: '' as Ref }, app: { diff --git a/plugins/contact-resources/src/components/PersonContent.svelte b/plugins/contact-resources/src/components/PersonContent.svelte index 55e4fed58a..2964d891b1 100644 --- a/plugins/contact-resources/src/components/PersonContent.svelte +++ b/plugins/contact-resources/src/components/PersonContent.svelte @@ -50,6 +50,7 @@ export let compact: boolean = false export let showStatus: boolean = false export let type: ObjectPresenterType = 'link' + export let overflowLabel = true const client = getClient() @@ -93,6 +94,7 @@ {type} {maxWidth} {showStatus} + {overflowLabel} /> {/if} {#if shouldShowName} - + {name} {/if} {:else if type === 'text'} - + {name} {/if} diff --git a/plugins/contact-resources/src/components/PersonPresenter.svelte b/plugins/contact-resources/src/components/PersonPresenter.svelte index 8e8b438150..3674461c1d 100644 --- a/plugins/contact-resources/src/components/PersonPresenter.svelte +++ b/plugins/contact-resources/src/components/PersonPresenter.svelte @@ -43,6 +43,7 @@ export let compact = false export let type: ObjectPresenterType = 'link' export let showStatus: boolean = false + export let overflowLabel = true const client = getClient() $: personValue = typeof value === 'string' ? $personByIdStore.get(value) : value @@ -99,6 +100,7 @@ {compact} {type} {showStatus} + {overflowLabel} on:accent-color /> {/if} diff --git a/plugins/notification-resources/src/utils.ts b/plugins/notification-resources/src/utils.ts index 4fd9a7ea6a..2d51edccfd 100644 --- a/plugins/notification-resources/src/utils.ts +++ b/plugins/notification-resources/src/utils.ts @@ -83,7 +83,8 @@ export function loadNotificationSettings (): void { providersSettings.set(res) } ) - typeSettingsQuery.query(notification.class.NotificationTypeSetting, {}, (res) => { + + typeSettingsQuery.query(notification.class.NotificationTypeSetting, { space: core.space.Workspace }, (res) => { typesSettings.set(res) }) } diff --git a/plugins/view-resources/src/plugin.ts b/plugins/view-resources/src/plugin.ts index ed8e8cdc50..292131de8d 100644 --- a/plugins/view-resources/src/plugin.ts +++ b/plugins/view-resources/src/plugin.ts @@ -88,7 +88,6 @@ export default mergeIds(viewId, view, { AfterDate: '' as IntlString, BetweenDates: '' as IntlString, SaveAs: '' as IntlString, - And: '' as IntlString, Between: '' as IntlString, ShowColors: '' as IntlString, Show: '' as IntlString, diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts index fc8463d468..df90297256 100644 --- a/plugins/view/src/index.ts +++ b/plugins/view/src/index.ts @@ -209,7 +209,8 @@ const view = plugin(viewId, { Unpin: '' as IntlString, Join: '' as IntlString, Leave: '' as IntlString, - Copied: '' as IntlString + Copied: '' as IntlString, + And: '' as IntlString }, icon: { Table: '' as Asset, diff --git a/server-plugins/activity-resources/src/index.ts b/server-plugins/activity-resources/src/index.ts index b4c5992a7f..e8f2e096a9 100644 --- a/server-plugins/activity-resources/src/index.ts +++ b/server-plugins/activity-resources/src/index.ts @@ -150,8 +150,7 @@ export async function createReactionNotifications ( tx, parentMessage, [docUpdateMessage], - { isOwn: true, isSpace: false, shouldUpdateTimestamp: false }, - new Map() + { isOwn: true, isSpace: false, shouldUpdateTimestamp: false } ) ) @@ -264,12 +263,11 @@ export async function generateDocUpdateMessages ( originTx?: TxCUD, objectCache?: DocObjectCache ): Promise[]> { - const { hierarchy } = control - if (tx.space === core.space.DerivedTx) { return res } + const { hierarchy } = control const etx = TxProcessor.extractTx(tx) as TxCUD if ( @@ -321,6 +319,7 @@ export async function generateDocUpdateMessages ( let doc = objectCache?.docs?.get(tx.objectId) if (doc === undefined) { doc = (await control.findAll(tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] + objectCache?.docs?.set(tx.objectId, doc) } return await ctx.with( 'pushDocUpdateMessages', @@ -349,6 +348,7 @@ export async function generateDocUpdateMessages ( let doc = objectCache?.docs?.get(tx.objectId) if (doc === undefined) { doc = (await control.findAll(tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] + objectCache?.docs?.set(tx.objectId, doc) } if (doc !== undefined) { return await ctx.with( @@ -376,33 +376,35 @@ export async function generateDocUpdateMessages ( } async function ActivityMessagesHandler (tx: TxCUD, control: TriggerControl): Promise { - if (tx.space === core.space.DerivedTx) { - return [] - } - if (control.hierarchy.isDerived(tx.objectClass, activity.class.ActivityMessage)) { return [] } + const cache: DocObjectCache = { + docs: new Map(), + transactions: new Map() + } const txes = await control.ctx.with( 'generateDocUpdateMessages', {}, - async (ctx) => await generateDocUpdateMessages(ctx, tx, control) + async (ctx) => await generateDocUpdateMessages(ctx, tx, control, [], undefined, cache) ) - if (txes.length === 0) { - return [] - } - const messages = txes.map((messageTx) => TxProcessor.createDoc2Doc(messageTx.tx as TxCreateDoc)) const notificationTxes = await control.ctx.with( 'createCollaboratorNotifications', {}, - async (ctx) => await createCollaboratorNotifications(ctx, tx, control, messages) + async (ctx) => + await createCollaboratorNotifications(ctx, tx, control, messages, undefined, cache.docs as Map, Doc>) ) - return [...txes, ...notificationTxes] + const result = [...txes, ...notificationTxes] + + if (result.length > 0) { + await control.apply(result) + } + return [] } async function OnDocRemoved (originTx: TxCUD, control: TriggerControl): Promise { diff --git a/server-plugins/activity-resources/src/utils.ts b/server-plugins/activity-resources/src/utils.ts index 23070abd10..1617adcd1c 100644 --- a/server-plugins/activity-resources/src/utils.ts +++ b/server-plugins/activity-resources/src/utils.ts @@ -250,14 +250,12 @@ export async function getTxAttributesUpdates ( const hierarchy = control.hierarchy - const filterSet = new Set() - for (const c of controlRules ?? []) { - for (const f of c.skipFields ?? []) { - filterSet.add(f) - } - } + const allowedFields = new Set(controlRules?.flatMap((it) => it.allowedFields ?? []) ?? []) + const skipFields = new Set(controlRules?.flatMap((it) => it.skipFields ?? []) ?? []) - const keys = getAvailableAttributesKeys(tx, hierarchy).filter((it) => !filterSet.has(it)) + const keys = getAvailableAttributesKeys(tx, hierarchy).filter( + (it) => !skipFields.has(it) && (allowedFields.size === 0 || allowedFields.has(it)) + ) if (keys.length === 0) { return [] @@ -268,14 +266,7 @@ export async function getTxAttributesUpdates ( const isMixin = hierarchy.isDerived(tx._class, core.class.TxMixin) const mixin = isMixin ? (tx as TxMixin).mixin : undefined - const { doc, prevDoc } = await getDocDiff( - control, - updateObject._class, - updateObject._id, - originTx._id, - mixin, - objectCache - ) + let docDiff: { doc?: Doc, prevDoc?: Doc } | undefined for (const key of keys) { let attrValue = modifiedAttributes[key] @@ -302,14 +293,25 @@ export async function getTxAttributesUpdates ( continue } - if (Array.isArray(attrValue) && doc != null) { - const diff = await getAttributeDiff(control, doc, prevDoc, key, attrClass, isMixin) + if ( + hierarchy.isDerived(attrClass, core.class.TypeMarkup) || + hierarchy.isDerived(attrClass, core.class.TypeCollaborativeMarkup) || + mixin === notification.mixin.Collaborators + ) { + if (docDiff === undefined) { + docDiff = await getDocDiff(control, updateObject._class, updateObject._id, originTx._id, mixin, objectCache) + } + } + + if (Array.isArray(attrValue) && docDiff?.doc !== undefined) { + const diff = await getAttributeDiff(control, docDiff.doc, docDiff.prevDoc, key, attrClass, isMixin) added.push(...diff.added) removed.push(...diff.removed) attrValue = [] } - if (prevDoc !== undefined) { + if (docDiff?.prevDoc !== undefined) { + const { prevDoc } = docDiff const rawPrevValue = isMixin ? (hierarchy.as(prevDoc, attrClass) as any)[key] : (prevDoc as any)[key] if (Array.isArray(rawPrevValue)) { diff --git a/server-plugins/chunter-resources/src/index.ts b/server-plugins/chunter-resources/src/index.ts index 5ef6cfbbd0..3d5434a826 100644 --- a/server-plugins/chunter-resources/src/index.ts +++ b/server-plugins/chunter-resources/src/index.ts @@ -45,7 +45,7 @@ import core, { TxUpdateDoc, UserStatus } from '@hcengineering/core' -import notification, { Collaborators, DocNotifyContext, NotificationContent } from '@hcengineering/notification' +import notification, { DocNotifyContext, NotificationContent } from '@hcengineering/notification' import { getMetadata, IntlString, translate } from '@hcengineering/platform' import serverCore, { TriggerControl } from '@hcengineering/server-core' import { @@ -288,13 +288,6 @@ export async function ChunterTrigger (tx: TxCUD, control: TriggerControl): res.push( ...(await control.ctx.with('OnThreadMessageDeleted', {}, async (ctx) => await OnThreadMessageDeleted(tx, control))) ) - res.push( - ...(await control.ctx.with( - 'OnCollaboratorsChanged', - {}, - async (ctx) => await OnCollaboratorsChanged(tx as TxMixin, control) - )) - ) res.push( ...(await control.ctx.with('OnChatMessageCreated', {}, async (ctx) => await OnChatMessageCreated(tx, control))) ) @@ -381,104 +374,6 @@ function combineAttributes (attributes: any[], key: string, operator: string, ar ).filter((v) => v != null) } -async function OnChannelMembersChanged (tx: TxUpdateDoc, control: TriggerControl): Promise { - const changedAttributes = Object.entries(tx.operations) - .flatMap(([id, val]) => (['$push', '$pull'].includes(id) ? Object.keys(val) : id)) - .filter((id) => !id.startsWith('$')) - - if (!changedAttributes.includes('members')) { - return [] - } - - const added = combineAttributes([tx.operations], 'members', '$push', '$each') - const removed = combineAttributes([tx.operations], 'members', '$pull', '$in') - - const res: Tx[] = [] - const allContexts = await control.findAll(notification.class.DocNotifyContext, { objectId: tx.objectId }) - - if (removed.length > 0) { - res.push( - control.txFactory.createTxMixin(tx.objectId, tx.objectClass, tx.objectSpace, notification.mixin.Collaborators, { - $pull: { - collaborators: { $in: removed } - } - }) - ) - } - - if (added.length > 0) { - res.push( - control.txFactory.createTxMixin(tx.objectId, tx.objectClass, tx.objectSpace, notification.mixin.Collaborators, { - $push: { - collaborators: { $each: added, $position: 0 } - } - }) - ) - } - - const accounts = - added.length > 0 ? await control.modelDb.findAll(contact.class.PersonAccount, { _id: { $in: added } }) : [] - const spaces = - accounts.length > 0 - ? await control.findAll(contact.class.PersonSpace, { person: { $in: accounts.map((x) => x.person) } }) - : [] - - for (const addedMember of added) { - const context = allContexts.find(({ user }) => user === addedMember) - - if (context === undefined) { - const account = accounts.find(({ _id }) => _id === addedMember) - if (account === undefined) continue - const space = spaces.find(({ person }) => person === account.person) - if (space === undefined) continue - const createTx = control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, space._id, { - objectId: tx.objectId, - objectClass: tx.objectClass, - objectSpace: tx.objectSpace, - user: addedMember, - lastViewedTimestamp: tx.modifiedOn, - isPinned: false - }) - - await control.apply([createTx]) - } else { - const updateTx = control.txFactory.createTxUpdateDoc(context._class, context.space, context._id, { - lastViewedTimestamp: tx.modifiedOn - }) - - res.push(updateTx) - } - } - - const contextsToRemove = allContexts.filter(({ user }) => removed.includes(user)) - - for (const context of contextsToRemove) { - res.push(control.txFactory.createTxRemoveDoc(context._class, context.space, context._id)) - } - - return res -} - -async function OnCollaboratorsChanged (tx: TxMixin, control: TriggerControl): Promise { - if (tx._class !== core.class.TxMixin || tx.mixin !== notification.mixin.Collaborators) return [] - - if (!control.hierarchy.isDerived(tx.objectClass, chunter.class.Channel)) return [] - - const doc = (await control.findAll(tx.objectClass, { _id: tx.objectId }))[0] as Channel | undefined - - if (doc === undefined) return [] - if (doc.private) return [] - - const added = combineAttributes([tx.attributes], 'collaborators', '$push', '$each') - const res: Tx[] = [] - - for (const addedMember of added) { - res.push(...joinChannel(control, doc, addedMember)) - } - - return res -} - async function hideOldDirects ( directs: DocNotifyContext[], control: TriggerControl, @@ -582,7 +477,7 @@ export async function updateChatInfo (control: TriggerControl, status: UserStatu ({ objectClass }) => !hierarchy.isDerived(objectClass, chunter.class.DirectMessage) && !hierarchy.isDerived(objectClass, chunter.class.Channel) && - !hierarchy.isDerived(objectClass, chunter.class.Channel) + !hierarchy.isDerived(objectClass, activity.class.ActivityMessage) ) const directTxes = await hideOldDirects(directContexts, control, date) @@ -661,12 +556,21 @@ async function OnContextUpdate (tx: TxUpdateDoc, control: Trig return [] } +async function JoinChannelTypeMatch (originTx: Tx, _: Doc, user: Ref): Promise { + if (originTx.modifiedBy === user) return false + if (originTx._class !== core.class.TxUpdateDoc) return false + + const tx = originTx as TxUpdateDoc + const added = combineAttributes([tx.operations], 'members', '$push', '$each') + + return added.includes(user) +} + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type export default async () => ({ trigger: { ChunterTrigger, OnChatMessageRemoved, - OnChannelMembersChanged, ChatNotificationsHandler, OnUserStatus, OnContextUpdate @@ -676,6 +580,7 @@ export default async () => ({ ChannelHTMLPresenter: channelHTMLPresenter, ChannelTextPresenter: channelTextPresenter, ChunterNotificationContentProvider: getChunterNotificationContent, - ChatMessageTextPresenter + ChatMessageTextPresenter, + JoinChannelTypeMatch } }) diff --git a/server-plugins/chunter/src/index.ts b/server-plugins/chunter/src/index.ts index 4c800ff204..d32a731e4f 100644 --- a/server-plugins/chunter/src/index.ts +++ b/server-plugins/chunter/src/index.ts @@ -16,7 +16,7 @@ import type { Plugin, Resource } from '@hcengineering/platform' import { plugin } from '@hcengineering/platform' import { ObjectDDParticipantFunc, TriggerFunc } from '@hcengineering/server-core' -import { NotificationContentProvider, Presenter } from '@hcengineering/server-notification' +import { NotificationContentProvider, Presenter, TypeMatchFunc } from '@hcengineering/server-notification' /** * @public @@ -30,7 +30,6 @@ export default plugin(serverChunterId, { trigger: { ChunterTrigger: '' as Resource, OnChatMessageRemoved: '' as Resource, - OnChannelMembersChanged: '' as Resource, ChatNotificationsHandler: '' as Resource, OnUserStatus: '' as Resource, OnContextUpdate: '' as Resource @@ -40,6 +39,7 @@ export default plugin(serverChunterId, { ChannelHTMLPresenter: '' as Resource, ChannelTextPresenter: '' as Resource, ChunterNotificationContentProvider: '' as Resource, - ChatMessageTextPresenter: '' as Resource + ChatMessageTextPresenter: '' as Resource, + JoinChannelTypeMatch: '' as TypeMatchFunc } }) diff --git a/server-plugins/notification-resources/src/index.ts b/server-plugins/notification-resources/src/index.ts index e6a2b77002..eb87ef3110 100644 --- a/server-plugins/notification-resources/src/index.ts +++ b/server-plugins/notification-resources/src/index.ts @@ -87,6 +87,8 @@ import webpush, { WebPushError } from 'web-push' import { Content, NotifyParams, NotifyResult } from './types' import { + createPullCollaboratorsTx, + createPushCollaboratorsTx, getHTMLPresenter, getNotificationContent, getTextPresenter, @@ -269,13 +271,13 @@ async function getValueCollaborators (value: any, attr: AnyAttribute, control: T } async function getKeyCollaborators ( - doc: Doc, + docClass: Ref>, value: any, field: string, control: TriggerControl ): Promise[] | undefined> { if (value !== undefined && value !== null) { - const attr = control.hierarchy.findAttribute(doc._class, field) + const attr = control.hierarchy.findAttribute(docClass, field) if (attr !== undefined) { return await getValueCollaborators(value, attr, control) } @@ -297,7 +299,7 @@ export async function getDocCollaborators ( const newCollaborators = await ctx.with( 'getKeyCollaborators', {}, - async () => await getKeyCollaborators(doc, value, field, control) + async () => await getKeyCollaborators(doc._class, value, field, control) ) if (newCollaborators !== undefined) { for (const newCollaborator of newCollaborators) { @@ -321,35 +323,25 @@ export async function pushInboxNotifications ( modifiedOn: Timestamp, shouldUpdateTimestamp = true ): Promise | undefined> { - const account = receiver.account const context = contexts.find((context) => context.user === receiver._id && context.objectId === objectId) let docNotifyContextId: Ref if (context === undefined) { - const createContextTx = control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, receiver.space, { - user: receiver._id, + docNotifyContextId = await createNotifyContext( + control, objectId, objectClass, objectSpace, - isPinned: false, - lastUpdateTimestamp: shouldUpdateTimestamp ? modifiedOn : undefined - }) - await control.apply([createContextTx]) - if (receiver.account?.email !== undefined) { - control.operationContext.derived.targets['docNotifyContext' + createContextTx._id] = (it) => { - if (it._id === createContextTx._id) { - return [receiver.account?.email] - } - } - } - docNotifyContextId = createContextTx.objectId + receiver, + shouldUpdateTimestamp ? modifiedOn : undefined + ) } else { docNotifyContextId = context._id } const notificationData = { - user: account._id, + user: receiver._id, isViewed: false, docNotifyContext: docNotifyContextId, archived: false, @@ -630,7 +622,6 @@ export async function applyNotificationProviders ( sender, data._id ) - // console.log('Push takes', Date.now() - now, 'ms') if (pushTx !== undefined) { res.push(pushTx) } @@ -651,6 +642,33 @@ export async function applyNotificationProviders ( } } +async function createNotifyContext ( + control: TriggerControl, + objectId: Ref, + objectClass: Ref>, + objectSpace: Ref, + receiver: ReceiverInfo, + updateTimestamp?: Timestamp +): Promise> { + const createTx = control.txFactory.createTxCreateDoc(notification.class.DocNotifyContext, receiver.space, { + user: receiver._id, + objectId, + objectClass, + objectSpace, + isPinned: false, + lastUpdateTimestamp: updateTimestamp + }) + await control.apply([createTx]) + if (receiver.account?.email !== undefined) { + control.operationContext.derived.targets['docNotifyContext' + createTx._id] = (it) => { + if (it._id === createTx._id) { + return [receiver.account?.email] + } + } + } + return createTx.objectId +} + export async function getNotificationTxes ( control: TriggerControl, object: Doc, @@ -709,6 +727,21 @@ export async function getNotificationTxes ( sender ) } + } else { + const context = docNotifyContexts.find( + (context) => context.objectId === message.attachedTo && context.user === receiver.account._id + ) + + if (context === undefined) { + await createNotifyContext( + control, + message.attachedTo, + message.attachedToClass, + message.space, + receiver, + params.shouldUpdateTimestamp ? originTx.modifiedOn : undefined + ) + } } } return res @@ -754,6 +787,39 @@ async function updateContextsTimestamp ( await control.apply(res) } +async function removeContexts ( + contexts: DocNotifyContext[], + unsubscribe: Ref[], + control: TriggerControl +): Promise { + if (contexts.length === 0) return + if (unsubscribe.length === 0) return + + const unsubscribeAccounts = await control.modelDb.findAll(contact.class.PersonAccount, { + _id: { $in: unsubscribe } + }) + const res: Tx[] = [] + + for (const context of contexts) { + const account = unsubscribeAccounts.find(({ _id }) => _id === context.user) + if (account === undefined) continue + + const removeTx = control.txFactory.createTxRemoveDoc(context._class, context.space, context._id) + + res.push(removeTx) + + if (account.email !== undefined) { + control.operationContext.derived.targets['docNotifyContext' + removeTx._id] = (it) => { + if (it._id === removeTx._id) { + return [account.email] + } + } + } + } + + await control.apply(res) +} + export async function createCollabDocInfo ( ctx: MeasureContext, collaborators: Ref[], @@ -763,7 +829,7 @@ export async function createCollabDocInfo ( object: Doc, activityMessages: ActivityMessage[], params: NotifyParams, - cache: Map, Doc> + unsubscribe: Ref[] = [] ): Promise { let res: Tx[] = [] @@ -774,6 +840,7 @@ export async function createCollabDocInfo ( const notifyContexts = await control.findAllCtx(ctx, notification.class.DocNotifyContext, { objectId: object._id }) await updateContextsTimestamp(notifyContexts, originTx.modifiedOn, control, originTx.modifiedBy) + await removeContexts(notifyContexts, unsubscribe, control) const docMessages = activityMessages.filter((message) => message.attachedTo === object._id) if (docMessages.length === 0) { @@ -855,6 +922,50 @@ export function getMixinTx ( ) } +async function getTxCollabs ( + ctx: MeasureContext, + tx: TxCUD, + control: TriggerControl, + doc: Doc +): Promise<{ + added: Ref[] + removed: Ref[] + result: Ref[] + }> { + const { hierarchy } = control + const mixin = hierarchy.classHierarchyMixin( + doc._class, + notification.mixin.ClassCollaborators + ) + if (mixin === undefined) return { added: [], removed: [], result: [] } + + if (tx._class === core.class.TxCreateDoc) { + const collabs = await getDocCollaborators(ctx, doc, mixin, control) + return { added: collabs, removed: [], result: collabs } + } + + if (tx._class === core.class.TxRemoveDoc) { + if (hierarchy.hasMixin(doc, notification.mixin.Collaborators)) { + return { added: [], removed: [], result: hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? [] } + } + + return { added: [], removed: [], result: [] } + } + + if ([core.class.TxUpdateDoc, core.class.TxMixin].includes(tx._class)) { + const collabs = new Set(hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? []) + const ops = isMixinTx(tx) ? tx.attributes : (tx as TxUpdateDoc).operations + const newCollaborators = (await getNewCollaborators(ops, mixin, doc._class, control)).filter((p) => !collabs.has(p)) + const isSpace = control.hierarchy.isDerived(doc._class, core.class.Space) + const removedCollabs = isSpace ? await getRemovedMembers(ops, mixin, (doc as Space)._class, control) : [] + const result = [...collabs, ...newCollaborators].filter((p) => !removedCollabs.includes(p)) + + return { added: newCollaborators, removed: removedCollabs, result } + } + + return { added: [], removed: [], result: [] } +} + async function getSpaceCollabTxes ( control: TriggerControl, doc: Doc, @@ -887,8 +998,7 @@ async function getSpaceCollabTxes ( originTx, doc, activityMessages, - { isSpace: true, isOwn: false, shouldUpdateTimestamp: true }, - cache + { isSpace: true, isOwn: false, shouldUpdateTimestamp: true } ) } } @@ -931,8 +1041,7 @@ async function createCollaboratorDoc ( originTx, doc, activityMessage, - { isOwn: true, isSpace: false, shouldUpdateTimestamp: true }, - cache + { isOwn: true, isSpace: false, shouldUpdateTimestamp: true } ) ) res.push(mixinTx) @@ -1105,8 +1214,7 @@ async function collectionCollabDoc ( tx, doc, activityMessages, - { isOwn: false, isSpace: false, shouldUpdateTimestamp: true }, - cache + { isOwn: false, isSpace: false, shouldUpdateTimestamp: true } ) ) ) @@ -1168,7 +1276,7 @@ async function removeCollaboratorDoc (tx: TxRemoveDoc, control: TriggerCont async function getNewCollaborators ( ops: DocumentUpdate | MixinUpdate, mixin: ClassCollaborators, - doc: Doc, + docClass: Ref>, control: TriggerControl ): Promise[]> { const newCollaborators = new Set>() @@ -1179,7 +1287,7 @@ async function getNewCollaborators ( if (typeof value !== 'string') { value = value.$each } - const newCollabs = await getKeyCollaborators(doc, value, key, control) + const newCollabs = await getKeyCollaborators(docClass, value, key, control) if (newCollabs !== undefined) { for (const newCollab of newCollabs) { newCollaborators.add(newCollab) @@ -1192,7 +1300,7 @@ async function getNewCollaborators ( if (key.startsWith('$')) continue if (mixin.fields.includes(key)) { const value = (ops as any)[key] - const newCollabs = await getKeyCollaborators(doc, value, key, control) + const newCollabs = await getKeyCollaborators(docClass, value, key, control) if (newCollabs !== undefined) { for (const newCollab of newCollabs) { newCollaborators.add(newCollab) @@ -1203,6 +1311,30 @@ async function getNewCollaborators ( return Array.from(newCollaborators.values()) } +async function getRemovedMembers ( + ops: DocumentUpdate | MixinUpdate, + mixin: ClassCollaborators, + docClass: Ref>, + control: TriggerControl +): Promise[]> { + const removedCollaborators: Ref[] = [] + if (ops.$pull !== undefined && 'members' in ops.$pull) { + const key = 'members' + if (mixin.fields.includes(key)) { + let value = (ops.$pull as any)[key] + if (typeof value !== 'string') { + value = value.$in + } + const collabs = await getKeyCollaborators(docClass, value, key, control) + if (collabs !== undefined) { + removedCollaborators.push(...collabs) + } + } + } + + return Array.from(new Set(removedCollaborators).values()) +} + async function updateCollaboratorDoc ( ctx: MeasureContext, tx: TxUpdateDoc | TxMixin, @@ -1218,31 +1350,28 @@ async function updateCollaboratorDoc ( const doc = await ctx.with( 'find-doc', { _class: tx.objectClass }, - async () => (await control.findAllCtx(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] + async () => + cache.get(tx.objectId) ?? (await control.findAllCtx(ctx, tx.objectClass, { _id: tx.objectId }, { limit: 1 }))[0] ) if (doc === undefined) return [] + cache.set(doc._id, doc) const params: NotifyParams = { isOwn: true, isSpace: false, shouldUpdateTimestamp: true } if (hierarchy.hasMixin(doc, notification.mixin.Collaborators)) { // we should handle change field and subscribe new collaborators - const collabMixin = hierarchy.as(doc, notification.mixin.Collaborators) - const collabs = new Set(collabMixin.collaborators) - const ops = isMixinTx(tx) ? tx.attributes : tx.operations - const newCollaborators = await ctx.with('get-new-collaborators', {}, async () => - (await getNewCollaborators(ops, mixin, doc, control)).filter((p) => !collabs.has(p)) + const collabsInfo = await ctx.with( + 'get-tx-collaborators', + {}, + async () => await getTxCollabs(ctx, tx, control, doc) ) - if (newCollaborators.length > 0) { - res.push( - control.txFactory.createTxMixin(tx.objectId, tx.objectClass, tx.objectSpace, notification.mixin.Collaborators, { - $push: { - collaborators: { - $each: newCollaborators, - $position: 0 - } - } - }) - ) + if (collabsInfo.added.length > 0) { + res.push(createPushCollaboratorsTx(control, tx.objectId, tx.objectClass, tx.objectSpace, collabsInfo.added)) } + + if (collabsInfo.removed.length > 0) { + res.push(createPullCollaboratorsTx(control, tx.objectId, tx.objectClass, tx.objectSpace, collabsInfo.removed)) + } + res = res.concat( await ctx.with( 'create-collab-docinfo', @@ -1250,14 +1379,14 @@ async function updateCollaboratorDoc ( async () => await createCollabDocInfo( ctx, - [...collabMixin.collaborators, ...newCollaborators] as Ref[], + collabsInfo.result as Ref[], control, tx, originTx, doc, activityMessages, params, - cache + collabsInfo.removed as Ref[] ) ) ) @@ -1277,8 +1406,7 @@ async function updateCollaboratorDoc ( originTx, doc, activityMessages, - params, - cache + params ) ) } @@ -1409,6 +1537,59 @@ async function applyUserTxes ( return res } +async function updateCollaborators (ctx: MeasureContext, control: TriggerControl, tx: TxCUD): Promise { + if (tx._class !== core.class.TxUpdateDoc && tx._class !== core.class.TxMixin) return [] + + const hierarchy = control.hierarchy + + const mixin = hierarchy.classHierarchyMixin(tx.objectClass, notification.mixin.ClassCollaborators) + if (mixin === undefined) return [] + + const { objectClass, objectId, objectSpace } = tx + const ops = isMixinTx(tx) ? tx.attributes : (tx as TxUpdateDoc).operations + const addedCollaborators = await getNewCollaborators(ops, mixin, objectClass, control) + const isSpace = control.hierarchy.isDerived(objectClass, core.class.Space) + const removedCollaborators = isSpace + ? await getRemovedMembers(ops, mixin, objectClass as Ref>, control) + : [] + + if (removedCollaborators.length === 0 && addedCollaborators.length === 0) return [] + + const doc = (await control.findAll(objectClass, { _id: objectId }, { limit: 1 }))[0] + if (doc === undefined) return [] + + const res: Tx[] = [] + const currentCollaborators = new Set(hierarchy.as(doc, notification.mixin.Collaborators).collaborators ?? []) + const toAdd = addedCollaborators.filter((p) => !currentCollaborators.has(p)) + + if (toAdd.length === 0 && removedCollaborators.length === 0) return [] + + if (toAdd.length > 0) { + res.push(createPushCollaboratorsTx(control, objectId, objectClass, objectSpace, toAdd)) + } + + if (removedCollaborators.length > 0) { + res.push(createPullCollaboratorsTx(control, objectId, objectClass, objectSpace, removedCollaborators)) + } + + if (hierarchy.classHierarchyMixin(objectClass, activity.mixin.ActivityDoc) === undefined) return res + + const contexts = await control.findAll(notification.class.DocNotifyContext, { attachedTo: objectId }) + const addedInfo = await getUsersInfo(ctx, toAdd as Ref[], control) + + for (const addedUser of addedInfo) { + const info = toReceiverInfo(hierarchy, addedUser) + if (info === undefined) continue + const context = contexts.find(({ user }) => user === info._id) + if (context !== undefined) continue + await createNotifyContext(control, objectId, objectClass, objectSpace, info) + } + + await removeContexts(contexts, removedCollaborators as Ref[], control) + + return res +} + export async function createCollaboratorNotifications ( ctx: MeasureContext, tx: TxCUD, @@ -1418,7 +1599,12 @@ export async function createCollaboratorNotifications ( cache: Map, Doc> = new Map, Doc>() ): Promise { if (tx.space === core.space.DerivedTx) { - return [] + // do not forgot update collaborators for derived tx + return await ctx.with( + 'updateDerivedCollaborators', + {}, + async () => await updateCollaborators(ctx, control, TxProcessor.extractTx(tx) as TxCUD) + ) } if (activityMessages.length === 0) { diff --git a/server-plugins/notification-resources/src/utils.ts b/server-plugins/notification-resources/src/utils.ts index d34f1ac3fa..a973aac609 100644 --- a/server-plugins/notification-resources/src/utils.ts +++ b/server-plugins/notification-resources/src/utils.ts @@ -12,6 +12,15 @@ // See the License for the specific language governing permissions and // limitations under the License. // +import notification, { + BaseNotificationType, + Collaborators, + CommonNotificationType, + NotificationContent, + NotificationProvider, + NotificationType +} from '@hcengineering/notification' +import type { TriggerControl } from '@hcengineering/server-core' import { Analytics } from '@hcengineering/analytics' import contact, { formatName, PersonAccount } from '@hcengineering/contact' import core, { @@ -23,6 +32,7 @@ import core, { matchQuery, MixinUpdate, Ref, + Space, toIdMap, Tx, TxCreateDoc, @@ -33,15 +43,7 @@ import core, { TxUpdateDoc, type MeasureContext } from '@hcengineering/core' -import notification, { - BaseNotificationType, - CommonNotificationType, - NotificationContent, - NotificationProvider, - NotificationType -} from '@hcengineering/notification' import { getResource, IntlString, translate } from '@hcengineering/platform' -import type { TriggerControl } from '@hcengineering/server-core' import serverNotification, { getPersonAccountById, HTMLPresenter, @@ -158,7 +160,9 @@ export async function isAllowed ( return false } - const typesSettings = await control.queryFind(notification.class.NotificationTypeSetting, {}) + const typesSettings = await control.queryFind(notification.class.NotificationTypeSetting, { + space: core.space.Workspace + }) const setting = typesSettings.find( (it) => it.attachedTo === provider._id && it.type === type._id && it.modifiedBy === receiver ) @@ -475,6 +479,7 @@ export async function getUsersInfo ( ids: Ref[], control: TriggerControl ): Promise<(ReceiverInfo | SenderInfo)[]> { + if (ids.length === 0) return [] const accounts = await control.modelDb.findAll(contact.class.PersonAccount, { _id: { $in: ids } }) const personIds = accounts.map((it) => it.person) const accountById = toIdMap(accounts) @@ -522,3 +527,32 @@ export function toReceiverInfo (hierarchy: Hierarchy, info?: SenderInfo | Receiv space: info.space } } + +export function createPushCollaboratorsTx ( + control: TriggerControl, + objectId: Ref, + objectClass: Ref>, + space: Ref, + collaborators: Ref[] +): TxMixin { + return control.txFactory.createTxMixin(objectId, objectClass, space, notification.mixin.Collaborators, { + $push: { + collaborators: { + $each: collaborators, + $position: 0 + } + } + }) +} + +export function createPullCollaboratorsTx ( + control: TriggerControl, + objectId: Ref, + objectClass: Ref>, + space: Ref, + collaborators: Ref[] +): TxMixin { + return control.txFactory.createTxMixin(objectId, objectClass, space, notification.mixin.Collaborators, { + $pull: { collaborators: { $in: collaborators } } + }) +} diff --git a/tests/sanity/tests/chat/chat.spec.ts b/tests/sanity/tests/chat/chat.spec.ts index 19eb25affd..c2237960a5 100644 --- a/tests/sanity/tests/chat/chat.spec.ts +++ b/tests/sanity/tests/chat/chat.spec.ts @@ -158,10 +158,11 @@ test.describe('channel tests', () => { await channelPageSecond.checkIfChannelTableExist(data.channelName, true) await channelPageSecond.clickJoinChannelButton() await channelPageSecond.clickChooseChannel(data.channelName) + const checkJoinButton = await page2.locator('button[data-id="btnJoin"]').isVisible({ timeout: 1500 }) + if (checkJoinButton) await page2.locator('button[data-id="btnJoin"]').click() await channelPageSecond.checkMessageExist('Test message', true, 'Test message') await channelPageSecond.sendMessage('My dream is to fly') await channelPageSecond.checkMessageExist('My dream is to fly', true, 'My dream is to fly') - await channelPage.clickOnClosePopupButton() await channelPage.checkMessageExist('My dream is to fly', true, 'My dream is to fly') await page2.close() }) diff --git a/tests/sanity/tests/model/channel-page.ts b/tests/sanity/tests/model/channel-page.ts index 9991085cc1..d4c898ea3d 100644 --- a/tests/sanity/tests/model/channel-page.ts +++ b/tests/sanity/tests/model/channel-page.ts @@ -30,20 +30,20 @@ export class ChannelPage extends CommonPage { readonly addMemberToChannelButton = (userName: string): Locator => this.page.getByText(userName) readonly joinChannelButton = (): Locator => this.page.getByRole('button', { name: 'Join' }) readonly addEmojiButton = (): Locator => - this.page.locator('.activityMessage-actionPopup > button[data-id$="AddReactionAction"]') + this.page.locator('.activityMessage-actionPopup > button[data-id$="AddReactionAction"]').last() readonly selectEmoji = (emoji: string): Locator => this.page.getByText(emoji) readonly saveMessageButton = (): Locator => - this.page.locator('.activityMessage-actionPopup > button[data-id$="SaveForLaterAction"]') + this.page.locator('.activityMessage-actionPopup > button[data-id$="SaveForLaterAction"]').last() readonly pinMessageButton = (): Locator => - this.page.locator('.activityMessage-actionPopup > button[data-id$="PinMessageAction"]') + this.page.locator('.activityMessage-actionPopup > button[data-id$="PinMessageAction"]').last() readonly replyButton = (): Locator => - this.page.locator('.activityMessage-actionPopup > button[data-id$="ReplyToThreadAction"]') + this.page.locator('.activityMessage-actionPopup > button[data-id$="ReplyToThreadAction"]').last() readonly openMoreButton = (): Locator => - this.page.locator('.activityMessage-actionPopup > button[data-id="btnMoreActions"]') + this.page.locator('.activityMessage-actionPopup > button[data-id="btnMoreActions"]').last() readonly messageSaveMarker = (): Locator => this.page.locator('.saveMarker') readonly saveMessageTab = (): Locator => this.page.getByRole('button', { name: 'Saved' }) @@ -66,7 +66,7 @@ export class ChannelPage extends CommonPage { readonly privateOrPublicPopupButton = (change: string): Locator => this.page.locator('div.popup div.menu-item', { hasText: change }) - readonly userAdded = (user: string): Locator => this.page.getByText(user) + readonly userAdded = (user: string): Locator => this.page.locator('.members').getByText(user) private readonly addMemberPreview = (): Locator => this.page.getByRole('button', { name: 'Add members' }) private readonly addButtonPreview = (): Locator => this.page.getByRole('button', { name: 'Add', exact: true }) From b6bb2b7a0c1d074c044216c100a97df771671c1a Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Tue, 6 Aug 2024 23:10:00 +0700 Subject: [PATCH 17/20] Fix docke target to include ai-bot Signed-off-by: Andrey Sobolev --- common/config/rush/command-line.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/config/rush/command-line.json b/common/config/rush/command-line.json index 11fc7155cc..ecd0a088ac 100644 --- a/common/config/rush/command-line.json +++ b/common/config/rush/command-line.json @@ -238,7 +238,7 @@ "summary": "Build docker with platform", "description": "use to build all docker containers required for platform", "safeForSimultaneousRushProcesses": true, - "shellCommand": "rush docker:build -p 20 --to @hcengineering/pod-server --to @hcengineering/pod-front --to @hcengineering/prod --to @hcengineering/pod-account --to @hcengineering/pod-collaborator --to @hcengineering/tool --to @hcengineering/pod-print --to @hcengineering/pod-sign --to @hcengineering/pod-analytics-collector --to @hcengineering/rekoni-service" + "shellCommand": "rush docker:build -p 20 --to @hcengineering/pod-server --to @hcengineering/pod-front --to @hcengineering/prod --to @hcengineering/pod-account --to @hcengineering/pod-collaborator --to @hcengineering/tool --to @hcengineering/pod-print --to @hcengineering/pod-sign --to @hcengineering/pod-analytics-collector --to @hcengineering/rekoni-service --to @hcengineering/pod-ai-bot" }, { "commandKind": "global", From ce9a30a3ff470ebcbd0395ef03cf64e0a4750ebc Mon Sep 17 00:00:00 2001 From: Alexey Zinoviev Date: Tue, 6 Aug 2024 20:37:01 +0400 Subject: [PATCH 18/20] uberf-7765: only retry network errors when loading config for desktop app (#6274) Signed-off-by: Alexey Zinoviev --- desktop/src/ui/preload.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/desktop/src/ui/preload.ts b/desktop/src/ui/preload.ts index 1daeb28ac2..4502e52ec1 100644 --- a/desktop/src/ui/preload.ts +++ b/desktop/src/ui/preload.ts @@ -19,21 +19,27 @@ export function concatLink (host: string, path: string): string { async function loadServerConfig (url: string): Promise { let retries = 5 - let error: any + let res: Response | undefined do { try { - return await (await fetch(url)).json() + res = await fetch(url) + break } catch (e) { - error = e retries-- - if (retries > 0) { - await new Promise((resolve) => setTimeout(resolve, 1000 * (5 - retries))) + if (retries === 0) { + throw new Error(`Failed to load server config: ${e}`) } + await new Promise((resolve) => setTimeout(resolve, 1000 * (5 - retries))) } } while (retries > 0) - throw new Error(`Failed to load server config: ${error}`) + if (res === undefined) { + // In theory should never get here + throw new Error('Failed to load server config') + } + + return await res.json() } const openArg = (process.argv.find((it) => it.startsWith('--open=')) ?? '').split('--open=')[1] From 954a3bb9e75909fc423152993ffd18e15299abbc Mon Sep 17 00:00:00 2001 From: Denis Bykhov Date: Wed, 7 Aug 2024 11:40:50 +0500 Subject: [PATCH 19/20] Fix love connection (#6275) Signed-off-by: Denis Bykhov --- models/love/src/index.ts | 2 ++ packages/core/src/memdb.ts | 13 +++++++++---- packages/presentation/src/plugin.ts | 3 ++- plugins/client-resources/src/connection.ts | 2 +- plugins/guest-resources/src/connect.ts | 5 ++++- .../love-resources/src/components/ControlExt.svelte | 1 - plugins/love-resources/src/components/Room.svelte | 7 ++++++- plugins/love-resources/src/utils.ts | 11 +++++++---- plugins/love/src/index.ts | 1 + plugins/workbench-resources/src/connect.ts | 3 +++ server-plugins/love-resources/src/index.ts | 5 +++-- 11 files changed, 38 insertions(+), 15 deletions(-) diff --git a/models/love/src/index.ts b/models/love/src/index.ts index b71ee1ef3e..e92ef5a5a9 100644 --- a/models/love/src/index.ts +++ b/models/love/src/index.ts @@ -87,6 +87,8 @@ export class TParticipantInfo extends TDoc implements ParticipantInfo { x!: number y!: number + + sessionId!: string | null } @Model(love.class.JoinRequest, core.class.Doc, DOMAIN_TRANSIENT) diff --git a/packages/core/src/memdb.ts b/packages/core/src/memdb.ts index 5fe5dc5200..42cbfb9c87 100644 --- a/packages/core/src/memdb.ts +++ b/packages/core/src/memdb.ts @@ -337,13 +337,18 @@ export class ModelDb extends MemDb { } protected async txUpdateDoc (tx: TxUpdateDoc): Promise { - const doc = this.getObject(tx.objectId) as any - TxProcessor.updateDoc2Doc(doc, tx) - return tx.retrieve === true ? { object: doc } : {} + try { + const doc = this.getObject(tx.objectId) as any + TxProcessor.updateDoc2Doc(doc, tx) + return tx.retrieve === true ? { object: doc } : {} + } catch (err: any) {} + return {} } protected async txRemoveDoc (tx: TxRemoveDoc): Promise { - this.delDoc(tx.objectId) + try { + this.delDoc(tx.objectId) + } catch (err: any) {} return {} } diff --git a/packages/presentation/src/plugin.ts b/packages/presentation/src/plugin.ts index 05efb3e116..93adbf2540 100644 --- a/packages/presentation/src/plugin.ts +++ b/packages/presentation/src/plugin.ts @@ -136,7 +136,8 @@ export default plugin(presentationId, { Endpoint: '' as Metadata, FrontUrl: '' as Asset, PreviewConfig: '' as Metadata, - ClientHook: '' as Metadata + ClientHook: '' as Metadata, + SessionId: '' as Metadata }, status: { FileTooLarge: '' as StatusCode diff --git a/plugins/client-resources/src/connection.ts b/plugins/client-resources/src/connection.ts index 249496e044..138ad75cc3 100644 --- a/plugins/client-resources/src/connection.ts +++ b/plugins/client-resources/src/connection.ts @@ -261,7 +261,7 @@ class Connection implements ClientConnection { void this.opt?.onConnect?.( (resp as HelloResponse).reconnect === true ? ClientConnectEvent.Reconnected : ClientConnectEvent.Connected, - null + this.sessionId ) this.schedulePing(socketId) return diff --git a/plugins/guest-resources/src/connect.ts b/plugins/guest-resources/src/connect.ts index c9ef545952..69901c048e 100644 --- a/plugins/guest-resources/src/connect.ts +++ b/plugins/guest-resources/src/connect.ts @@ -72,9 +72,12 @@ export async function connect (title: string): Promise { }) }, // We need to refresh all active live queries and clear old queries. - onConnect: (event: ClientConnectEvent) => { + onConnect: (event: ClientConnectEvent, data: any) => { console.log('WorkbenchClient: onConnect', event) try { + if (event === ClientConnectEvent.Connected) { + setMetadata(presentation.metadata.SessionId, data) + } if ((_clientSet && event === ClientConnectEvent.Connected) || event === ClientConnectEvent.Refresh) { void refreshClient(true) } diff --git a/plugins/love-resources/src/components/ControlExt.svelte b/plugins/love-resources/src/components/ControlExt.svelte index a08fce5cfe..5cf23602a9 100644 --- a/plugins/love-resources/src/components/ControlExt.svelte +++ b/plugins/love-resources/src/components/ControlExt.svelte @@ -57,7 +57,6 @@ rooms } from '../stores' import { - connectRoom, disconnect, getRoomName, isCameraEnabled, diff --git a/plugins/love-resources/src/components/Room.svelte b/plugins/love-resources/src/components/Room.svelte index e06d5d4725..3f0bb1e38a 100644 --- a/plugins/love-resources/src/components/Room.svelte +++ b/plugins/love-resources/src/components/Room.svelte @@ -43,6 +43,7 @@ } from '../utils' import ControlBar from './ControlBar.svelte' import ParticipantView from './ParticipantView.svelte' + import presentation from '@hcengineering/presentation' export let withVideo: boolean export let room: TypeRoom @@ -223,7 +224,11 @@ await $storePromise - if (!$isConnected && !$isCurrentInstanceConnected) { + if ( + !$isConnected && + !$isCurrentInstanceConnected && + $myInfo?.sessionId === getMetadata(presentation.metadata.SessionId) + ) { const info = $infos.filter((p) => p.room === room._id) await tryConnect($personByIdStore, $myInfo, room, info, $myRequests, $invites) } diff --git a/plugins/love-resources/src/utils.ts b/plugins/love-resources/src/utils.ts index 2c3d7ff42f..0fa2f60d4b 100644 --- a/plugins/love-resources/src/utils.ts +++ b/plugins/love-resources/src/utils.ts @@ -485,14 +485,16 @@ async function moveToRoom ( y: number, currentInfo: ParticipantInfo | undefined, currentPerson: Person, - room: Room + room: Room, + sessionId: string | null ): Promise { const client = getClient() if (currentInfo !== undefined) { await client.diffUpdate(currentInfo, { x, y, - room: room._id + room: room._id, + sessionId }) } else { await client.createDoc(love.class.ParticipantInfo, core.space.Workspace, { @@ -500,7 +502,8 @@ async function moveToRoom ( y, room: room._id, person: currentPerson._id, - name: currentPerson.name + name: currentPerson.name, + sessionId }) } const loc = getCurrentLocation() @@ -529,7 +532,7 @@ export async function connectRoom ( room: Room ): Promise { await disconnect() - await moveToRoom(x, y, currentInfo, currentPerson, room) + await moveToRoom(x, y, currentInfo, currentPerson, room, getMetadata(presentation.metadata.SessionId) ?? null) await connectLK(currentPerson, room) } diff --git a/plugins/love/src/index.ts b/plugins/love/src/index.ts index 95816ec231..62eada1e03 100644 --- a/plugins/love/src/index.ts +++ b/plugins/love/src/index.ts @@ -51,6 +51,7 @@ export interface ParticipantInfo extends Doc { room: Ref x: number y: number + sessionId: string | null } export interface RoomInfo extends Doc { diff --git a/plugins/workbench-resources/src/connect.ts b/plugins/workbench-resources/src/connect.ts index 62faafa29a..df9a1e25ac 100644 --- a/plugins/workbench-resources/src/connect.ts +++ b/plugins/workbench-resources/src/connect.ts @@ -168,6 +168,9 @@ export async function connect (title: string): Promise { return } try { + if (event === ClientConnectEvent.Connected) { + setMetadata(presentation.metadata.SessionId, data) + } if ((_clientSet && event === ClientConnectEvent.Connected) || event === ClientConnectEvent.Refresh) { void ctx.with('refresh client', {}, async () => { await refreshClient(tokenChanged) diff --git a/server-plugins/love-resources/src/index.ts b/server-plugins/love-resources/src/index.ts index 4a9d1483d5..4f973b29b1 100644 --- a/server-plugins/love-resources/src/index.ts +++ b/server-plugins/love-resources/src/index.ts @@ -84,7 +84,8 @@ async function createUserInfo (acc: Ref, control: TriggerControl): Prom name: person !== undefined ? getName(control.hierarchy, person, control.branding?.lastNameFirst) : account.email, room: room?._id ?? love.ids.Reception, x: 0, - y: 0 + y: 0, + sessionId: null }) const ptx = control.txFactory.createTxApplyIf( core.space.Workspace, @@ -93,7 +94,7 @@ async function createUserInfo (acc: Ref, control: TriggerControl): Prom [ { _class: love.class.ParticipantInfo, - query: { person } + query: { person: personId } } ], [tx], From c5772821926ad4b71251169e6c9d4e31bcec474f Mon Sep 17 00:00:00 2001 From: Alexander Platov Date: Wed, 7 Aug 2024 09:42:49 +0300 Subject: [PATCH 20/20] Added and fixed UI tests (#6276) Signed-off-by: Alexander Platov --- .../navigator/DocTreeElement.svelte | 10 +- tests/sanity/tests/chat/chat.spec.ts | 4 +- .../sanity/tests/collaborative/issues.spec.ts | 8 +- .../sanity/tests/documents/documents.spec.ts | 19 +++ tests/sanity/tests/inbox/inbox.spec.ts | 117 +++++++++--------- tests/sanity/tests/model/common-page.ts | 8 +- .../model/documents/document-content-page.ts | 6 + .../tests/model/documents/documents-page.ts | 11 +- .../sanity/tests/model/inbox.ts/inbox-page.ts | 6 +- tests/sanity/tests/model/team-page.ts | 3 + .../model/tracker/issues-details-page.ts | 5 +- .../sanity/tests/model/tracker/issues-page.ts | 2 +- .../model/tracker/template-details-page.ts | 3 +- tests/sanity/tests/planning/plan.spec.ts | 9 +- tests/sanity/tests/tracker/common-steps.ts | 5 +- tests/sanity/tests/tracker/issues.spec.ts | 31 +---- .../tests/tracker/related-issues.spec.ts | 2 - tests/sanity/tests/tracker/subissues.spec.ts | 2 - tests/sanity/tests/tracker/template.spec.ts | 28 +---- tests/sanity/tests/tracker/tracker.spec.ts | 2 +- tests/sanity/tests/tracker/tracker.utils.ts | 18 ++- tests/sanity/tests/utils.ts | 8 ++ tests/sanity/tests/workspace/create.spec.ts | 6 +- 23 files changed, 171 insertions(+), 142 deletions(-) diff --git a/plugins/document-resources/src/components/navigator/DocTreeElement.svelte b/plugins/document-resources/src/components/navigator/DocTreeElement.svelte index db0794da4c..43a6a0fba4 100644 --- a/plugins/document-resources/src/components/navigator/DocTreeElement.svelte +++ b/plugins/document-resources/src/components/navigator/DocTreeElement.svelte @@ -75,12 +75,20 @@ icon={action.icon} kind={'tertiary'} size={'extra-small'} + dataId={action.label} tooltip={{ label: action.label, direction: 'top' }} on:click={(evt) => action.action({}, evt)} /> {/if} {/each} - + diff --git a/tests/sanity/tests/chat/chat.spec.ts b/tests/sanity/tests/chat/chat.spec.ts index c2237960a5..44af888e6f 100644 --- a/tests/sanity/tests/chat/chat.spec.ts +++ b/tests/sanity/tests/chat/chat.spec.ts @@ -416,6 +416,7 @@ test.describe('channel tests', () => { }) test('Checking backlinks in the Chat', async ({ browser, page }) => { + await api.createAccount(newUser2.email, newUser2.password, newUser2.firstName, newUser2.lastName) await leftSideMenuPage.openProfileMenu() await leftSideMenuPage.inviteToWorkspace() await leftSideMenuPage.getInviteLink() @@ -424,10 +425,10 @@ test.describe('channel tests', () => { const page2 = await browser.newPage() const leftSideMenuPageSecond = new LeftSideMenuPage(page2) const channelPageSecond = new ChannelPage(page2) - await api.createAccount(newUser2.email, newUser2.password, newUser2.firstName, newUser2.lastName) await page2.goto(linkText ?? '') const joinPage = new SignInJoinPage(page2) await joinPage.join(newUser2) + await leftSideMenuPageSecond.clickChunter() await leftSideMenuPage.clickChunter() await channelPage.clickChannel('general') @@ -435,7 +436,6 @@ test.describe('channel tests', () => { await channelPage.sendMention(mentionName) await channelPage.checkMessageExist(`@${mentionName}`, true, `@${mentionName}`) - await leftSideMenuPageSecond.clickChunter() await channelPageSecond.clickChannel('general') await channelPageSecond.checkMessageExist(`@${mentionName}`, true, `@${mentionName}`) await page2.close() diff --git a/tests/sanity/tests/collaborative/issues.spec.ts b/tests/sanity/tests/collaborative/issues.spec.ts index 8ed4cb8897..7b7827c241 100644 --- a/tests/sanity/tests/collaborative/issues.spec.ts +++ b/tests/sanity/tests/collaborative/issues.spec.ts @@ -69,11 +69,7 @@ test.describe('Collaborative test for issue', () => { await issuesPageSecond.openIssueByName(newIssue.title) const issuesDetailsPageSecond = new IssuesDetailsPage(userSecondPage) - await issuesDetailsPageSecond.checkIssue({ - ...newIssue, - milestone: 'Milestone', - estimation: '2h' - }) + await issuesDetailsPageSecond.checkIssue(newIssue) await closePages() }) @@ -176,7 +172,7 @@ test.describe('Collaborative test for issue', () => { await issuesPageSecond.openIssueByName(issue.title) const issuesDetailsPageSecond = new IssuesDetailsPage(userSecondPage) - await issuesDetailsPageSecond.checkIssue({ ...issue }) + await issuesDetailsPageSecond.checkIssue(issue) }) await closePages() diff --git a/tests/sanity/tests/documents/documents.spec.ts b/tests/sanity/tests/documents/documents.spec.ts index c2b2099575..50b8acdf5d 100644 --- a/tests/sanity/tests/documents/documents.spec.ts +++ b/tests/sanity/tests/documents/documents.spec.ts @@ -150,4 +150,23 @@ test.describe('Documents tests', () => { await documentContentPage.addLinkToText(contentLink, 'test/link/123456') await documentContentPage.checkLinkInTheText(contentLink, 'test/link/123456') }) + + test('Locked document and checking URL', async ({ page, context }) => { + const newDocument: NewDocument = { + title: `New Document-${generateId()}`, + space: 'Default' + } + + await leftSideMenuPage.clickDocuments() + await documentsPage.clickOnButtonCreateDocument() + await documentsPage.createDocument(newDocument) + await documentsPage.selectMoreActionOfDocument(newDocument.title, 'Lock') + await documentsPage.selectMoreActionOfDocument(newDocument.title, 'Copy document URL to clipboard') + await context.grantPermissions(['clipboard-read']) + const handle = await page.evaluateHandle(() => navigator.clipboard.readText()) + const clipboardContent = await handle.jsonValue() + await page.goto(`${clipboardContent}`) + await documentContentPage.checkDocumentTitle(newDocument.title) + await documentContentPage.checkDocumentLocked() + }) }) diff --git a/tests/sanity/tests/inbox/inbox.spec.ts b/tests/sanity/tests/inbox/inbox.spec.ts index d20d2d4fea..5c8aa99a16 100644 --- a/tests/sanity/tests/inbox/inbox.spec.ts +++ b/tests/sanity/tests/inbox/inbox.spec.ts @@ -1,5 +1,5 @@ import { test } from '@playwright/test' -import { PlatformURI, generateTestData } from '../utils' +import { PlatformURI, generateTestData, getTimeForPlanner, attachScreenshot } from '../utils' import { LeftSideMenuPage } from '../model/left-side-menu-page' import { ApiEndpoint } from '../API/Api' import { LoginPage } from '../model/login-page' @@ -13,6 +13,8 @@ import { ChannelPage } from '../model/channel-page' import { UserProfilePage } from '../model/profile/user-profile-page' import { MenuItems, NotificationsPage } from '../model/profile/notifications-page' import { SelectWorkspacePage } from '../model/select-workspace-page' +import { PlanningPage } from '../model/planning/planning-page' +import { TeamPage } from '../model/team-page' test.describe('Inbox tests', () => { let leftSideMenuPage: LeftSideMenuPage @@ -49,11 +51,7 @@ test.describe('Inbox tests', () => { test('User is able to create a task, assign a himself and see it inside the inbox', async ({ page }) => { const newIssue = createNewIssueData(data.firstName, data.lastName) await prepareNewIssueWithOpenStep(page, newIssue) - await issuesDetailsPage.checkIssue({ - ...newIssue, - milestone: 'Milestone', - estimation: '2h' - }) + await issuesDetailsPage.checkIssue(newIssue) await leftSideMenuPage.clickTracker() await leftSideMenuPage.clickNotification() @@ -63,44 +61,28 @@ test.describe('Inbox tests', () => { test('User is able to create a task, assign a himself and open it from inbox', async ({ page }) => { const newIssue = createNewIssueData(data.firstName, data.lastName) await prepareNewIssueWithOpenStep(page, newIssue) - await issuesDetailsPage.checkIssue({ - ...newIssue, - milestone: 'Milestone', - estimation: '2h' - }) + await issuesDetailsPage.checkIssue(newIssue) await leftSideMenuPage.clickTracker() await leftSideMenuPage.clickNotification() await inboxPage.checkIfTaskIsPresentInInbox(newIssue.title) await inboxPage.clickOnToDo(newIssue.title) await inboxPage.clickLeftSidePanelOpen() - await issuesDetailsPage.checkIssue({ - ...newIssue, - milestone: 'Milestone', - estimation: '2h' - }) + await issuesDetailsPage.checkIssue(newIssue) }) test.skip('User is able to create a task, assign a himself and close it from inbox', async ({ page }) => { const newIssue = createNewIssueData(data.firstName, data.lastName) await prepareNewIssueWithOpenStep(page, newIssue) - await issuesDetailsPage.checkIssue({ - ...newIssue, - milestone: 'Milestone', - estimation: '2h' - }) + await issuesDetailsPage.checkIssue(newIssue) await leftSideMenuPage.clickTracker() await leftSideMenuPage.clickNotification() await inboxPage.checkIfTaskIsPresentInInbox(newIssue.title) await inboxPage.clickOnToDo(newIssue.title) await inboxPage.clickLeftSidePanelOpen() - await issuesDetailsPage.checkIssue({ - ...newIssue, - milestone: 'Milestone', - estimation: '2h' - }) + await issuesDetailsPage.checkIssue(newIssue) await inboxPage.clickCloseLeftSidePanel() // ADD ASSERT ONCE THE ISSUE IS FIXED }) @@ -120,11 +102,7 @@ test.describe('Inbox tests', () => { const newIssue = createNewIssueData(newUser2.firstName, newUser2.lastName) await prepareNewIssueWithOpenStep(page, newIssue) - await issuesDetailsPage.checkIssue({ - ...newIssue, - milestone: 'Milestone', - estimation: '2h' - }) + await issuesDetailsPage.checkIssue(newIssue) await leftSideMenuPageSecond.clickTracker() await leftSideMenuPageSecond.clickNotification() await inboxPageSecond.checkIfTaskIsPresentInInbox(newIssue.title) @@ -147,21 +125,13 @@ test.describe('Inbox tests', () => { const newIssue = createNewIssueData(newUser2.firstName, newUser2.lastName) await prepareNewIssueWithOpenStep(page, newIssue) - await issuesDetailsPage.checkIssue({ - ...newIssue, - milestone: 'Milestone', - estimation: '2h' - }) + await issuesDetailsPage.checkIssue(newIssue) await leftSideMenuPageSecond.clickTracker() await leftSideMenuPageSecond.clickNotification() await inboxPageSecond.checkIfTaskIsPresentInInbox(newIssue.title) await inboxPageSecond.clickOnToDo(newIssue.title) await inboxPageSecond.clickLeftSidePanelOpen() - await issuesDetailsPageSecond.checkIssue({ - ...newIssue, - milestone: 'Milestone', - estimation: '2h' - }) + await issuesDetailsPageSecond.checkIssue(newIssue) await page2.close() }) test.skip('User is able to create a task, assign a other user and close it from inbox', async ({ page, browser }) => { @@ -180,21 +150,13 @@ test.describe('Inbox tests', () => { const newIssue = createNewIssueData(newUser2.firstName, newUser2.lastName) await prepareNewIssueWithOpenStep(page, newIssue) - await issuesDetailsPage.checkIssue({ - ...newIssue, - milestone: 'Milestone', - estimation: '2h' - }) + await issuesDetailsPage.checkIssue(newIssue) await leftSideMenuPageSecond.clickTracker() await leftSideMenuPageSecond.clickNotification() await inboxPageSecond.checkIfTaskIsPresentInInbox(newIssue.title) await inboxPageSecond.clickOnToDo(newIssue.title) await inboxPageSecond.clickLeftSidePanelOpen() - await issuesDetailsPageSecond.checkIssue({ - ...newIssue, - milestone: 'Milestone', - estimation: '2h' - }) + await issuesDetailsPageSecond.checkIssue(newIssue) await inboxPage.clickCloseLeftSidePanel() // ADD ASSERT ONCE THE ISSUE IS FIXED await page2.close() @@ -285,11 +247,7 @@ test.describe('Inbox tests', () => { const newIssue = createNewIssueData(newUser2.firstName, newUser2.lastName) await prepareNewIssueWithOpenStep(page, newIssue) - await issuesDetailsPage.checkIssue({ - ...newIssue, - milestone: 'Milestone', - estimation: '2h' - }) + await issuesDetailsPage.checkIssue(newIssue) await leftSideMenuPageSecond.clickTracker() await leftSideMenuPageSecond.clickNotification() await inboxPageSecond.clickOnInboxFilter('Channels') @@ -300,4 +258,51 @@ test.describe('Inbox tests', () => { await inboxPageSecond.checkIfInboxChatExists('Channel general', false) await page2.close() }) + + test('Checking the ability to receive a task and schedule it', async ({ page, browser }) => { + await leftSideMenuPage.openProfileMenu() + await leftSideMenuPage.inviteToWorkspace() + await leftSideMenuPage.getInviteLink() + const linkText = await page.locator('.antiPopup .link').textContent() + await leftSideMenuPage.clickOnCloseInvite() + + const page2 = await browser.newPage() + await page2.goto(linkText ?? '') + const joinPage = new SignInJoinPage(page2) + await joinPage.join(newUser2) + + const newIssue = createNewIssueData(data.firstName, data.lastName, { + status: 'Todo', + assignee: `${newUser2.lastName} ${newUser2.firstName}`, + estimation: '0' + }) + await prepareNewIssueWithOpenStep(page, newIssue) + await issuesDetailsPage.checkIssue(newIssue) + + const leftSideMenuPageSecond = new LeftSideMenuPage(page2) + const inboxPageSecond = new InboxPage(page2) + const issuesDetailsPageSecond = new IssuesDetailsPage(page2) + const planningPageSecond = new PlanningPage(page2) + await leftSideMenuPageSecond.clickNotification() + await inboxPageSecond.checkIfIssueIsPresentInInbox(newIssue.title) + await inboxPageSecond.clickIssuePresentInInbox(newIssue.title) + await inboxPageSecond.clickLeftSidePanelOpen() + await issuesDetailsPageSecond.checkIssue(newIssue) + await leftSideMenuPageSecond.clickPlanner() + await planningPageSecond.closeNotification() + await planningPageSecond.dragdropTomorrow(newIssue.title, getTimeForPlanner()) + await planningPageSecond.eventInSchedule(newIssue.title).isVisible() + await attachScreenshot('Recive_task_and_scheduled.png', page2) + + await attachScreenshot('Recive_task_and_scheduled-Detail.png', page) + await issuesDetailsPage.checkIssue({ ...newIssue, status: 'In Progress' }) + await leftSideMenuPage.clickTeam() + const teamPage = new TeamPage(page) + await teamPage.checkTeamPageIsOpened() + await teamPage.selectTeam('Default') + await teamPage.buttonNextDay().click() + await attachScreenshot('Recive_task_and_scheduled-Tomorrow.png', page) + await teamPage.getItemByText('Tomorrow', newIssue.title).isVisible() + await page2.close() + }) }) diff --git a/tests/sanity/tests/model/common-page.ts b/tests/sanity/tests/model/common-page.ts index cd94426f64..0f6083b9b0 100644 --- a/tests/sanity/tests/model/common-page.ts +++ b/tests/sanity/tests/model/common-page.ts @@ -31,7 +31,7 @@ export class CommonPage { tagsStringAddTagButtonSubmit = (): Locator => this.page.locator('div.popup form[id="tags:string:AddTag"] button[type="submit"]') - notifyContainerButton = (): Locator => this.page.locator('div.notify-container button[type="button"].small').nth(0) + notifyContainerButton = (): Locator => this.page.locator('div.notifyPopup button[data-id="btnNotifyClose"]').first() errorSpan = (): Locator => this.page.locator('div.ERROR span') infoSpan = (): Locator => this.page.locator('div.INFO span') popupSubmitButton = (): Locator => this.page.locator('div.popup button[type="submit"]') @@ -165,8 +165,10 @@ export class CommonPage { await this.selectPopupSpanLines(point).click() } - async closeNotification (page: Page): Promise { - await this.notifyContainerButton().click() + async closeNotification (): Promise { + while (await this.notifyContainerButton().isVisible()) { + await this.notifyContainerButton().click() + } } async checkError (page: Page, errorMessage: string): Promise { diff --git a/tests/sanity/tests/model/documents/document-content-page.ts b/tests/sanity/tests/model/documents/document-content-page.ts index 5d6de50ea6..f806ebd5f4 100644 --- a/tests/sanity/tests/model/documents/document-content-page.ts +++ b/tests/sanity/tests/model/documents/document-content-page.ts @@ -19,6 +19,8 @@ export class DocumentContentPage extends CommonPage { readonly buttonMoreActions = (): Locator => this.page.locator('div.hulyHeader-buttonsGroup button#btn-doc-title-open-more') + readonly buttonLockedInTitle = (): Locator => this.page.getByRole('button', { name: 'Locked' }) + readonly popupPanel = (): Locator => this.page.locator('div.popupPanel-title') readonly popupPanelH1 = (): Locator => this.page.locator('div.antiPopup > h1') @@ -26,6 +28,10 @@ export class DocumentContentPage extends CommonPage { await expect(this.buttonDocumentTitle()).toHaveValue(title) } + async checkDocumentLocked (): Promise { + await expect(this.buttonLockedInTitle()).toBeVisible({ timeout: 1000 }) + } + async addContentToTheNewLine (newContent: string): Promise { await expect(this.inputContent()).toBeVisible() await expect(this.inputContent()).toHaveJSProperty('contentEditable', 'true') diff --git a/tests/sanity/tests/model/documents/documents-page.ts b/tests/sanity/tests/model/documents/documents-page.ts index 52fb26d215..7f11d1da24 100644 --- a/tests/sanity/tests/model/documents/documents-page.ts +++ b/tests/sanity/tests/model/documents/documents-page.ts @@ -20,6 +20,9 @@ export class DocumentsPage extends CommonPage { readonly buttonCreateDocument = (): Locator => this.page.locator('div[data-float="navigator"] button[id="new-document"]') + readonly buttonDocument = (name: string): Locator => + this.page.locator('button.hulyNavItem-container > span[class*="label"]', { hasText: name }) + readonly divTeamspacesParent = (): Locator => this.page.locator('div#navGroup-tree-teamspaces').locator('xpath=../button[1]') @@ -97,7 +100,13 @@ export class DocumentsPage extends CommonPage { } async openDocument (name: string): Promise { - await this.page.locator('button.hulyNavItem-container > span[class*="label"]', { hasText: name }).click() + await this.buttonDocument(name).click() + } + + async selectMoreActionOfDocument (name: string, popupItem: string): Promise { + await this.buttonDocument(name).hover() + await this.page.getByRole('button', { name }).getByRole('button').nth(2).click() + await this.selectFromDropdown(this.page, popupItem) } async openDocumentForTeamspace (spaceName: string, documentName: string): Promise { diff --git a/tests/sanity/tests/model/inbox.ts/inbox-page.ts b/tests/sanity/tests/model/inbox.ts/inbox-page.ts index d8ad4a116a..1d456395dd 100644 --- a/tests/sanity/tests/model/inbox.ts/inbox-page.ts +++ b/tests/sanity/tests/model/inbox.ts/inbox-page.ts @@ -12,7 +12,7 @@ export class InboxPage { readonly leftSidePanelOpen = (): Locator => this.page.locator('#btnPAside') readonly leftSidePanelClose = (): Locator => this.page.locator('#btnPClose') readonly inboxChat = (text: string): Locator => this.page.getByText(text) - readonly issueTitle = (issueTitle: string): Locator => this.page.getByTitle(issueTitle) + readonly issueTitle = (issueTitle: string): Locator => this.page.getByText(issueTitle).first() // ACTIONS @@ -48,6 +48,10 @@ export class InboxPage { await expect(this.issueTitle(issueTitle)).toBeVisible() } + async clickIssuePresentInInbox (issueTitle: string): Promise { + await this.issueTitle(issueTitle).click() + } + async checkIfInboxChatExists (text: string, exists: boolean): Promise { if (exists) { await expect(this.inboxChat(text)).toBeVisible() diff --git a/tests/sanity/tests/model/team-page.ts b/tests/sanity/tests/model/team-page.ts index 8bba4f4e29..e4e93cc5ab 100644 --- a/tests/sanity/tests/model/team-page.ts +++ b/tests/sanity/tests/model/team-page.ts @@ -16,6 +16,9 @@ export class TeamPage extends CommonPage { buttonNextDay = (): Locator => this.page.locator('div.hulyComponent div.hulyHeader-container .actions button[data-id="btnNext"]') + getItemByText = (column: string, title: string): Locator => + this.page.locator('div.hulyComponent div.item', { hasText: column }).locator('div.item', { hasText: title }) + async checkTeamPageIsOpened (): Promise { await expect(this.appHeader()).toBeVisible() } diff --git a/tests/sanity/tests/model/tracker/issues-details-page.ts b/tests/sanity/tests/model/tracker/issues-details-page.ts index e1e77c44a3..270646f99b 100644 --- a/tests/sanity/tests/model/tracker/issues-details-page.ts +++ b/tests/sanity/tests/model/tracker/issues-details-page.ts @@ -1,6 +1,7 @@ import { expect, type Locator, type Page } from '@playwright/test' import { CommonTrackerPage } from './common-tracker-page' import { Issue, NewIssue } from './types' +import { convertEstimation } from '../../tracker/tracker.utils' export class IssuesDetailsPage extends CommonTrackerPage { readonly page: Page @@ -139,10 +140,10 @@ export class IssuesDetailsPage extends CommonTrackerPage { await expect(this.buttonComponent()).toHaveText(data.component) } if (data.milestone != null) { - await expect(this.buttonMilestone()).toHaveText(data.milestone) + await expect(this.buttonMilestone()).toHaveText(data.milestone === 'No Milestone' ? 'Milestone' : data.milestone) } if (data.estimation != null) { - await expect(this.textEstimation()).toHaveText(data.estimation) + await expect(this.textEstimation()).toHaveText(convertEstimation(data.estimation)) } if (data.parentIssue != null) { await expect(this.textParentTitle()).toHaveText(data.parentIssue) diff --git a/tests/sanity/tests/model/tracker/issues-page.ts b/tests/sanity/tests/model/tracker/issues-page.ts index 869789e494..b44f8068d7 100644 --- a/tests/sanity/tests/model/tracker/issues-page.ts +++ b/tests/sanity/tests/model/tracker/issues-page.ts @@ -412,7 +412,7 @@ export class IssuesPage extends CommonTrackerPage { await this.fillNewIssueForm(data) await this.clickButtonCreateIssue() if (closeNotification) { - await this.closeNotification(this.page) + await this.closeNotification() } await attachScreenshot(`createdNewIssue-${data.title}.png`, this.page) } diff --git a/tests/sanity/tests/model/tracker/template-details-page.ts b/tests/sanity/tests/model/tracker/template-details-page.ts index 5d14235475..eb44b9bc75 100644 --- a/tests/sanity/tests/model/tracker/template-details-page.ts +++ b/tests/sanity/tests/model/tracker/template-details-page.ts @@ -1,6 +1,7 @@ import { expect, type Locator } from '@playwright/test' import { CommonTrackerPage } from './common-tracker-page' import { Issue, NewIssue } from './types' +import { convertEstimation } from '../../tracker/tracker.utils' export class TemplateDetailsPage extends CommonTrackerPage { inputTitle = (): Locator => this.page.locator('div.popupPanel-body input[type="text"]') @@ -34,7 +35,7 @@ export class TemplateDetailsPage extends CommonTrackerPage { await expect(this.buttonComponent()).toHaveText(data.component) } if (data.estimation != null) { - await expect(this.buttonEstimation()).toHaveText(data.estimation) + await expect(this.buttonEstimation()).toHaveText(convertEstimation(data.estimation)) } } diff --git a/tests/sanity/tests/planning/plan.spec.ts b/tests/sanity/tests/planning/plan.spec.ts index 1dfd5ca6d0..9c0c8988b0 100644 --- a/tests/sanity/tests/planning/plan.spec.ts +++ b/tests/sanity/tests/planning/plan.spec.ts @@ -1,5 +1,5 @@ import { test } from '@playwright/test' -import { generateId, PlatformSetting, PlatformURI, generateTestData } from '../utils' +import { generateId, PlatformSetting, PlatformURI, generateTestData, getTimeForPlanner } from '../utils' import { PlanningPage } from '../model/planning/planning-page' import { NewToDo } from '../model/planning/types' import { PlanningNavigationMenuPage } from '../model/planning/planning-navigation-menu-page' @@ -179,12 +179,7 @@ test.describe('Planning ToDo tests', () => { } const titleV = `Visible ToDo ${generateId()}` const titleI = `Inisible ToDo ${generateId()}` - - let hour = new Date().getHours() - const ampm = hour < 13 ? 'am' : 'pm' - hour = hour < 1 ? 1 : hour >= 11 && hour < 13 ? 11 : hour >= 22 ? 10 : hour > 12 ? hour - 12 : hour - const time = `${hour}${ampm}` - // const timeI = `${hour + 1}${ampm}` + const time = getTimeForPlanner() const leftSideMenuPage: LeftSideMenuPage = new LeftSideMenuPage(page) const loginPage: LoginPage = new LoginPage(page) diff --git a/tests/sanity/tests/tracker/common-steps.ts b/tests/sanity/tests/tracker/common-steps.ts index ab53e6a6a3..097f960510 100644 --- a/tests/sanity/tests/tracker/common-steps.ts +++ b/tests/sanity/tests/tracker/common-steps.ts @@ -26,7 +26,7 @@ export async function prepareNewIssueWithOpenStep (page: Page, issue: NewIssue): }) } -export function createNewIssueData (firstName: string, lastName: string): NewIssue { +export function createNewIssueData (firstName: string, lastName: string, replace?: object): NewIssue { return { title: faker.lorem.words(3), description: faker.lorem.sentence(), @@ -39,6 +39,7 @@ export function createNewIssueData (firstName: string, lastName: string): NewIss estimation: '2', milestone: 'No Milestone', duedate: 'today', - filePath: 'cat.jpeg' + filePath: 'cat.jpeg', + ...replace } } diff --git a/tests/sanity/tests/tracker/issues.spec.ts b/tests/sanity/tests/tracker/issues.spec.ts index c32a57ab71..fb0c445f07 100644 --- a/tests/sanity/tests/tracker/issues.spec.ts +++ b/tests/sanity/tests/tracker/issues.spec.ts @@ -46,11 +46,7 @@ test.describe('Tracker issue tests', () => { } await prepareNewIssueWithOpenStep(page, newIssue) - await issuesDetailsPage.checkIssue({ - ...newIssue, - milestone: 'Milestone', - estimation: '2h' - }) + await issuesDetailsPage.checkIssue(newIssue) }) test('Edit an issue', async ({ page }) => { @@ -74,34 +70,19 @@ test.describe('Tracker issue tests', () => { await issuesDetailsPage.checkIssue({ ...newIssue, - ...editIssue, - estimation: '1d' + ...editIssue }) - const estimations = new Map([ - ['0', '0h'], - ['1', '1h'], - ['1.25', '1h 15m'], - ['1.259', '1h 15m'], - ['1.26', '1h 15m'], - ['1.27', '1h 16m'], - ['1.5', '1h 30m'], - ['1.75', '1h 45m'], - ['2', '2h'], - ['7', '7h'], - ['8', '1d'], - ['9', '1d 1h'], - ['9.5', '1d 1h 30m'] - ]) + const estimations = ['0', '1', '1.25', '1.259', '1.26', '1.27', '1.5', '1.75', '2', '7', '8', '9', '9.5'] - for (const [input, expected] of estimations.entries()) { + for (const input of estimations) { await issuesDetailsPage.editIssue({ estimation: input }) await issuesDetailsPage.checkIssue({ ...newIssue, ...editIssue, - estimation: expected + estimation: input }) } }) @@ -222,7 +203,7 @@ test.describe('Tracker issue tests', () => { title: `New Issue-${generateId(4)}`, description: 'New Issue', priority: 'Medium', - estimation: '1d', + estimation: '8', component: 'Default component', milestone: 'Edit Milestone' } diff --git a/tests/sanity/tests/tracker/related-issues.spec.ts b/tests/sanity/tests/tracker/related-issues.spec.ts index d23d8cac0a..b9d399f64e 100644 --- a/tests/sanity/tests/tracker/related-issues.spec.ts +++ b/tests/sanity/tests/tracker/related-issues.spec.ts @@ -57,8 +57,6 @@ test.describe('Tracker related issue tests', () => { await issuesDetailsPage.checkIssue({ ...newIssue, ...relatedIssue, - milestone: 'Milestone', - estimation: '1d 4h', relatedIssue: 'TSK' }) }) diff --git a/tests/sanity/tests/tracker/subissues.spec.ts b/tests/sanity/tests/tracker/subissues.spec.ts index 6e8298aa2e..c4d998f68b 100644 --- a/tests/sanity/tests/tracker/subissues.spec.ts +++ b/tests/sanity/tests/tracker/subissues.spec.ts @@ -94,8 +94,6 @@ test.describe('Tracker sub-issues tests', () => { await issuesDetailsPage.checkIssue({ ...newSubIssue, ...editSubIssue, - milestone: 'Milestone', - estimation: '1d', parentIssue: newIssue.title }) }) diff --git a/tests/sanity/tests/tracker/template.spec.ts b/tests/sanity/tests/tracker/template.spec.ts index c379928c09..6b87bc9f49 100644 --- a/tests/sanity/tests/tracker/template.spec.ts +++ b/tests/sanity/tests/tracker/template.spec.ts @@ -41,10 +41,7 @@ test.describe('Tracker template tests', () => { await trackerNavigationMenuPage.openTemplateForProject('Default') await templatePage.createNewTemplate(newTemplate) await templatePage.openTemplate(newTemplate.title) - await templateDetailsPage.checkTemplate({ - ...newTemplate, - estimation: '2h' - }) + await templateDetailsPage.checkTemplate(newTemplate) }) test('Edit a Template', async ({ page }) => { @@ -69,36 +66,21 @@ test.describe('Tracker template tests', () => { await templateDetailsPage.editTemplate(editTemplate) await templateDetailsPage.checkTemplate({ ...newTemplate, - ...editTemplate, - estimation: '1d' + ...editTemplate }) await templateDetailsPage.checkActivityContent(`New template: ${newTemplate.title}`) - const estimations = new Map([ - ['0', '0h'], - ['1', '1h'], - ['1.25', '1h 15m'], - ['1.259', '1h 15m'], - ['1.26', '1h 15m'], - ['1.27', '1h 16m'], - ['1.5', '1h 30m'], - ['1.75', '1h 45m'], - ['2', '2h'], - ['7', '7h'], - ['8', '1d'], - ['9', '1d 1h'], - ['9.5', '1d 1h 30m'] - ]) + const estimations = ['0', '1', '1.25', '1.259', '1.26', '1.27', '1.5', '1.75', '2', '7', '8', '9', '9.5'] - for (const [input, expected] of estimations.entries()) { + for (const input of estimations) { await templateDetailsPage.editTemplate({ estimation: input }) await templateDetailsPage.checkTemplate({ ...newTemplate, ...editTemplate, - estimation: expected + estimation: input }) } }) diff --git a/tests/sanity/tests/tracker/tracker.spec.ts b/tests/sanity/tests/tracker/tracker.spec.ts index 4529f105e3..b119b24fab 100644 --- a/tests/sanity/tests/tracker/tracker.spec.ts +++ b/tests/sanity/tests/tracker/tracker.spec.ts @@ -168,7 +168,7 @@ test.describe('Tracker tests', () => { status: 'Todo', priority: 'Urgent', assignee: 'Appleseed John', - estimation: '1h', + estimation: '1', dueDate: '24' }) }) diff --git a/tests/sanity/tests/tracker/tracker.utils.ts b/tests/sanity/tests/tracker/tracker.utils.ts index a7d271ca0b..c296ba09c0 100644 --- a/tests/sanity/tests/tracker/tracker.utils.ts +++ b/tests/sanity/tests/tracker/tracker.utils.ts @@ -204,7 +204,7 @@ export async function checkIssueDraft (page: Page, props: IssueProps): Promise= 11 && hour < 13 ? 11 : hour >= 22 ? 10 : hour > 12 ? hour - 12 : hour + + return `${hour}${ampm}` +} + // Consistent data export const workspaceName = faker.lorem.word() export const userName = faker.internet.userName() diff --git a/tests/sanity/tests/workspace/create.spec.ts b/tests/sanity/tests/workspace/create.spec.ts index 4e4d2a5936..d51304d485 100644 --- a/tests/sanity/tests/workspace/create.spec.ts +++ b/tests/sanity/tests/workspace/create.spec.ts @@ -82,11 +82,7 @@ test.describe('Workspace tests', () => { await issuesPage.openIssueByName(newIssue.title) const issuesDetailsPage = new IssuesDetailsPage(page) - await issuesDetailsPage.checkIssue({ - ...newIssue, - milestone: 'Milestone', - estimation: '2h' - }) + await issuesDetailsPage.checkIssue(newIssue) }) test('Check validation steps description for the create flow', async ({ page }) => {