From 3c822510cdab929f73e91dee004956eeb92b96f8 Mon Sep 17 00:00:00 2001 From: Victor Ilyushchenko Date: Fri, 7 Feb 2025 19:27:04 +0300 Subject: [PATCH] Additional tweaks for EQMS-1317 (#7854) * Additional tweaks for EZQMS-1317 Signed-off-by: Victor Ilyushchenko * fixed tests Signed-off-by: Victor Ilyushchenko * v2 Signed-off-by: Victor Ilyushchenko * fixed tests Signed-off-by: Victor Ilyushchenko --------- Signed-off-by: Victor Ilyushchenko --- .../controlled-documents-assets/lang/cs.json | 2 +- .../controlled-documents-assets/lang/de.json | 2 +- .../controlled-documents-assets/lang/en.json | 2 +- .../controlled-documents-assets/lang/fr.json | 2 +- .../controlled-documents-assets/lang/it.json | 2 +- .../controlled-documents-assets/lang/ru.json | 2 +- .../controlled-documents-assets/lang/zh.json | 2 +- .../src/components/EditDocPanel.svelte | 8 +- .../document/DocumentSignatories.svelte | 153 ++++++------------ .../right-panel/DocumentApprovalItem.svelte | 126 ++++----------- .../right-panel/DocumentApprovalsTab.svelte | 83 ++++++---- .../src/plugin.ts | 2 +- .../src/stores/editors/document/editor.ts | 2 +- plugins/controlled-documents/src/utils.ts | 137 +++++++++++++++- .../sanity/tests/documents/documents.spec.ts | 2 +- .../documents/document-approvals-page.ts | 9 +- .../model/documents/document-content-page.ts | 4 +- 17 files changed, 291 insertions(+), 249 deletions(-) diff --git a/plugins/controlled-documents-assets/lang/cs.json b/plugins/controlled-documents-assets/lang/cs.json index 72564318c7..34f47aca5f 100644 --- a/plugins/controlled-documents-assets/lang/cs.json +++ b/plugins/controlled-documents-assets/lang/cs.json @@ -21,7 +21,7 @@ "Major": "Hlavní", "Minor": "Vedlejší", "Patch": "Oprava", - "DocumentApprovals": "Revize a schválení", + "ValidationWorkflow": "Ověřovací pracovní postup", "ChangeOwner": "Změnit vlastníka dokumentu", "ChangeOwnerHintBeginning": "Převést vlastnictví", "ChangeOwnerHintEnd": "na jinou osobu.", diff --git a/plugins/controlled-documents-assets/lang/de.json b/plugins/controlled-documents-assets/lang/de.json index 3c44f68fda..0653e4b4c0 100644 --- a/plugins/controlled-documents-assets/lang/de.json +++ b/plugins/controlled-documents-assets/lang/de.json @@ -21,7 +21,7 @@ "Major": "Hauptversion", "Minor": "Nebenversion", "Patch": "Patch", - "DocumentApprovals": "Prüfungen und Genehmigungen", + "ValidationWorkflow": "Validierungsworkflow", "ChangeOwner": "Dokumentenbesitzer ändern", "ChangeOwnerHintBeginning": "Übertragen Sie den Besitz des", "ChangeOwnerHintEnd": "an eine andere Person.", diff --git a/plugins/controlled-documents-assets/lang/en.json b/plugins/controlled-documents-assets/lang/en.json index 9bbde8a196..dbd49df4b7 100644 --- a/plugins/controlled-documents-assets/lang/en.json +++ b/plugins/controlled-documents-assets/lang/en.json @@ -21,7 +21,7 @@ "Major": "Major", "Minor": "Minor", "Patch": "Patch", - "DocumentApprovals": "Reviews and Approvals", + "ValidationWorkflow": "Validation workflow", "ChangeOwner": "Change document owner", "ChangeOwnerHintBeginning": "Transfer ownership of the", "ChangeOwnerHintEnd": "to another person.", diff --git a/plugins/controlled-documents-assets/lang/fr.json b/plugins/controlled-documents-assets/lang/fr.json index 5ccba0a666..9b2ee9fe3f 100644 --- a/plugins/controlled-documents-assets/lang/fr.json +++ b/plugins/controlled-documents-assets/lang/fr.json @@ -21,7 +21,7 @@ "Major": "Majeur", "Minor": "Mineur", "Patch": "Correctif", - "DocumentApprovals": "Révisions et approbations", + "ValidationWorkflow": "Flux de validation", "ChangeOwner": "Changer le propriétaire du document", "ChangeOwnerHintBeginning": "Transférer la propriété du", "ChangeOwnerHintEnd": "à une autre personne.", diff --git a/plugins/controlled-documents-assets/lang/it.json b/plugins/controlled-documents-assets/lang/it.json index 5fcaf63e52..1ec19f9672 100644 --- a/plugins/controlled-documents-assets/lang/it.json +++ b/plugins/controlled-documents-assets/lang/it.json @@ -21,7 +21,7 @@ "Major": "Maggiore", "Minor": "Minore", "Patch": "Patch", - "DocumentApprovals": "Revisioni e Approvazioni", + "ValidationWorkflow": "Flusso di convalida", "ChangeOwner": "Cambia proprietario del documento", "ChangeOwnerHintBeginning": "Trasferisci la proprietà del", "ChangeOwnerHintEnd": "a un'altra persona.", diff --git a/plugins/controlled-documents-assets/lang/ru.json b/plugins/controlled-documents-assets/lang/ru.json index 9093c92ee1..dd99112ec0 100644 --- a/plugins/controlled-documents-assets/lang/ru.json +++ b/plugins/controlled-documents-assets/lang/ru.json @@ -21,7 +21,7 @@ "Major": "Мажорная", "Minor": "Минорная", "Patch": "Патч", - "DocumentApprovals": "Рецензии и утверждения", + "ValidationWorkflow": "Процесс валидации", "ChangeOwner": "Изменить владельца документа", "ChangeOwnerHintBeginning": "Передайте права владельца документа", "ChangeOwnerHintEnd": "другому лицу.", diff --git a/plugins/controlled-documents-assets/lang/zh.json b/plugins/controlled-documents-assets/lang/zh.json index f7ac73621a..d0b07eddbe 100644 --- a/plugins/controlled-documents-assets/lang/zh.json +++ b/plugins/controlled-documents-assets/lang/zh.json @@ -21,7 +21,7 @@ "Major": "主要", "Minor": "次要", "Patch": "补丁", - "DocumentApprovals": "审查和批准", + "ValidationWorkflow": "验证工作流程", "ChangeOwner": "更改文档所有者", "ChangeOwnerHintBeginning": "转移所有权", "ChangeOwnerHintEnd": "给其他人。", diff --git a/plugins/controlled-documents-resources/src/components/EditDocPanel.svelte b/plugins/controlled-documents-resources/src/components/EditDocPanel.svelte index 5af0cfa7d2..e11c2c176f 100644 --- a/plugins/controlled-documents-resources/src/components/EditDocPanel.svelte +++ b/plugins/controlled-documents-resources/src/components/EditDocPanel.svelte @@ -36,6 +36,7 @@ ControlledDocument, ControlledDocumentState, DocumentRequest, + DocumentState, Project } from '@hcengineering/controlled-documents' import { createEventDispatcher, onDestroy, onMount } from 'svelte' @@ -163,10 +164,15 @@ return } + const hierarchy = client.getHierarchy() + + const isReviewed = $controlledDocument.controlledState === ControlledDocumentState.Reviewed + const isApprovalRequest = hierarchy.isDerived(requestClass, documents.class.DocumentApprovalRequest) + const teamPopupData: TeamPopupData = { controlledDoc: $controlledDocument, requestClass, - requireSignature: true + requireSignature: !(isReviewed && isApprovalRequest) } showPopup(TeamPopup, teamPopupData, 'center') diff --git a/plugins/controlled-documents-resources/src/components/document/DocumentSignatories.svelte b/plugins/controlled-documents-resources/src/components/document/DocumentSignatories.svelte index 3d128dd2d9..390a3197cc 100644 --- a/plugins/controlled-documents-resources/src/components/document/DocumentSignatories.svelte +++ b/plugins/controlled-documents-resources/src/components/document/DocumentSignatories.svelte @@ -13,121 +13,68 @@ // limitations under the License. --> {#if expanded}
- {#each approvals as approver} + {#each approvals as approval} + {@const messages = approval.messages ?? []}
- - {#key approver.timestamp} - + + {#key approval.timestamp} - {#if approver.approved === 'approved'} + + {#if approval.state === 'approved'} - {:else if approver.approved === 'rejected'} + {:else if approval.state === 'rejected'} - {:else if approver.approved === 'cancelled'} + {:else if approval.state === 'cancelled'} - {:else if approver.approved === 'waiting'} + {:else if approval.state === 'waiting'} {/if} {/key}
- {#if rejectingMessage !== undefined && approver.approved === 'rejected'} -
{rejectingMessage}
- {/if} + {#each messages as m} +
{m.message}
+ {/each} {/each}
{/if} @@ -199,7 +137,7 @@ flex-shrink: 0; border-bottom: 1px solid var(--theme-divider-color); - .reject-message { + .approval-status-message { font-weight: 400; padding: 0.625rem 1rem 0 2rem; } diff --git a/plugins/controlled-documents-resources/src/components/document/right-panel/DocumentApprovalsTab.svelte b/plugins/controlled-documents-resources/src/components/document/right-panel/DocumentApprovalsTab.svelte index 8bdd6645c5..61149a69f5 100644 --- a/plugins/controlled-documents-resources/src/components/document/right-panel/DocumentApprovalsTab.svelte +++ b/plugins/controlled-documents-resources/src/components/document/right-panel/DocumentApprovalsTab.svelte @@ -2,56 +2,71 @@ import documents, { ControlledDocumentState, DocumentRequest, - DocumentState + DocumentState, + emptyBundle, + extractValidationWorkflow } from '@hcengineering/controlled-documents' import { SortingOrder } from '@hcengineering/core' import { createQuery, getClient } from '@hcengineering/presentation' import { Label, Scroller } from '@hcengineering/ui' - import { $controlledDocument as controlledDocument } from '../../../stores/editors/document' - import document from '../../../plugin' - import RightPanelTabHeader from './RightPanelTabHeader.svelte' - import DocumentApprovalItem from './DocumentApprovalItem.svelte' + import { personIdByAccountId } from '@hcengineering/contact-resources' + import documentsRes from '../../../plugin' + import { + $controlledDocument as controlledDocument, + $documentSnapshots as documentSnapshots + } from '../../../stores/editors/document' import DocumentApprovalGuideItem from './DocumentApprovalGuideItem.svelte' + import DocumentApprovalItem from './DocumentApprovalItem.svelte' + import RightPanelTabHeader from './RightPanelTabHeader.svelte' + import chunter, { ChatMessage } from '@hcengineering/chunter' const client = getClient() const hierarchy = client.getHierarchy() let requests: DocumentRequest[] = [] - let approvals: DocumentRequest[] = [] + let messages: ChatMessage[] = [] - $: approvals = requests.filter((p) => hierarchy.isDerived(p._class, documents.class.DocumentApprovalRequest)) + $: doc = $controlledDocument + const requestQuery = createQuery() + $: if (doc) { + requestQuery.query(documents.class.DocumentRequest, { attachedTo: doc._id }, (r) => { + requests = r + }) + } - const query = createQuery() - $: query.query( - documents.class.DocumentRequest, + const messageQuery = createQuery() + $: if (doc) { + messageQuery.query(chunter.class.ChatMessage, { attachedTo: { $in: requests.map((r) => r._id) } }, (r) => { + messages = r + }) + } + + $: workflow = extractValidationWorkflow( + hierarchy, { - _class: { - $in: [documents.class.DocumentApprovalRequest, documents.class.DocumentReviewRequest] - }, - attachedTo: $controlledDocument?._id + ...emptyBundle(), + ControlledDocument: doc ? [doc] : [], + DocumentRequest: requests, + DocumentSnapshot: $documentSnapshots, + ChatMessage: messages }, - (result) => { - requests = result - }, - { - sort: { createdOn: SortingOrder.Descending } - } + (ref) => $personIdByAccountId.get(ref) ) - $: hasGuide = - $controlledDocument?.state === DocumentState.Draft && - ($controlledDocument?.controlledState == null || - ![ - ControlledDocumentState.Approved, - ControlledDocumentState.Rejected, - ControlledDocumentState.InApproval - ].includes($controlledDocument?.controlledState)) + $: validationStates = ((doc ? workflow.get(doc._id) : []) ?? []).slice() + + const noGuideStates: (ControlledDocumentState | undefined)[] = [ + ControlledDocumentState.Approved, + ControlledDocumentState.Rejected, + ControlledDocumentState.InApproval + ] + $: hasGuide = doc && doc.state === DocumentState.Draft && !noGuideStates.includes(doc.controlledState) - @@ -60,13 +75,13 @@ {/if} - {#if requests.length > 0} - {#each requests as object, idx} - + {#if validationStates.length > 0} + {#each validationStates as state, idx} + {/each} {/if} - {#if !hasGuide && approvals.length === 0} -
+ {#if !hasGuide && requests.length === 0} +
{/if}
diff --git a/plugins/controlled-documents-resources/src/plugin.ts b/plugins/controlled-documents-resources/src/plugin.ts index b91b51218d..2c139b36c7 100644 --- a/plugins/controlled-documents-resources/src/plugin.ts +++ b/plugins/controlled-documents-resources/src/plugin.ts @@ -46,7 +46,7 @@ export default mergeIds(documentsId, documents, { }, string: { ID: '' as IntlString, - DocumentApprovals: '' as IntlString, + ValidationWorkflow: '' as IntlString, Cancel: '' as IntlString, NewDocumentDialogClose: '' as IntlString, NewDocumentCloseNote: '' as IntlString, 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 5c43e56f34..374ace90be 100644 --- a/plugins/controlled-documents-resources/src/stores/editors/document/editor.ts +++ b/plugins/controlled-documents-resources/src/stores/editors/document/editor.ts @@ -366,7 +366,7 @@ export const $availableRightPanelTabs = combine($canViewDocumentComments, (canVi tabs.push({ id: RightPanelTab.APPROVALS, icon: plugin.icon.Approvals, - showTooltip: { label: plugin.string.DocumentApprovals } + showTooltip: { label: plugin.string.ValidationWorkflow } }) return tabs diff --git a/plugins/controlled-documents/src/utils.ts b/plugins/controlled-documents/src/utils.ts index 6d2796fb6f..0e093632e1 100644 --- a/plugins/controlled-documents/src/utils.ts +++ b/plugins/controlled-documents/src/utils.ts @@ -20,11 +20,13 @@ import { Doc, DocumentQuery, DocumentUpdate, + Hierarchy, getCurrentAccount, Rank, Ref, SortingOrder, Space, + Timestamp, toIdMap, TxOperations } from '@hcengineering/core' @@ -35,7 +37,7 @@ import documents from './plugin' import attachment, { Attachment } from '@hcengineering/attachment' import chunter, { ChatMessage } from '@hcengineering/chunter' -import { Employee } from '@hcengineering/contact' +import { Person, PersonAccount, Employee } from '@hcengineering/contact' import { makeRank } from '@hcengineering/rank' import tags, { TagReference } from '@hcengineering/tags' import { @@ -628,6 +630,139 @@ async function _transferDocuments ( return commit.result } +export interface DocumentApprovalState { + person?: Ref + role: 'author' | 'reviewer' | 'approver' + state: 'approved' | 'rejected' | 'cancelled' | 'waiting' + timestamp?: Timestamp + messages?: ChatMessage[] +} + +export interface DocumentValidationState { + requests: DocumentRequest[] + snapshot?: DocumentSnapshot + document: ControlledDocument + approvals: DocumentApprovalState[] + modifiedOn?: Timestamp +} + +export function extractValidationWorkflow ( + hierarchy: Hierarchy, + bundle: DocumentBundle, + accountIdToPerson: (ref: Ref) => Ref | undefined +): Map, DocumentValidationState[]> { + const result: ReturnType = new Map() + + const getApprovalStates = (request: DocumentRequest | undefined): DocumentApprovalState[] => { + if (request === undefined) return [] + + const role = hierarchy.isDerived(request._class, documents.class.DocumentReviewRequest) ? 'reviewer' : 'approver' + + const rejected: DocumentApprovalState[] = + request.rejected !== undefined + ? [ + { + person: request.rejected, + role, + state: 'rejected', + timestamp: request.modifiedOn + } + ] + : [] + + const approved: DocumentApprovalState[] = request.approved.map((person, idx) => { + return { + person, + role, + state: 'approved', + timestamp: request.approvedDates?.[idx] ?? request.modifiedOn + } + }) + + const ignored: DocumentApprovalState[] = request.requested + .filter((person) => person !== request.rejected) + .filter((person) => !request.approved.includes(person)) + .map((person) => { + return { + person, + role, + state: request.rejected !== undefined ? 'cancelled' : 'waiting' + } + }) + + const states = [...rejected, ...approved, ...ignored] + + const messages = bundle.ChatMessage.filter((m) => m.attachedTo === request._id) + for (const state of states) { + state.messages = messages.filter((m) => accountIdToPerson(m.createdBy as Ref) === state.person) + } + + return states + } + + for (const document of bundle.ControlledDocument) { + const snapshots = bundle.DocumentSnapshot.filter((s) => s.attachedTo === document._id).sort( + (a, b) => (a.createdOn ?? 0) - (b.createdOn ?? 0) + ) + const requests = bundle.DocumentRequest.filter((s) => s.attachedTo === document._id).sort( + (a, b) => (a.createdOn ?? 0) - (b.createdOn ?? 0) + ) + + const states: DocumentValidationState[] = [...snapshots, undefined].map((snapshot) => { + return { + requests: [], + snapshot, + document, + approvals: [], + messages: [] + } + }) + + for (const request of requests) { + const state = + states.find((s) => (s.snapshot?.createdOn ?? 0) > (request.createdOn ?? 0)) ?? states[states.length - 1] + state.requests.push(request) + } + + for (const state of states) { + const review = state.requests.findLast((r) => + hierarchy.isDerived(r._class, documents.class.DocumentReviewRequest) + ) + let approval = state.requests.findLast((r) => + hierarchy.isDerived(r._class, documents.class.DocumentApprovalRequest) + ) + + if ((approval?.createdOn ?? 0) < (review?.createdOn ?? 0)) approval = undefined + + const anchor = review ?? approval + const author = + anchor?.createdBy !== undefined + ? accountIdToPerson?.(anchor.createdBy as Ref) ?? document.author + : document.author + + state.approvals = [ + { + person: author, + role: 'author', + state: anchor !== undefined ? 'approved' : 'waiting', + timestamp: anchor !== undefined ? anchor.createdOn ?? document.createdOn : undefined + }, + ...getApprovalStates(review), + ...getApprovalStates(approval) + ] + + if (state.requests.length > 0) { + state.modifiedOn = Math.max(...state.requests.map((r) => r.modifiedOn ?? 0)) + } + } + + states.reverse() + result.set(document._id, states) + } + + return result +} + /** * @public */ diff --git a/qms-tests/sanity/tests/documents/documents.spec.ts b/qms-tests/sanity/tests/documents/documents.spec.ts index 18228d0633..de0eb170d5 100644 --- a/qms-tests/sanity/tests/documents/documents.spec.ts +++ b/qms-tests/sanity/tests/documents/documents.spec.ts @@ -950,7 +950,7 @@ test.describe('QMS. Documents tests', () => { await test.step('9. Send for Approval', async () => { await documentContentPage.buttonSendForApproval.click() - await documentContentPage.fillSelectApproversForm([reviewer]) + await documentContentPage.fillSelectApproversForm([reviewer], true) await documentContentPage.checkDocumentStatus(DocumentStatus.IN_APPROVAL) await documentContentPage.checkDocument({ ...documentDetails, diff --git a/qms-tests/sanity/tests/model/documents/document-approvals-page.ts b/qms-tests/sanity/tests/model/documents/document-approvals-page.ts index e9c31cf7d7..92b95f2b4c 100644 --- a/qms-tests/sanity/tests/model/documents/document-approvals-page.ts +++ b/qms-tests/sanity/tests/model/documents/document-approvals-page.ts @@ -12,15 +12,16 @@ export class DocumentApprovalsPage extends DocumentCommonPage { async checkRejectApproval (approvalName: string, message: string): Promise { await expect( this.page - .locator('div.reject-message', { hasText: message }) + .locator('div.approval-status-message', { hasText: message }) .locator('xpath=..') .locator('div.approver span.ap-label') + .last() ).toHaveText(approvalName) } async checkSuccessApproval (approvalName: string): Promise { - await expect(this.page.locator('svg[fill*="accepted"]').locator('xpath=../..').locator('span.ap-label')).toHaveText( - approvalName - ) + await expect( + this.page.locator('svg[fill*="accepted"]').locator('xpath=../..').locator('span.ap-label').last() + ).toHaveText(approvalName) } } 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 62f6c18ebb..2cf6d82713 100644 --- a/qms-tests/sanity/tests/model/documents/document-content-page.ts +++ b/qms-tests/sanity/tests/model/documents/document-content-page.ts @@ -733,14 +733,14 @@ export class DocumentContentPage extends DocumentCommonPage { await this.confirmSubmission() } - async fillSelectApproversForm (approvers: Array): Promise { + async fillSelectApproversForm (approvers: Array, skipConfirm: boolean = false): Promise { await this.buttonAddMembers.click() for (const approver of approvers) { await this.selectListItemWithSearch(this.page, approver) } await this.textSelectApproversPopup.click({ force: true }) await this.buttonSelectMemberSubmit.click() - await this.confirmSubmission() + if (!skipConfirm) await this.confirmSubmission() } async checkCurrentRights (right: DocumentRights): Promise {