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 {
]