From b949744500f0e7f5598c1478d4b6e314f8cfdd70 Mon Sep 17 00:00:00 2001 From: Victor Ilyushchenko Date: Mon, 13 Jan 2025 14:19:47 +0300 Subject: [PATCH] EZQMS-1317: authors signature on review and approval request & block review bypass (#7631) Signed-off-by: Victor Ilyushchenko --- .../ui/src/components/ModernDialog.svelte | 3 +- .../controlled-documents-assets/lang/en.json | 2 ++ .../controlled-documents-assets/lang/fr.json | 2 ++ .../controlled-documents-assets/lang/ru.json | 2 ++ .../src/components/EditDocPanel.svelte | 3 +- .../src/components/SignatureDialog.svelte | 1 + .../src/components/TeamPopup.svelte | 33 ++++++++++++++++--- .../document/DocumentSignatories.svelte | 9 ++++- .../src/plugin.ts | 2 ++ .../src/stores/editors/document/actions.ts | 4 +++ .../editors/document/canSendForApproval.ts | 13 ++++++-- .../src/stores/editors/document/editor.ts | 5 +++ .../src/stores/editors/document/query.ts | 24 +++++++++++++- .../src/utils.ts | 1 + .../sanity/tests/documents/ES-50.spec.ts | 4 +-- .../sanity/tests/documents/documents.spec.ts | 30 ++++++++++++++--- .../model/documents/document-content-page.ts | 10 ++++++ 17 files changed, 130 insertions(+), 18 deletions(-) diff --git a/packages/ui/src/components/ModernDialog.svelte b/packages/ui/src/components/ModernDialog.svelte index 7fcf88f560..3c5d4605b4 100644 --- a/packages/ui/src/components/ModernDialog.svelte +++ b/packages/ui/src/components/ModernDialog.svelte @@ -44,6 +44,7 @@ export let withoutFooter = false export let closeIcon: AnySvelteComponent = Close export let shadow: boolean = false + export let className: string = '' const dispatch = createEventDispatcher() @@ -66,7 +67,7 @@
import { createEventDispatcher } from 'svelte' import { RequestStatus } from '@hcengineering/request' - import { Label, ModernDialog } from '@hcengineering/ui' + import { Label, ModernDialog, showPopup } from '@hcengineering/ui' import { getClient } from '@hcengineering/presentation' import contact, { Employee, PersonAccount } from '@hcengineering/contact' import { Class, Ref } from '@hcengineering/core' @@ -20,10 +20,12 @@ import documentsRes from '../plugin' import { sendApprovalRequest, sendReviewRequest } from '../utils' + import SignatureDialog from './SignatureDialog.svelte' export let controlledDoc: ControlledDocument export let requestClass: Ref> export let readonly: boolean = false + export let requireSignature: boolean = false const client = getClient() const hierarchy = client.getHierarchy() @@ -67,13 +69,34 @@ let users: Ref[] = controlledDoc[docField] ?? [] async function submit (): Promise { - loading = true + const complete = async (): Promise => { + loading = true - await sendRequestFunc?.(client, controlledDoc, users) + await sendRequestFunc?.(client, controlledDoc, users) - loading = false + loading = false - dispatch('close') + dispatch('close') + } + + if (requireSignature) { + showPopup( + SignatureDialog, + { + confirmationTitle: isReviewRequest + ? documentsRes.string.ConfirmReviewSubmission + : documentsRes.string.ConfirmApprovalSubmission + }, + 'center', + async (res) => { + if (!res) return + + await complete() + } + ) + } else { + await complete() + } } $: canSubmit = docRequest === undefined && users.length > 0 diff --git a/plugins/controlled-documents-resources/src/components/document/DocumentSignatories.svelte b/plugins/controlled-documents-resources/src/components/document/DocumentSignatories.svelte index 5a398676ac..3d128dd2d9 100644 --- a/plugins/controlled-documents-resources/src/components/document/DocumentSignatories.svelte +++ b/plugins/controlled-documents-resources/src/components/document/DocumentSignatories.svelte @@ -87,12 +87,19 @@ return rawName !== undefined ? formatName(rawName) : '' } + const authorSignDate = + reviewRequest !== undefined + ? reviewRequest.createdOn + : approvalRequest !== undefined + ? approvalRequest.createdOn + : $controlledDocument.createdOn + signers = [ { id: $controlledDocument.author, role: 'author', name: getNameByEmployeeId($controlledDocument.author), - date: $controlledDocument.createdOn !== undefined ? formatSignatureDate($controlledDocument.createdOn) : '' + date: authorSignDate !== undefined ? formatSignatureDate(authorSignDate) : '' } ] diff --git a/plugins/controlled-documents-resources/src/plugin.ts b/plugins/controlled-documents-resources/src/plugin.ts index 2ef3db37da..7ee46767e1 100644 --- a/plugins/controlled-documents-resources/src/plugin.ts +++ b/plugins/controlled-documents-resources/src/plugin.ts @@ -116,9 +116,11 @@ export default mergeIds(documentsId, documents, { ConfirmApproval: '' as IntlString, ConfirmRejection: '' as IntlString, + ConfirmApprovalSubmission: '' as IntlString, ProvideRejectionReason: '' as IntlString, RejectionReason: '' as IntlString, ConfirmReviewCompletion: '' as IntlString, + ConfirmReviewSubmission: '' as IntlString, AddApprovalTitle: '' as IntlString, AddApprovalDescription1: '' as IntlString, AddApprovalDescription2: '' as IntlString, diff --git a/plugins/controlled-documents-resources/src/stores/editors/document/actions.ts b/plugins/controlled-documents-resources/src/stores/editors/document/actions.ts index 65594d8c7d..d14cc4c360 100644 --- a/plugins/controlled-documents-resources/src/stores/editors/document/actions.ts +++ b/plugins/controlled-documents-resources/src/stores/editors/document/actions.ts @@ -64,6 +64,10 @@ export const documentAllVersionsUpdated = createEvent( export const reviewRequestUpdated = createEvent(generateActionName('reviewRequestUpdated')) +export const reviewRequestHistoryUpdated = createEvent( + generateActionName('reviewRequestHistoryUpdated') +) + export const approvalRequestUpdated = createEvent(generateActionName('approvalRequestUpdated')) export const editorModeUpdated = createEvent(generateActionName('editorModeUpdated')) diff --git a/plugins/controlled-documents-resources/src/stores/editors/document/canSendForApproval.ts b/plugins/controlled-documents-resources/src/stores/editors/document/canSendForApproval.ts index 2a88fcd43f..da89eedd2f 100644 --- a/plugins/controlled-documents-resources/src/stores/editors/document/canSendForApproval.ts +++ b/plugins/controlled-documents-resources/src/stores/editors/document/canSendForApproval.ts @@ -17,17 +17,24 @@ import { ControlledDocumentState, DocumentState } from '@hcengineering/controlle import { TrainingState } from '@hcengineering/training' import { combine } from 'effector' import { $documentComments } from './documentComments' -import { $documentState, $isLatestVersion, $training } from './editor' +import { $controlledDocument, $documentState, $isLatestVersion, $reviewRequestHistory, $training } from './editor' export const $canSendForApproval = combine( + $controlledDocument, $isLatestVersion, $documentState, $documentComments, $training, - (isLatestVersion, state, comments, training) => { + $reviewRequestHistory, + (document, isLatestVersion, state, comments, training, reviewHistory) => { + let haveBeenReviewedOnce = false + if (document !== null) { + const reviews = (reviewHistory ?? []).filter((review) => review.attachedTo === document._id) + if (reviews.length > 0) haveBeenReviewedOnce = true + } return ( isLatestVersion && - (state === DocumentState.Draft || state === ControlledDocumentState.Reviewed) && + ((state === DocumentState.Draft && !haveBeenReviewedOnce) || state === ControlledDocumentState.Reviewed) && comments.every((comment) => comment.resolved) && (training === null || training.state === TrainingState.Released) ) diff --git a/plugins/controlled-documents-resources/src/stores/editors/document/editor.ts b/plugins/controlled-documents-resources/src/stores/editors/document/editor.ts index 4b1ea606f7..5c43e56f34 100644 --- a/plugins/controlled-documents-resources/src/stores/editors/document/editor.ts +++ b/plugins/controlled-documents-resources/src/stores/editors/document/editor.ts @@ -43,6 +43,7 @@ import { documentAllVersionsUpdated, editorModeUpdated, reviewRequestUpdated, + reviewRequestHistoryUpdated, rightPanelTabChanged, documentSnapshotsUpdated, trainingUpdated, @@ -122,6 +123,10 @@ export const $reviewRequest = createStore(null) .on(reviewRequestUpdated, (_, payload) => payload) .reset(controlledDocumentClosed) +export const $reviewRequestHistory = createStore(null) + .on(reviewRequestHistoryUpdated, (_, payload) => payload) + .reset(controlledDocumentClosed) + export const $approvalRequest = createStore(null) .on(approvalRequestUpdated, (_, payload) => payload) .reset(controlledDocumentClosed) diff --git a/plugins/controlled-documents-resources/src/stores/editors/document/query.ts b/plugins/controlled-documents-resources/src/stores/editors/document/query.ts index e5bce7be3e..6c9d5ce41d 100644 --- a/plugins/controlled-documents-resources/src/stores/editors/document/query.ts +++ b/plugins/controlled-documents-resources/src/stores/editors/document/query.ts @@ -39,7 +39,8 @@ import { documentSnapshotsUpdated, trainingUpdated, projectUpdated, - projectDocumentsUpdated + projectDocumentsUpdated, + reviewRequestHistoryUpdated } from './actions' import { $documentCommentsFilter } from './documentComments' import { $controlledDocument, $documentTraining } from './editor' @@ -48,6 +49,7 @@ const controlledDocumentQuery = createQuery(true) const documentVersionsQuery = createQuery(true) const documentSnapshotsQuery = createQuery(true) const reviewRequestQuery = createQuery(true) +const reviewRequestHistoryQuery = createQuery(true) const approvalRequestQuery = createQuery(true) const documentCommentsQuery = createQuery(true) const workingCopyMetadataQuery = createQuery(true) @@ -125,6 +127,25 @@ const queryReviewRequestFx = createEffect( } ) +const queryReviewRequestHistoryFx = createEffect( + (payload: { _id: Ref, _class: Ref> }) => { + const { _id, _class } = payload + if (_id == null || _class == null) { + reviewRequestHistoryQuery.unsubscribe() + return + } + reviewRequestHistoryQuery.query( + documents.class.DocumentReviewRequest, + { attachedTo: _id, attachedToClass: _class }, + (result) => { + if (result !== null && result !== undefined && result.length > 0) { + reviewRequestHistoryUpdated(result) + } + } + ) + } +) + const queryApprovalRequestFx = createEffect( (payload: { _id: Ref, _class: Ref> }) => { const { _id, _class } = payload @@ -259,6 +280,7 @@ forward({ to: [ queryControlledDocumentFx, queryReviewRequestFx, + queryReviewRequestHistoryFx, queryApprovalRequestFx, querySavedAttachmentsFx, queryProjectFx, diff --git a/plugins/controlled-documents-resources/src/utils.ts b/plugins/controlled-documents-resources/src/utils.ts index fe07c0132d..2cff4e07ed 100644 --- a/plugins/controlled-documents-resources/src/utils.ts +++ b/plugins/controlled-documents-resources/src/utils.ts @@ -185,6 +185,7 @@ export async function getDocumentMetaLinkFragment (document: Doc): Promise> + requireSignature?: boolean } export async function sendReviewRequest ( diff --git a/qms-tests/sanity/tests/documents/ES-50.spec.ts b/qms-tests/sanity/tests/documents/ES-50.spec.ts index fd420e207b..d331c5e26d 100644 --- a/qms-tests/sanity/tests/documents/ES-50.spec.ts +++ b/qms-tests/sanity/tests/documents/ES-50.spec.ts @@ -226,7 +226,7 @@ test.describe('QMS. PDF Download and Preview', () => { const documentContentPageSecond = new DocumentContentPage(userSecondPage) const documentsPageSecond = new DocumentsPage(userSecondPage) await documentsPageSecond.openDocument(approveDocument.title) - await documentContentPageSecond.clickApproveButton() + await documentContentPageSecond.confirmApproval() await documentsPageSecond.openDocument(approveDocument.title) }) await test.step('5. check if reviewers and approvers are visible', async () => { @@ -275,7 +275,7 @@ test.describe('QMS. PDF Download and Preview', () => { const documentContentPageSecond = new DocumentContentPage(userSecondPage) const documentsPageSecond = new DocumentsPage(userSecondPage) await documentsPageSecond.openDocument(approveDocument.title) - await documentContentPageSecond.clickApproveButton() + await documentContentPageSecond.confirmApproval() await documentsPageSecond.openDocument(approveDocument.title) }) await test.step('5. check if reviewers and approvers are visible', async () => { diff --git a/qms-tests/sanity/tests/documents/documents.spec.ts b/qms-tests/sanity/tests/documents/documents.spec.ts index d5b2783cb1..80b8dd0afa 100644 --- a/qms-tests/sanity/tests/documents/documents.spec.ts +++ b/qms-tests/sanity/tests/documents/documents.spec.ts @@ -926,7 +926,29 @@ test.describe('QMS. Documents tests', () => { await attachScreenshot('TESTS-206_check_document.png', page) }) - await test.step('7. Send for Approval', async () => { + await test.step('7. Send for Review', async () => { + await documentContentPage.buttonSendForReview.click() + await documentContentPage.fillSelectReviewersForm([]) + await documentContentPage.checkDocumentStatus(DocumentStatus.IN_REVIEW) + await documentContentPage.checkDocument({ + ...documentDetails, + status: DocumentStatus.IN_REVIEW + }) + await attachScreenshot('TESTS-206_send_for_review_2.png', page) + }) + + await test.step('8. Complete Review', async () => { + const documentContentPageSecond = new DocumentContentPage(userSecondPage) + + await documentContentPageSecond.completeReview() + + await documentContentPageSecond.checkDocumentStatus(DocumentStatus.REVIEWED) + await documentContentPageSecond.checkCurrentRights(DocumentRights.VIEWING) + + await attachScreenshot('TESTS-206_complete_review_2.png', page) + }) + + await test.step('9. Send for Approval', async () => { await documentContentPage.buttonSendForApproval.click() await documentContentPage.fillSelectApproversForm([reviewer]) await documentContentPage.checkDocumentStatus(DocumentStatus.IN_APPROVAL) @@ -938,7 +960,7 @@ test.describe('QMS. Documents tests', () => { await documentContentPage.checkCurrentRights(DocumentRights.VIEWING) }) - await test.step('8. Approve document', async () => { + await test.step('10. Approve document', async () => { const documentsPageSecond = new DocumentsPage(userSecondPage) await documentsPageSecond.openDocument(completeDocument.title) @@ -957,7 +979,7 @@ test.describe('QMS. Documents tests', () => { await attachScreenshot('TESTS-206_approve_document.png', page) }) - await test.step('9. Check document', async () => { + await test.step('11. Check document', async () => { await documentContentPage.checkDocumentStatus(DocumentStatus.EFFECTIVE) await documentContentPage.checkDocument({ ...documentDetails, @@ -969,7 +991,7 @@ test.describe('QMS. Documents tests', () => { await attachScreenshot('TESTS-206_check_document.png', page) }) - await test.step('10. Check History tab', async () => { + await test.step('12. Check History tab', async () => { await documentContentPage.buttonHistoryTab.first().click() const documentHistoryPage = new DocumentHistoryPage(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 accdaf7f8b..9ca1a3e759 100644 --- a/qms-tests/sanity/tests/model/documents/document-content-page.ts +++ b/qms-tests/sanity/tests/model/documents/document-content-page.ts @@ -31,6 +31,7 @@ export class DocumentContentPage extends DocumentCommonPage { readonly buttonCompleteReview: Locator readonly inputPassword: Locator readonly buttonSubmit: Locator + readonly buttonSubmitSignature: Locator readonly buttonReject: Locator readonly inputRejectionReason: Locator readonly buttonApprove: Locator @@ -137,6 +138,7 @@ export class DocumentContentPage extends DocumentCommonPage { }) this.inputPassword = page.locator('input[name="documents:string:Password"]') this.buttonSubmit = page.locator('div.popup button[type="submit"]') + this.buttonSubmitSignature = page.locator('.signature-dialog button[type="submit"]') this.buttonReject = page.locator('button[type="button"] > span', { hasText: 'Reject' }) this.inputRejectionReason = page.locator('div.popup div[id="rejection-reason"] input') this.buttonApprove = page.locator('button[type="button"] > span', { hasText: 'Approve' }) @@ -727,6 +729,7 @@ export class DocumentContentPage extends DocumentCommonPage { } await this.textSelectReviewersPopup.click({ force: true }) await this.buttonSelectMemberSubmit.click() + await this.confirmSubmission() } async fillSelectApproversForm (approvers: Array): Promise { @@ -736,6 +739,7 @@ export class DocumentContentPage extends DocumentCommonPage { } await this.textSelectApproversPopup.click({ force: true }) await this.buttonSelectMemberSubmit.click() + await this.confirmSubmission() } async checkCurrentRights (right: DocumentRights): Promise { @@ -772,6 +776,7 @@ export class DocumentContentPage extends DocumentCommonPage { await this.addReasonAndImpactToTheDocument(reason, impact) await this.buttonSendForApproval.click() await this.buttonSelectMemberSubmit.click() + await this.confirmSubmission() await this.checkDocumentStatus(DocumentStatus.IN_APPROVAL) await this.checkDocument({ ...documentDetails, @@ -817,6 +822,11 @@ export class DocumentContentPage extends DocumentCommonPage { await this.buttonSubmit.click() } + async confirmSubmission (): Promise { + await this.inputPassword.fill(PlatformPassword) + await this.buttonSubmitSignature.click() + } + async changeCurrentRight (newRight: DocumentRights): Promise { await this.buttonCurrentRights.click() await this.selectMenuItem(this.page, newRight)